1use flate2::Compression;
7use flate2::read::GzDecoder;
8use flate2::write::GzEncoder;
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::{Read, Write};
12use std::path::{Path, PathBuf};
13use tar::{Archive, Builder, Header};
14
15use crate::error::{CoreError, Result};
16use crate::manifest::Manifest;
17use crate::pack::LoadedPack;
18
19pub fn create_archive(pack: &LoadedPack, output: &Path) -> Result<PathBuf> {
29 let manifest = Manifest::generate(pack)?;
31 let manifest_content = manifest.to_string();
32
33 let file = File::create(output)?;
35 let encoder = GzEncoder::new(file, Compression::default());
36 let mut builder = Builder::new(encoder);
37
38 add_bytes_to_archive(&mut builder, "MANIFEST", manifest_content.as_bytes())?;
40
41 let pack_yaml = pack.root.join("Pack.yaml");
43 if pack_yaml.exists() {
44 add_file_to_archive(&mut builder, &pack_yaml, "Pack.yaml")?;
45 }
46
47 if pack.values_path.exists() {
49 add_file_to_archive(&mut builder, &pack.values_path, "values.yaml")?;
50 }
51
52 if let Some(schema_path) = &pack.schema_path
54 && schema_path.exists()
55 {
56 let schema_name = schema_path
57 .file_name()
58 .map(|n| n.to_string_lossy().to_string())
59 .unwrap_or_else(|| "values.schema.yaml".to_string());
60 add_file_to_archive(&mut builder, schema_path, &schema_name)?;
61 }
62
63 let template_files = pack.template_files()?;
65 for file_path in template_files {
66 let rel_path = file_path
67 .strip_prefix(&pack.root)
68 .unwrap_or(&file_path)
69 .to_string_lossy()
70 .replace('\\', "/");
72 add_file_to_archive(&mut builder, &file_path, &rel_path)?;
73 }
74
75 let encoder = builder.into_inner()?;
77 encoder.finish()?;
78
79 Ok(output.to_path_buf())
80}
81
82pub fn extract_archive(archive_path: &Path, dest: &Path) -> Result<()> {
84 let file = File::open(archive_path)?;
85 let decoder = GzDecoder::new(file);
86 let mut archive = Archive::new(decoder);
87
88 std::fs::create_dir_all(dest)?;
90
91 archive.unpack(dest)?;
92
93 Ok(())
94}
95
96pub fn list_archive(archive_path: &Path) -> Result<Vec<ArchiveEntry>> {
98 let file = File::open(archive_path)?;
99 let decoder = GzDecoder::new(file);
100 let mut archive = Archive::new(decoder);
101
102 let mut entries = Vec::new();
103
104 for entry in archive.entries()? {
105 let entry = entry?;
106 let path = entry.path()?.to_string_lossy().to_string();
107 let size = entry.header().size()?;
108 let is_dir = entry.header().entry_type().is_dir();
109
110 entries.push(ArchiveEntry { path, size, is_dir });
111 }
112
113 Ok(entries)
114}
115
116pub fn read_file_from_archive(archive_path: &Path, file_path: &str) -> Result<Vec<u8>> {
118 let file = File::open(archive_path)?;
119 let decoder = GzDecoder::new(file);
120 let mut archive = Archive::new(decoder);
121
122 for entry in archive.entries()? {
123 let mut entry = entry?;
124 let path = entry.path()?.to_string_lossy().to_string();
125
126 if path == file_path {
127 let mut content = Vec::new();
128 entry.read_to_end(&mut content)?;
129 return Ok(content);
130 }
131 }
132
133 Err(CoreError::Archive {
134 message: format!("File not found in archive: {}", file_path),
135 })
136}
137
138pub fn read_manifest_from_archive(archive_path: &Path) -> Result<Manifest> {
140 let content = read_file_from_archive(archive_path, "MANIFEST")?;
141 let text = String::from_utf8(content).map_err(|e| CoreError::Archive {
142 message: format!("Invalid UTF-8 in MANIFEST: {}", e),
143 })?;
144 Manifest::parse(&text)
145}
146
147fn read_all_files_from_archive(archive_path: &Path) -> Result<HashMap<String, Vec<u8>>> {
152 let file = File::open(archive_path)?;
153 let decoder = GzDecoder::new(file);
154 let mut archive = Archive::new(decoder);
155 let mut contents = HashMap::new();
156
157 for entry in archive.entries()? {
158 let mut entry = entry?;
159 if entry.header().entry_type().is_dir() {
160 continue;
161 }
162
163 let path = entry.path()?.to_string_lossy().to_string();
164 let mut data = Vec::new();
165 entry.read_to_end(&mut data)?;
166 contents.insert(path, data);
167 }
168
169 Ok(contents)
170}
171
172pub fn verify_archive(archive_path: &Path) -> Result<crate::manifest::VerificationResult> {
176 let manifest = read_manifest_from_archive(archive_path)?;
177
178 let file_contents = read_all_files_from_archive(archive_path)?;
180
181 manifest.verify_files(|path| {
182 file_contents
183 .get(path)
184 .cloned()
185 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"))
186 })
187}
188
189#[derive(Debug, Clone)]
191pub struct ArchiveEntry {
192 pub path: String,
194 pub size: u64,
196 pub is_dir: bool,
198}
199
200fn add_file_to_archive<W: Write>(
202 builder: &mut Builder<W>,
203 file_path: &Path,
204 archive_path: &str,
205) -> Result<()> {
206 let content = std::fs::read(file_path)?;
207 add_bytes_to_archive(builder, archive_path, &content)
208}
209
210fn add_bytes_to_archive<W: Write>(
212 builder: &mut Builder<W>,
213 archive_path: &str,
214 content: &[u8],
215) -> Result<()> {
216 let mut header = Header::new_gnu();
217 header.set_size(content.len() as u64);
218 header.set_mode(0o644);
219 header.set_mtime(0); header.set_cksum();
221
222 builder.append_data(&mut header, archive_path, content)?;
223
224 Ok(())
225}
226
227#[must_use]
229pub fn default_archive_name(pack: &LoadedPack) -> String {
230 format!(
231 "{}-{}.tar.gz",
232 pack.pack.metadata.name, pack.pack.metadata.version
233 )
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use tempfile::TempDir;
240
241 fn create_test_pack(dir: &Path) {
242 std::fs::write(
244 dir.join("Pack.yaml"),
245 r#"apiVersion: sherpack/v1
246kind: application
247metadata:
248 name: testpack
249 version: 1.0.0
250"#,
251 )
252 .unwrap();
253
254 std::fs::write(dir.join("values.yaml"), "replicas: 3\n").unwrap();
256
257 let templates_dir = dir.join("templates");
259 std::fs::create_dir_all(&templates_dir).unwrap();
260
261 std::fs::write(
263 templates_dir.join("deployment.yaml"),
264 "apiVersion: apps/v1\nkind: Deployment\n",
265 )
266 .unwrap();
267 }
268
269 #[test]
270 fn test_create_and_extract_archive() {
271 let temp = TempDir::new().unwrap();
272 let pack_dir = temp.path().join("pack");
273 std::fs::create_dir_all(&pack_dir).unwrap();
274 create_test_pack(&pack_dir);
275
276 let pack = LoadedPack::load(&pack_dir).unwrap();
278
279 let archive_path = temp.path().join("test.tar.gz");
281 create_archive(&pack, &archive_path).unwrap();
282
283 assert!(archive_path.exists());
284
285 let entries = list_archive(&archive_path).unwrap();
287 let paths: Vec<_> = entries.iter().map(|e| e.path.as_str()).collect();
288
289 assert!(paths.contains(&"MANIFEST"));
290 assert!(paths.contains(&"Pack.yaml"));
291 assert!(paths.contains(&"values.yaml"));
292 assert!(paths.iter().any(|p| p.contains("deployment.yaml")));
293
294 let extract_dir = temp.path().join("extracted");
296 extract_archive(&archive_path, &extract_dir).unwrap();
297
298 assert!(extract_dir.join("MANIFEST").exists());
299 assert!(extract_dir.join("Pack.yaml").exists());
300 assert!(extract_dir.join("values.yaml").exists());
301 }
302
303 #[test]
304 fn test_read_manifest_from_archive() {
305 let temp = TempDir::new().unwrap();
306 let pack_dir = temp.path().join("pack");
307 std::fs::create_dir_all(&pack_dir).unwrap();
308 create_test_pack(&pack_dir);
309
310 let pack = LoadedPack::load(&pack_dir).unwrap();
311 let archive_path = temp.path().join("test.tar.gz");
312 create_archive(&pack, &archive_path).unwrap();
313
314 let manifest = read_manifest_from_archive(&archive_path).unwrap();
316 assert_eq!(manifest.name, "testpack");
317 assert_eq!(manifest.pack_version.to_string(), "1.0.0");
318 }
319
320 #[test]
321 fn test_verify_archive() {
322 let temp = TempDir::new().unwrap();
323 let pack_dir = temp.path().join("pack");
324 std::fs::create_dir_all(&pack_dir).unwrap();
325 create_test_pack(&pack_dir);
326
327 let pack = LoadedPack::load(&pack_dir).unwrap();
328 let archive_path = temp.path().join("test.tar.gz");
329 create_archive(&pack, &archive_path).unwrap();
330
331 let result = verify_archive(&archive_path).unwrap();
333 assert!(result.valid);
334 assert!(result.mismatched.is_empty());
335 assert!(result.missing.is_empty());
336 }
337
338 #[test]
339 fn test_default_archive_name() {
340 let temp = TempDir::new().unwrap();
341 create_test_pack(temp.path());
342
343 let pack = LoadedPack::load(temp.path()).unwrap();
344 let name = default_archive_name(&pack);
345
346 assert_eq!(name, "testpack-1.0.0.tar.gz");
347 }
348}