vx_installer/formats/
tar.rs1use super::FormatHandler;
4use crate::{progress::ProgressContext, Error, Result};
5use std::io::Read;
6use std::path::{Path, PathBuf};
7
8pub struct TarHandler;
10
11impl TarHandler {
12 pub fn new() -> Self {
14 Self
15 }
16
17 fn detect_compression(&self, file_path: &Path) -> CompressionType {
19 if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
20 if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
21 CompressionType::Gzip
22 } else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
23 CompressionType::Xz
24 } else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
25 CompressionType::Bzip2
26 } else if filename.ends_with(".tar") {
27 CompressionType::None
28 } else {
29 CompressionType::Unknown
30 }
31 } else {
32 CompressionType::Unknown
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
39enum CompressionType {
40 None,
41 Gzip,
42 Xz,
43 Bzip2,
44 Unknown,
45}
46
47#[async_trait::async_trait]
48impl FormatHandler for TarHandler {
49 fn name(&self) -> &str {
50 "tar"
51 }
52
53 fn can_handle(&self, file_path: &Path) -> bool {
54 if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
55 filename.ends_with(".tar")
56 || filename.ends_with(".tar.gz")
57 || filename.ends_with(".tgz")
58 || filename.ends_with(".tar.xz")
59 || filename.ends_with(".txz")
60 || filename.ends_with(".tar.bz2")
61 || filename.ends_with(".tbz2")
62 } else {
63 false
64 }
65 }
66
67 async fn extract(
68 &self,
69 source_path: &Path,
70 target_dir: &Path,
71 progress: &ProgressContext,
72 ) -> Result<Vec<PathBuf>> {
73 std::fs::create_dir_all(target_dir)?;
75
76 let compression = self.detect_compression(source_path);
77
78 progress.start("Extracting TAR archive", None).await?;
79
80 let file = std::fs::File::open(source_path)?;
81 let mut extracted_files = Vec::new();
82
83 match compression {
84 CompressionType::None => {
85 self.extract_tar(file, target_dir, &mut extracted_files)
86 .await?;
87 }
88 CompressionType::Gzip => {
89 let decoder = flate2::read::GzDecoder::new(file);
90 self.extract_tar(decoder, target_dir, &mut extracted_files)
91 .await?;
92 }
93 CompressionType::Xz => {
94 return Err(Error::unsupported_format("tar.xz"));
96 }
97 CompressionType::Bzip2 => {
98 return Err(Error::unsupported_format("tar.bz2"));
100 }
101 CompressionType::Unknown => {
102 return Err(Error::unsupported_format("unknown tar format"));
103 }
104 }
105
106 progress.finish("TAR extraction completed").await?;
107
108 Ok(extracted_files)
109 }
110}
111
112impl TarHandler {
113 async fn extract_tar<R: Read>(
115 &self,
116 reader: R,
117 target_dir: &Path,
118 extracted_files: &mut Vec<PathBuf>,
119 ) -> Result<()> {
120 let mut archive = tar::Archive::new(reader);
121
122 for entry in archive.entries()? {
123 let mut entry = entry?;
124 let path = entry.path()?;
125 let target_path = target_dir.join(&path);
126
127 if let Some(parent) = target_path.parent() {
129 std::fs::create_dir_all(parent)?;
130 }
131
132 if entry.header().entry_type().is_dir() {
134 std::fs::create_dir_all(&target_path)?;
135 } else {
136 entry.unpack(&target_path)?;
137
138 #[cfg(unix)]
140 {
141 let mode = entry.header().mode()?;
142 if mode & 0o111 != 0 {
143 self.make_executable(&target_path)?;
144 }
145 }
146
147 extracted_files.push(target_path);
148 }
149 }
150
151 Ok(())
152 }
153}
154
155impl Default for TarHandler {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[tokio::test]
166 async fn test_tar_handler_can_handle() {
167 let handler = TarHandler::new();
168
169 assert!(handler.can_handle(Path::new("test.tar")));
170 assert!(handler.can_handle(Path::new("test.tar.gz")));
171 assert!(handler.can_handle(Path::new("test.tgz")));
172 assert!(handler.can_handle(Path::new("test.tar.xz")));
173 assert!(handler.can_handle(Path::new("test.tar.bz2")));
174 assert!(!handler.can_handle(Path::new("test.zip")));
175 assert!(!handler.can_handle(Path::new("test.exe")));
176 }
177
178 #[tokio::test]
179 async fn test_tar_handler_name() {
180 let handler = TarHandler::new();
181 assert_eq!(handler.name(), "tar");
182 }
183
184 #[test]
185 fn test_compression_detection() {
186 let handler = TarHandler::new();
187
188 assert!(matches!(
189 handler.detect_compression(Path::new("test.tar")),
190 CompressionType::None
191 ));
192 assert!(matches!(
193 handler.detect_compression(Path::new("test.tar.gz")),
194 CompressionType::Gzip
195 ));
196 assert!(matches!(
197 handler.detect_compression(Path::new("test.tgz")),
198 CompressionType::Gzip
199 ));
200 assert!(matches!(
201 handler.detect_compression(Path::new("test.tar.xz")),
202 CompressionType::Xz
203 ));
204 }
205}