cardinal_kernel/pack/
loader.rs1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::io::Read;
4use std::path::Path;
5
6use super::metadata::Manifest;
7
8pub fn load_pack<P: AsRef<Path>>(ccpack_path: P) -> Result<(Manifest, HashMap<String, Vec<u8>>)> {
16 let ccpack_path = ccpack_path.as_ref();
17
18 let compressed_data = std::fs::read(ccpack_path)
20 .with_context(|| format!("Failed to read pack file {}", ccpack_path.display()))?;
21
22 let tar_data = zstd::decode_all(&compressed_data[..])
23 .context("Failed to decompress pack file with zstd")?;
24
25 let mut archive = tar::Archive::new(&tar_data[..]);
27 let mut files = HashMap::new();
28 let mut manifest_content = None;
29
30 for entry in archive.entries().context("Failed to read tar entries")? {
31 let mut entry = entry.context("Failed to read tar entry")?;
32 let path = entry
33 .path()
34 .context("Failed to get entry path")?
35 .to_string_lossy()
36 .to_string();
37
38 let mut content = Vec::new();
39 entry
40 .read_to_end(&mut content)
41 .with_context(|| format!("Failed to read content of {}", path))?;
42
43 if path == "manifest.toml" {
44 manifest_content = Some(content.clone());
45 }
46
47 files.insert(path, content);
48 }
49
50 let manifest_bytes = manifest_content
52 .ok_or_else(|| anyhow::anyhow!("manifest.toml not found in pack"))?;
53
54 let manifest_str = String::from_utf8(manifest_bytes)
55 .context("manifest.toml is not valid UTF-8")?;
56
57 let manifest: Manifest = toml::from_str(&manifest_str)
58 .context("Failed to parse manifest.toml")?;
59
60 Ok((manifest, files))
61}
62
63pub fn list_pack<P: AsRef<Path>>(ccpack_path: P) -> Result<()> {
70 let ccpack_path = ccpack_path.as_ref();
71
72 let (manifest, _files) = load_pack(ccpack_path)
73 .with_context(|| format!("Failed to load pack {}", ccpack_path.display()))?;
74
75 println!("Pack: {}", manifest.pack.pack_id);
76 println!("Version: {}", manifest.pack.version);
77
78 if let Some(name) = &manifest.pack.name {
79 println!("Name: {}", name);
80 }
81
82 if let Some(desc) = &manifest.pack.description {
83 println!("Description: {}", desc);
84 }
85
86 if !manifest.pack.dependencies.is_empty() {
87 println!("Dependencies:");
88 for dep in &manifest.pack.dependencies {
89 println!(" - {}", dep);
90 }
91 }
92
93 println!();
94 println!("Files ({}):", manifest.files.len());
95
96 for file_entry in &manifest.files {
97 println!(
98 " {} ({} bytes, sha256: {})",
99 file_entry.path, file_entry.size, file_entry.sha256
100 );
101 }
102
103 Ok(())
104}
105
106pub fn unpack_pack<P: AsRef<Path>, Q: AsRef<Path>>(ccpack_path: P, output_dir: Q) -> Result<()> {
114 let ccpack_path = ccpack_path.as_ref();
115 let output_dir = output_dir.as_ref();
116
117 let (_manifest, files) = load_pack(ccpack_path)
118 .with_context(|| format!("Failed to load pack {}", ccpack_path.display()))?;
119
120 std::fs::create_dir_all(output_dir)
122 .with_context(|| format!("Failed to create output directory {}", output_dir.display()))?;
123
124 for (path, content) in &files {
126 let output_path = output_dir.join(path);
127
128 if let Some(parent) = output_path.parent() {
130 std::fs::create_dir_all(parent)
131 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
132 }
133
134 std::fs::write(&output_path, content)
135 .with_context(|| format!("Failed to write file {}", output_path.display()))?;
136
137 println!(" Extracted: {}", path);
138 }
139
140 println!("✓ Pack unpacked to: {}", output_dir.display());
141
142 Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::pack::builder::build_pack;
149 use crate::pack::metadata::PackMeta;
150 use std::fs;
151
152 #[test]
153 fn test_pack_roundtrip() {
154 let temp_dir = std::env::temp_dir().join("test_pack");
156 let _ = fs::remove_dir_all(&temp_dir); fs::create_dir_all(&temp_dir).unwrap();
158
159 let pack_meta = PackMeta {
161 pack_id: "test-pack".to_string(),
162 version: "1.0.0".to_string(),
163 dependencies: vec![],
164 name: Some("Test Pack".to_string()),
165 description: Some("A test pack".to_string()),
166 };
167
168 let pack_toml = toml::to_string(&pack_meta).unwrap();
169 fs::write(temp_dir.join("pack.toml"), pack_toml).unwrap();
170
171 fs::create_dir_all(temp_dir.join("cards")).unwrap();
173 fs::write(temp_dir.join("cards/test_card.toml"), "name = \"Test Card\"\n").unwrap();
174
175 fs::create_dir_all(temp_dir.join("scripts")).unwrap();
176 fs::write(temp_dir.join("scripts/test.rhai"), "// Test script\n").unwrap();
177
178 let pack_path = temp_dir.join("test.ccpack");
180 build_pack(&temp_dir, &pack_path).unwrap();
181
182 let (manifest, files) = load_pack(&pack_path).unwrap();
184
185 assert_eq!(manifest.pack.pack_id, "test-pack");
186 assert_eq!(manifest.pack.version, "1.0.0");
187 assert!(files.contains_key("pack.toml"));
188 assert!(files.contains_key("cards/test_card.toml"));
189 assert!(files.contains_key("scripts/test.rhai"));
190 assert!(files.contains_key("manifest.toml"));
191
192 let _ = fs::remove_dir_all(&temp_dir);
194 }
195}