winbrew_core\fs\archive\extract/
gzip.rs1use std::fs;
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4
5use flate2::read::GzDecoder;
6
7use crate::fs::{FsError, Result};
8
9pub(crate) fn extract_gzip_archive(gzip_path: &Path, destination_dir: &Path) -> Result<()> {
10 let file = fs::File::open(gzip_path).map_err(|err| FsError::open_archive(gzip_path, err))?;
11 let mut decoder = GzDecoder::new(file);
12
13 fs::create_dir_all(destination_dir)
14 .map_err(|err| FsError::create_directory(destination_dir, err))?;
15
16 let output_path = output_path_for_gzip_archive(gzip_path, destination_dir)?;
17 let temp_output_path = temporary_output_path_for(&output_path);
18 let mut output_file = fs::File::create(&temp_output_path)
19 .map_err(|err| FsError::create_extracted_file(&temp_output_path, err))?;
20
21 let copy_result = copy_gzip_contents(
22 gzip_path,
23 &output_path,
24 &temp_output_path,
25 &mut decoder,
26 &mut output_file,
27 );
28
29 drop(output_file);
30
31 if let Err(err) = copy_result {
32 let _ = fs::remove_file(&temp_output_path);
33 return Err(err);
34 }
35
36 fs::rename(&temp_output_path, &output_path)
37 .map_err(|err| FsError::finalize_file(&temp_output_path, &output_path, err))?;
38
39 Ok(())
40}
41
42fn copy_gzip_contents(
43 gzip_path: &Path,
44 output_path: &Path,
45 temp_output_path: &Path,
46 decoder: &mut GzDecoder<fs::File>,
47 output_file: &mut fs::File,
48) -> Result<()> {
49 let mut buffer = [0u8; 8 * 1024];
50
51 loop {
52 let bytes_read = decoder
53 .read(&mut buffer)
54 .map_err(|err| FsError::read_archive_entry(gzip_path, err))?;
55
56 if bytes_read == 0 {
57 break;
58 }
59
60 output_file
61 .write_all(&buffer[..bytes_read])
62 .map_err(|err| FsError::write_entry(output_path, err))?;
63 }
64
65 output_file
66 .sync_all()
67 .map_err(|err| FsError::sync_temp_file(temp_output_path, err))?;
68
69 Ok(())
70}
71
72fn output_path_for_gzip_archive(gzip_path: &Path, destination_dir: &Path) -> Result<PathBuf> {
73 let file_name = gzip_path
74 .file_name()
75 .and_then(|name| name.to_str())
76 .ok_or_else(|| {
77 FsError::open_archive(
78 gzip_path,
79 io::Error::new(
80 io::ErrorKind::InvalidInput,
81 "gzip archive path has no file name",
82 ),
83 )
84 })?;
85
86 let lower_file_name = file_name.to_ascii_lowercase();
87 if !lower_file_name.ends_with(".gz") {
88 return Err(FsError::open_archive(
89 gzip_path,
90 io::Error::new(
91 io::ErrorKind::InvalidInput,
92 "gzip archive path does not end with .gz",
93 ),
94 ));
95 }
96
97 let output_name = &file_name[..file_name.len() - 3];
98 if output_name.is_empty() {
99 return Err(FsError::open_archive(
100 gzip_path,
101 io::Error::new(
102 io::ErrorKind::InvalidInput,
103 "gzip archive path resolves to an empty output name",
104 ),
105 ));
106 }
107
108 Ok(destination_dir.join(output_name))
109}
110
111fn temporary_output_path_for(output_path: &Path) -> PathBuf {
112 let file_name = output_path
113 .file_name()
114 .and_then(|name| name.to_str())
115 .unwrap_or("output");
116
117 output_path.with_file_name(format!("{file_name}.tmp"))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::fs;
124 use std::io::Write;
125 use tempfile::tempdir;
126
127 fn create_gz_archive(path: &std::path::Path, contents: &[u8]) {
128 let file = fs::File::create(path).expect("create gz file");
129 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
130
131 encoder.write_all(contents).expect("write gz contents");
132 encoder.finish().expect("finish gz file");
133 }
134
135 #[test]
136 fn extract_gzip_archive_extracts_file() {
137 let temp_dir = tempdir().expect("temp dir");
138 let destination_dir = temp_dir.path().join("dest");
139 let archive_path = temp_dir.path().join("tool.exe.gz");
140
141 fs::create_dir_all(&destination_dir).expect("destination dir");
142 create_gz_archive(&archive_path, b"gzip payload");
143
144 extract_gzip_archive(&archive_path, &destination_dir).expect("gzip extraction");
145
146 assert_eq!(
147 fs::read(destination_dir.join("tool.exe")).expect("read"),
148 b"gzip payload"
149 );
150 }
151}