winbrew_core\fs\archive\extract/
tar.rs1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Component, Path, PathBuf};
4
5use bzip2::read::BzDecoder;
6use flate2::read::GzDecoder;
7
8use crate::fs::{FsError, Result};
9
10use super::super::context::ExtractionContext;
11use super::super::limits::ExtractionLimits;
12use super::super::platform::PlatformAdapter;
13
14const TAR_COPY_BUFFER_SIZE: usize = 256 * 1024;
15
16pub(crate) fn extract_tar_archive_with_platform<P: PlatformAdapter>(
17 archive_path: &Path,
18 destination_dir: &Path,
19 limits: ExtractionLimits,
20) -> Result<()> {
21 let archive_file =
22 fs::File::open(archive_path).map_err(|err| FsError::open_archive(archive_path, err))?;
23 let archive_size = fs::metadata(archive_path)
24 .map_err(|err| FsError::open_archive(archive_path, err))?
25 .len();
26 let reader = archive_reader_for_path(archive_path, archive_file);
27 let mut archive = tar::Archive::new(reader);
28 let mut extraction = ExtractionContext::<P>::new(limits);
29 let mut buffer = vec![0u8; TAR_COPY_BUFFER_SIZE];
30
31 let entries = archive
32 .entries()
33 .map_err(|err| FsError::read_archive_entry(archive_path, err))?;
34
35 for entry in entries {
36 let mut entry = entry.map_err(|err| FsError::read_archive_entry(archive_path, err))?;
37 extract_entry(
38 &mut entry,
39 archive_size,
40 destination_dir,
41 &mut extraction,
42 &mut buffer,
43 )?;
44 }
45
46 extraction.commit();
47 Ok(())
48}
49
50fn archive_reader_for_path(archive_path: &Path, file: fs::File) -> Box<dyn Read> {
51 let file_name = archive_path
52 .file_name()
53 .and_then(|name| name.to_str())
54 .unwrap_or_default()
55 .to_ascii_lowercase();
56
57 if file_name.ends_with(".tar.gz") || file_name.ends_with(".tgz") {
58 Box::new(GzDecoder::new(file))
59 } else if file_name.ends_with(".tbz2") || file_name.ends_with(".tar.bz2") {
60 Box::new(BzDecoder::new(file))
61 } else {
62 Box::new(file)
63 }
64}
65
66fn extract_entry<P: PlatformAdapter, R: Read>(
67 entry: &mut tar::Entry<'_, R>,
68 archive_size: u64,
69 destination_dir: &Path,
70 extraction: &mut ExtractionContext<P>,
71 buffer: &mut [u8],
72) -> Result<()> {
73 let entry_path = entry
74 .path()
75 .map_err(|_| FsError::invalid_archive_entry_path())?;
76 let enclosed_name = sanitize_entry_path(entry_path.as_ref())?;
77
78 let outpath = destination_dir.join(&enclosed_name);
79
80 extraction.validate_target(&outpath, destination_dir)?;
81 extraction.check_limits(&enclosed_name, entry.size(), archive_size)?;
82
83 let entry_type = entry.header().entry_type();
84
85 if entry_type.is_symlink() {
86 return Err(FsError::symlink_entry(&outpath));
87 }
88
89 if entry_type.is_hard_link() {
90 return Err(FsError::unsupported_entry(&outpath));
91 }
92
93 if entry_type.is_dir() {
94 extraction.ensure_directory_tree(&outpath)?;
95 return Ok(());
96 }
97
98 if !entry_type.is_file() {
99 return Err(FsError::unsupported_entry(&outpath));
100 }
101
102 if let Some(parent) = outpath.parent() {
103 extraction.ensure_directory_tree(parent)?;
104 }
105
106 let mut outfile = P::create_extraction_target_file(&outpath)
107 .map_err(|err| FsError::create_extracted_file(&outpath, err))?;
108 extraction.record_file(&outpath);
109
110 loop {
111 let bytes_read = entry
112 .read(buffer)
113 .map_err(|err| FsError::read_entry(&outpath, err))?;
114 if bytes_read == 0 {
115 break;
116 }
117
118 outfile
119 .write_all(&buffer[..bytes_read])
120 .map_err(|err| FsError::write_entry(&outpath, err))?;
121 }
122
123 Ok(())
124}
125
126fn sanitize_entry_path(path: &Path) -> Result<PathBuf> {
127 let mut enclosed = PathBuf::new();
128
129 for component in path.components() {
130 match component {
131 Component::Normal(part) => enclosed.push(part),
132 Component::CurDir => {}
133 _ => return Err(FsError::invalid_archive_entry_path()),
134 }
135 }
136
137 if enclosed.as_os_str().is_empty() {
138 return Err(FsError::invalid_archive_entry_path());
139 }
140
141 Ok(enclosed)
142}
143
144#[cfg(test)]
145mod tests {
146 use crate::fs::archive::{ArchiveKind, extract_archive};
147 use std::fs;
148 use tempfile::tempdir;
149
150 fn create_tar_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
151 let file = fs::File::create(path).expect("create tar file");
152 let mut builder = tar::Builder::new(file);
153 let mut header = tar::Header::new_gnu();
154 header.set_size(contents.len() as u64);
155 header.set_mode(0o644);
156 header.set_cksum();
157
158 builder
159 .append_data(&mut header, file_name, contents)
160 .expect("append tar entry");
161 builder.finish().expect("finish tar file");
162 }
163
164 fn create_tar_gz_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
165 let file = fs::File::create(path).expect("create tar.gz file");
166 let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
167 let mut builder = tar::Builder::new(encoder);
168 let mut header = tar::Header::new_gnu();
169 header.set_size(contents.len() as u64);
170 header.set_mode(0o644);
171 header.set_cksum();
172
173 builder
174 .append_data(&mut header, file_name, contents)
175 .expect("append tar.gz entry");
176 let encoder = builder.into_inner().expect("finish tar builder");
177 encoder.finish().expect("finish tar.gz file");
178 }
179
180 fn create_tar_bz2_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
181 let file = fs::File::create(path).expect("create tar.bz2 file");
182 let encoder = bzip2::write::BzEncoder::new(file, bzip2::Compression::default());
183 let mut builder = tar::Builder::new(encoder);
184 let mut header = tar::Header::new_gnu();
185 header.set_size(contents.len() as u64);
186 header.set_mode(0o644);
187 header.set_cksum();
188
189 builder
190 .append_data(&mut header, file_name, contents)
191 .expect("append tar.bz2 entry");
192 let encoder = builder.into_inner().expect("finish tar builder");
193 encoder.finish().expect("finish tar.bz2 file");
194 }
195
196 #[test]
197 fn extract_tar_archive_extracts_plain_tar() {
198 let temp_dir = tempdir().expect("temp dir");
199 let destination_dir = temp_dir.path().join("dest");
200 let archive_path = temp_dir.path().join("archive.tar");
201
202 fs::create_dir_all(&destination_dir).expect("destination dir");
203 create_tar_archive(&archive_path, "bin/tool.exe", b"tar payload");
204
205 extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir).expect("tar extraction");
206
207 assert_eq!(
208 fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
209 b"tar payload"
210 );
211 }
212
213 #[test]
214 fn extract_tar_archive_extracts_tar_gz() {
215 let temp_dir = tempdir().expect("temp dir");
216 let destination_dir = temp_dir.path().join("dest");
217 let archive_path = temp_dir.path().join("archive.tar.gz");
218
219 fs::create_dir_all(&destination_dir).expect("destination dir");
220 create_tar_gz_archive(&archive_path, "bin/tool.exe", b"tar gz payload");
221
222 extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir)
223 .expect("tar.gz extraction");
224
225 assert_eq!(
226 fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
227 b"tar gz payload"
228 );
229 }
230
231 #[test]
232 fn extract_tar_archive_extracts_tar_bz2() {
233 let temp_dir = tempdir().expect("temp dir");
234 let destination_dir = temp_dir.path().join("dest");
235 let archive_path = temp_dir.path().join("archive.tar.bz2");
236
237 fs::create_dir_all(&destination_dir).expect("destination dir");
238 create_tar_bz2_archive(&archive_path, "bin/tool.exe", b"tar bz2 payload");
239
240 extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir)
241 .expect("tar.bz2 extraction");
242
243 assert_eq!(
244 fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
245 b"tar bz2 payload"
246 );
247 }
248}