Skip to main content

cardinal_kernel/pack/
loader.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::io::Read;
4use std::path::Path;
5
6use super::metadata::Manifest;
7
8/// Load a .ccpack file into memory and return the manifest and file contents
9///
10/// # Arguments
11/// * `ccpack_path` - Path to the .ccpack file
12///
13/// # Returns
14/// A tuple of (Manifest, HashMap<path, content_bytes>)
15pub 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    // Read and decompress the pack file
19    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    // Extract tar archive
26    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    // Parse manifest
51    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
63/// List the contents of a .ccpack file
64///
65/// # Arguments
66/// * `ccpack_path` - Path to the .ccpack file
67///
68/// Prints information about the pack to stdout
69pub 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
106/// Unpack a .ccpack file to a directory
107///
108/// # Arguments
109/// * `ccpack_path` - Path to the .ccpack file
110/// * `output_dir` - Directory where files will be extracted
111///
112/// Extracts all files from the pack to the output directory
113pub 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    // Create output directory if it doesn't exist
121    std::fs::create_dir_all(output_dir)
122        .with_context(|| format!("Failed to create output directory {}", output_dir.display()))?;
123
124    // Extract all files
125    for (path, content) in &files {
126        let output_path = output_dir.join(path);
127
128        // Create parent directories if needed
129        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        // Create a temporary test pack directory
155        let temp_dir = std::env::temp_dir().join("test_pack");
156        let _ = fs::remove_dir_all(&temp_dir); // Clean up if exists
157        fs::create_dir_all(&temp_dir).unwrap();
158
159        // Create pack.toml
160        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        // Create some test files
172        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        // Build pack
179        let pack_path = temp_dir.join("test.ccpack");
180        build_pack(&temp_dir, &pack_path).unwrap();
181
182        // Load pack
183        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        // Clean up
193        let _ = fs::remove_dir_all(&temp_dir);
194    }
195}