Skip to main content

acorn/io/
archive.rs

1//! Archive creation and extraction utilities.
2use crate::io::{files_all, standard_project_folder, ApiResult, PathConversion};
3use crate::prelude::{create_dir_all, io, remove_file, ErrorKind, File, OpenOptions, Path, PathBuf, Read, Write};
4use crate::util::constants::app::{ARCHIVE_INFERENCE_BYTES, MAX_ARCHIVE_ENTRIES, MAX_ARCHIVE_EXPANDED_BYTES};
5use crate::util::MimeType;
6use color_eyre::Report;
7use core::fmt;
8use core::iter::once;
9use flate2::read::GzDecoder;
10use flate2::write::GzEncoder;
11use flate2::Compression;
12use sevenz_rust2::{ArchiveEntry, ArchiveReader, ArchiveWriter, Password};
13use tar::{Archive as TarArchive, Builder as TarBuilder};
14use zip::write::SimpleFileOptions;
15use zip::{ZipArchive, ZipWriter};
16
17/// Archive creation operations implemented by archive candidates
18pub trait ArchiveCreation {
19    /// Creates a 7z archive
20    fn archive_7z(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
21    /// Creates an uncompressed TAR archive
22    fn archive_tar(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
23    /// Creates a gzip-compressed TAR archive
24    fn archive_tar_gzip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
25    /// Creates a ZIP archive
26    fn archive_zip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
27}
28trait ArchiveEntryExt {
29    fn extract(&self, reader: &mut dyn Read, root: &Path) -> Result<bool, sevenz_rust2::Error>;
30    fn validate(&self, seen: Vec<PathBuf>) -> ApiResult<Vec<PathBuf>>;
31}
32/// Archive extraction operations implemented by archive candidates
33pub trait ArchiveExtraction {
34    /// Extracts a 7z archive
35    fn extract_7z(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
36    /// Extracts an uncompressed TAR archive
37    fn extract_tar(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
38    /// Extracts a gzip-compressed TAR archive
39    fn extract_tar_gzip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
40    /// Extracts a ZIP archive
41    fn extract_zip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
42}
43/// Operations supported by archive MIME types.
44pub trait ArchiveFormat {
45    /// Creates an archive from `source`.
46    fn archive(&self, candidate: ArchiveCandidate, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
47    /// Extracts an archive into `destination`.
48    fn extract(&self, candidate: ArchiveCandidate, destination: Option<PathBuf>) -> ApiResult<PathBuf>;
49}
50#[derive(Debug)]
51enum ArchiveError {
52    DestinationInsideSource,
53    DestinationInvalid,
54    DestinationMissingFilename,
55    DuplicatePath(PathBuf),
56    EntryExpandedSizeLimit,
57    ExpandedSizeLimit,
58    ExpandedSizeOverflow,
59    FilePathEmpty,
60    FormatInference(PathBuf),
61    InspectArchive { path: PathBuf, reason: Box<str> },
62    InspectSourceChild { path: PathBuf, reason: Box<str> },
63    OutputInvalid,
64    ReadArchive { path: PathBuf, reason: Box<str> },
65    ReplaceDestination(Box<str>),
66    ResolveSource(Box<str>),
67    SevenZip { operation: &'static str, reason: Box<str> },
68    SourceLink(PathBuf),
69    TooManyEntries,
70    UnsafeDestinationComponent(PathBuf),
71    UnsafeZipPath(Box<str>),
72    UnsupportedFormat(MimeType),
73    UnsupportedTarEntry,
74    ZipLink(PathBuf),
75}
76/// A local path that may be created as or extracted from an archive.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct ArchiveCandidate(PathBuf);
79impl ArchiveCandidate {
80    /// Creates an archive using the selected MIME type.
81    pub fn archive(self, destination: Option<PathBuf>, mime_type: MimeType) -> ApiResult<PathBuf> {
82        mime_type.archive(self, destination)
83    }
84    /// Extracts this archive, inferring its format when no override is supplied.
85    pub fn extract(self, destination: Option<PathBuf>, archive_format: Option<MimeType>) -> ApiResult<PathBuf> {
86        archive_format
87            .map_or_else(|| self.infer(), Ok)
88            .and_then(|format| format.extract(self, destination))
89    }
90    fn append_tar<W: Write>(self, writer: W) -> ApiResult<()> {
91        self.relative_children().and_then(|children| {
92            children
93                .into_iter()
94                .try_fold(TarBuilder::new(writer), |mut archive, (path, relative)| {
95                    archive.append_path_with_name(path, relative).map(|()| archive).map_err(Into::into)
96                })
97                .and_then(|mut archive| archive.finish().map_err(Into::into))
98        })
99    }
100    fn infer(&self) -> ApiResult<MimeType> {
101        File::open(&self.0)
102            .map_err(|why| ArchiveError::ReadArchive {
103                path: self.0.clone(),
104                reason: why.to_string().into(),
105            })
106            .and_then(|file| {
107                let mut header = Vec::new();
108                file.take(ARCHIVE_INFERENCE_BYTES)
109                    .read_to_end(&mut header)
110                    .map(|_| header)
111                    .map_err(|why| ArchiveError::InspectArchive {
112                        path: self.0.clone(),
113                        reason: why.to_string().into(),
114                    })
115            })
116            .map_err(Into::into)
117            .and_then(|header| MimeType::infer(&header).ok_or_else(|| ArchiveError::FormatInference(self.0.clone()).into()))
118    }
119    fn prepare_output(&self, destination: Option<PathBuf>, format: MimeType) -> ApiResult<(File, PathBuf)> {
120        let output = destination.unwrap_or_else(|| self.0.with_extension(format.file_type()));
121        self.0
122            .canonicalize()
123            .map_err(|why| Report::from(ArchiveError::ResolveSource(why.to_string().into())))
124            .and_then(|root| {
125                let parent = output
126                    .parent()
127                    .filter(|path| !path.as_os_str().is_empty())
128                    .unwrap_or_else(|| Path::new("."));
129                parent
130                    .canonicalize()
131                    .map_err(Into::into)
132                    .and_then(|parent| {
133                        output
134                            .file_name()
135                            .map(|name| parent.join(name))
136                            .ok_or_else(|| ArchiveError::DestinationMissingFilename.into())
137                    })
138                    .map(|output| (root, output))
139            })
140            .and_then(|(root, resolved)| match resolved.starts_with(&root) {
141                | true => Err(ArchiveError::DestinationInsideSource.into()),
142                | false => prepare_archive_destination(&output).map(|file| (file, output)),
143            })
144    }
145    fn relative_children(&self) -> ApiResult<Vec<(PathBuf, PathBuf)>> {
146        self.0
147            .canonicalize()
148            .map_err(|why| Report::from(ArchiveError::ResolveSource(why.to_string().into())))
149            .and_then(|root| {
150                files_all(root.clone(), None::<Vec<String>>)
151                    .into_iter()
152                    .map(|path| {
153                        path.symlink_metadata()
154                            .map_err(|why| {
155                                Report::from(ArchiveError::InspectSourceChild {
156                                    path: path.clone(),
157                                    reason: why.to_string().into(),
158                                })
159                            })
160                            .and_then(|metadata| match metadata.file_type().is_symlink() {
161                                | true => Err(ArchiveError::SourceLink(path.clone()).into()),
162                                | false => path.canonicalize().map_err(Into::into).and_then(|absolute| {
163                                    absolute
164                                        .strip_prefix(&root)
165                                        .map(Path::to_path_buf)
166                                        .map(|relative| (absolute, relative))
167                                        .map_err(Into::into)
168                                }),
169                            })
170                    })
171                    .collect()
172            })
173    }
174}
175impl ArchiveCreation for ArchiveCandidate {
176    fn archive_7z(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
177        self.prepare_output(destination, MimeType::SevenZip).and_then(|(file, output)| {
178            ArchiveWriter::new(file)
179                .map_err(|why| {
180                    Report::from(ArchiveError::SevenZip {
181                        operation: "create 7z archive",
182                        reason: why.to_string().into(),
183                    })
184                })
185                .and_then(|writer| {
186                    self.relative_children().and_then(|children| {
187                        children
188                            .into_iter()
189                            .try_fold(writer, |mut writer, (path, relative)| {
190                                let entry = ArchiveEntry::from_path(&path, relative.to_string_lossy().replace('\\', "/"));
191                                path.is_file()
192                                    .then(|| File::open(&path))
193                                    .transpose()
194                                    .map_err(Into::into)
195                                    .and_then(|reader| match writer.push_archive_entry(entry, reader) {
196                                        | Ok(_) => Ok(writer),
197                                        | Err(why) => Err(ArchiveError::SevenZip {
198                                            operation: "add 7z entry",
199                                            reason: why.to_string().into(),
200                                        }
201                                        .into()),
202                                    })
203                            })
204                            .and_then(|writer| {
205                                writer.finish().map_err(|why| {
206                                    ArchiveError::SevenZip {
207                                        operation: "finish 7z archive",
208                                        reason: why.to_string().into(),
209                                    }
210                                    .into()
211                                })
212                            })
213                    })
214                })
215                .map(|_| output)
216        })
217    }
218    fn archive_tar(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
219        self.prepare_output(destination, MimeType::Tar)
220            .and_then(|(file, output)| self.append_tar(file).map(|()| output))
221    }
222    fn archive_tar_gzip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
223        self.prepare_output(destination, MimeType::Gzip)
224            .and_then(|(file, output)| self.append_tar(GzEncoder::new(file, Compression::default())).map(|()| output))
225    }
226    fn archive_zip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
227        self.prepare_output(destination, MimeType::Zip).and_then(|(file, output)| {
228            let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
229            self.relative_children()
230                .and_then(|children| {
231                    children
232                        .into_iter()
233                        .try_fold(ZipWriter::new(file), |mut writer, (path, relative)| match path.is_dir() {
234                            | true => writer.add_directory_from_path(relative, options).map(|()| writer).map_err(Into::into),
235                            | false => writer
236                                .start_file_from_path(relative, options)
237                                .map_err(Into::into)
238                                .and_then(|()| File::open(path).map_err(Into::into))
239                                .and_then(|mut input| io::copy(&mut input, &mut writer).map_err(Into::into))
240                                .map(|_| writer),
241                        })
242                })
243                .and_then(|writer| writer.finish().map_err(Into::into))
244                .map(|_| output)
245        })
246    }
247}
248impl ArchiveExtraction for ArchiveCandidate {
249    fn extract_7z(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
250        extraction_root(destination).and_then(|root| {
251            File::open(&self.0)
252                .map_err(Into::into)
253                .and_then(|file| {
254                    ArchiveReader::new(file, Password::empty()).map_err(|why| {
255                        ArchiveError::SevenZip {
256                            operation: "read 7z archive",
257                            reason: why.to_string().into(),
258                        }
259                        .into()
260                    })
261                })
262                .and_then(|mut archive| {
263                    let validation = {
264                        let entries = archive.archive().files.as_slice();
265                        let size = entries.iter().try_fold(0_u64, |total, entry| {
266                            total.checked_add(entry.size()).ok_or_else(|| ArchiveError::ExpandedSizeOverflow.into())
267                        });
268                        match (entries.len() > MAX_ARCHIVE_ENTRIES, size) {
269                            | (true, _) => Err(ArchiveError::TooManyEntries.into()),
270                            | (_, Ok(size)) if size > MAX_ARCHIVE_EXPANDED_BYTES => Err(ArchiveError::ExpandedSizeLimit.into()),
271                            | (_, Err(why)) => Err(why),
272                            | _ => entries
273                                .iter()
274                                .try_fold(Vec::new(), |seen, entry| entry.validate(seen))
275                                .and_then(|paths| paths.iter().try_for_each(|relative| safe_target(&root, relative).map(|_| ()))),
276                        }
277                    };
278                    validation.and_then(|()| {
279                        archive.for_each_entries(|entry, reader| entry.extract(reader, &root)).map_err(|why| {
280                            ArchiveError::SevenZip {
281                                operation: "extract 7z archive",
282                                reason: why.to_string().into(),
283                            }
284                            .into()
285                        })
286                    })
287                })
288                .map(|()| root)
289        })
290    }
291    fn extract_tar(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
292        File::open(self.0)
293            .map_err(Into::into)
294            .and_then(|file| extract_tar_reader(file, destination))
295    }
296    fn extract_tar_gzip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
297        File::open(self.0)
298            .map(GzDecoder::new)
299            .map_err(Into::into)
300            .and_then(|decoder| extract_tar_reader(decoder, destination))
301    }
302    fn extract_zip(self, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
303        extraction_root(destination).and_then(|root| {
304            File::open(&self.0)
305                .map_err(Into::into)
306                .and_then(|file| ZipArchive::new(file).map_err(Into::into))
307                .and_then(|mut archive| match archive.len() > MAX_ARCHIVE_ENTRIES {
308                    | true => Err(ArchiveError::TooManyEntries.into()),
309                    | false => (0..archive.len()).try_fold(Vec::new(), |seen, index| {
310                        archive.by_index(index).map_err(Into::into).and_then(|mut entry| {
311                            entry
312                                .enclosed_name()
313                                .ok_or_else(|| ArchiveError::UnsafeZipPath(entry.name().into()).into())
314                                .and_then(|path| path.relative())
315                                .and_then(|relative| validate_entry(&root, seen, relative, entry.is_dir(), entry.size()))
316                                .and_then(|(seen, target)| {
317                                    let mode = entry.unix_mode().unwrap_or_default() & 0o170000;
318                                    match mode == 0o120000 {
319                                        | true => Err(ArchiveError::ZipLink(target).into()),
320                                        | false => {
321                                            let is_directory = entry.is_dir();
322                                            write_entry(&mut entry, &target, is_directory).map(|()| seen)
323                                        }
324                                    }
325                                })
326                        })
327                    }),
328                })
329                .map(|_| root)
330        })
331    }
332}
333impl From<&Path> for ArchiveCandidate {
334    fn from(value: &Path) -> Self {
335        Self(value.to_path_buf())
336    }
337}
338impl From<PathBuf> for ArchiveCandidate {
339    fn from(value: PathBuf) -> Self {
340        Self(value)
341    }
342}
343impl ArchiveEntryExt for ArchiveEntry {
344    fn extract(&self, reader: &mut dyn Read, root: &Path) -> Result<bool, sevenz_rust2::Error> {
345        Path::new(self.name())
346            .relative()
347            .and_then(|relative| safe_target(root, &relative).map(|target| (relative, target)))
348            .and_then(|(relative, target)| write_entry(reader, &target, self.is_directory()).map(|()| relative))
349            .map(|_| true)
350            .map_err(|why| sevenz_rust2::Error::Other(why.to_string().into()))
351    }
352    fn validate(&self, seen: Vec<PathBuf>) -> ApiResult<Vec<PathBuf>> {
353        Path::new(self.name()).relative().and_then(|relative| match seen.contains(&relative) {
354            | true => Err(ArchiveError::DuplicatePath(relative).into()),
355            | false => Ok(seen.into_iter().chain(once(relative)).collect()),
356        })
357    }
358}
359impl core::error::Error for ArchiveError {}
360impl fmt::Display for ArchiveError {
361    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
362        match self {
363            | Self::DestinationInsideSource => write!(formatter, "Archive destination cannot be inside its source directory"),
364            | Self::DestinationInvalid => write!(formatter, "Archive destination is not a regular directory"),
365            | Self::DestinationMissingFilename => write!(formatter, "Archive destination must name a file"),
366            | Self::DuplicatePath(path) => write!(formatter, "Duplicate archive path: {}", path.display()),
367            | Self::EntryExpandedSizeLimit => write!(formatter, "Archive entry expands beyond the supported size limit"),
368            | Self::ExpandedSizeLimit => write!(formatter, "Archive expands beyond the supported size limit"),
369            | Self::ExpandedSizeOverflow => write!(formatter, "Archive expanded size overflow"),
370            | Self::FilePathEmpty => write!(formatter, "Archive file path cannot be empty"),
371            | Self::FormatInference(path) => write!(formatter, "Unable to infer archive format for {}", path.display()),
372            | Self::InspectArchive { path, reason } => write!(formatter, "Failed to inspect archive {} — {reason}", path.display()),
373            | Self::InspectSourceChild { path, reason } => {
374                write!(formatter, "Failed to inspect archive source child {} — {reason}", path.display())
375            }
376            | Self::OutputInvalid => write!(formatter, "Archive destination exists and is not a regular file"),
377            | Self::ReadArchive { path, reason } => write!(formatter, "Failed to read archive {} — {reason}", path.display()),
378            | Self::ReplaceDestination(reason) => write!(formatter, "Failed to replace archive destination — {reason}"),
379            | Self::ResolveSource(reason) => write!(formatter, "Failed to resolve archive source — {reason}"),
380            | Self::SevenZip { operation, reason } => write!(formatter, "Failed to {operation} — {reason}"),
381            | Self::SourceLink(path) => write!(formatter, "Archive source links are not supported: {}", path.display()),
382            | Self::TooManyEntries => write!(formatter, "Archive contains too many entries"),
383            | Self::UnsafeDestinationComponent(path) => {
384                write!(formatter, "Unsafe archive destination component: {}", path.display())
385            }
386            | Self::UnsafeZipPath(path) => write!(formatter, "Unsafe ZIP path: {path}"),
387            | Self::UnsupportedFormat(format) => write!(formatter, "Unsupported archive format: {format}"),
388            | Self::UnsupportedTarEntry => write!(formatter, "Unsupported TAR entry"),
389            | Self::ZipLink(path) => write!(formatter, "ZIP links are not supported: {}", path.display()),
390        }
391    }
392}
393impl ArchiveFormat for MimeType {
394    fn archive(&self, source: ArchiveCandidate, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
395        match self {
396            | Self::Gzip => source.archive_tar_gzip(destination),
397            | Self::SevenZip => source.archive_7z(destination),
398            | Self::Tar => source.archive_tar(destination),
399            | Self::Zip => source.archive_zip(destination),
400            | _ => Err(ArchiveError::UnsupportedFormat(self.clone()).into()),
401        }
402    }
403    fn extract(&self, source: ArchiveCandidate, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
404        match self {
405            | Self::Gzip => source.extract_tar_gzip(destination),
406            | Self::SevenZip => source.extract_7z(destination),
407            | Self::Tar => source.extract_tar(destination),
408            | Self::Zip => source.extract_zip(destination),
409            | _ => Err(ArchiveError::UnsupportedFormat(self.clone()).into()),
410        }
411    }
412}
413/// Creates an archive using the selected MIME type.
414pub fn archive(source: PathBuf, destination: Option<PathBuf>, mime_type: MimeType) -> ApiResult<PathBuf> {
415    ArchiveCandidate::from(source).archive(destination, mime_type)
416}
417/// Extracts an archive, inferring its format from a bounded content header when no override is supplied.
418pub fn extract(source: PathBuf, destination: Option<PathBuf>, archive_format: Option<MimeType>) -> ApiResult<PathBuf> {
419    ArchiveCandidate::from(source).extract(destination, archive_format)
420}
421fn extract_tar_reader<R: Read>(reader: R, destination: Option<PathBuf>) -> ApiResult<PathBuf> {
422    extraction_root(destination).and_then(|root| {
423        TarArchive::new(reader)
424            .entries()
425            .map_err(Into::into)
426            .and_then(|entries| {
427                entries
428                    .enumerate()
429                    .try_fold(Vec::new(), |seen, (index, entry)| match index >= MAX_ARCHIVE_ENTRIES {
430                        | true => Err(ArchiveError::TooManyEntries.into()),
431                        | false => entry.map_err(Into::into).and_then(|mut entry| {
432                            entry
433                                .path()
434                                .map_err(Into::into)
435                                .and_then(|path| path.as_ref().relative())
436                                .and_then(|relative| {
437                                    let is_directory = entry.header().entry_type().is_dir();
438                                    let is_file = entry.header().entry_type().is_file();
439                                    match (relative.as_os_str().is_empty(), is_directory, is_file) {
440                                        | (true, true, _) => Ok((seen, root.clone(), true)),
441                                        | (false, true, _) | (false, _, true) => validate_entry(&root, seen, relative, is_directory, entry.size())
442                                            .map(|(seen, target)| (seen, target, is_directory)),
443                                        | _ => Err(ArchiveError::UnsupportedTarEntry.into()),
444                                    }
445                                })
446                                .and_then(|(seen, target, is_directory)| write_entry(&mut entry, &target, is_directory).map(|()| seen))
447                        }),
448                    })
449            })
450            .map(|_| root)
451    })
452}
453fn extraction_root(destination: Option<PathBuf>) -> ApiResult<PathBuf> {
454    let root = destination.unwrap_or_else(|| standard_project_folder("extract", None));
455    match root.symlink_metadata() {
456        | Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => Err(ArchiveError::DestinationInvalid.into()),
457        | Ok(_) => Ok(root),
458        | Err(why) if why.kind() == ErrorKind::NotFound => create_dir_all(&root).map(|()| root).map_err(Into::into),
459        | Err(why) => Err(why.into()),
460    }
461}
462fn prepare_archive_destination(path: &Path) -> ApiResult<File> {
463    match path.symlink_metadata() {
464        | Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => Err(ArchiveError::OutputInvalid.into()),
465        | Ok(_) => remove_file(path).map_err(|why| ArchiveError::ReplaceDestination(why.to_string().into()).into()),
466        | Err(why) if why.kind() == ErrorKind::NotFound => Ok(()),
467        | Err(why) => Err(why.into()),
468    }
469    .and_then(|()| OpenOptions::new().write(true).create_new(true).open(path).map_err(Into::into))
470}
471fn safe_target(root: &Path, relative: &Path) -> ApiResult<PathBuf> {
472    relative
473        .ancestors()
474        .skip(1)
475        .map(|parent| root.join(parent))
476        .try_for_each(|parent| match parent.symlink_metadata() {
477            | Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => Err(ArchiveError::UnsafeDestinationComponent(parent).into()),
478            | Ok(_) => Ok(()),
479            | Err(why) if why.kind() == ErrorKind::NotFound => Ok(()),
480            | Err(why) => Err(why.into()),
481        })
482        .map(|()| root.join(relative))
483}
484fn validate_entry(root: &Path, seen: Vec<PathBuf>, relative: PathBuf, is_directory: bool, size: u64) -> ApiResult<(Vec<PathBuf>, PathBuf)> {
485    match (relative.as_os_str().is_empty(), size > MAX_ARCHIVE_EXPANDED_BYTES) {
486        | (true, _) if !is_directory => Err(ArchiveError::FilePathEmpty.into()),
487        | (_, true) => Err(ArchiveError::EntryExpandedSizeLimit.into()),
488        | _ if seen.contains(&relative) => Err(ArchiveError::DuplicatePath(relative).into()),
489        | _ => safe_target(root, &relative).map(|target| (seen.into_iter().chain(once(relative)).collect(), target)),
490    }
491}
492fn write_entry(reader: &mut dyn Read, target: &Path, is_directory: bool) -> ApiResult<()> {
493    match is_directory {
494        | true => create_dir_all(target).map_err(Into::into),
495        | false => target
496            .parent()
497            .map_or(Ok(()), |parent| create_dir_all(parent).map_err(Into::into))
498            .and_then(|()| OpenOptions::new().write(true).create_new(true).open(target).map_err(Into::into))
499            .and_then(|mut output| io::copy(reader, &mut output).map(|_| ()).map_err(Into::into)),
500    }
501}