1use std::io::{Cursor, Write};
2
3use ed25519_dalek::{Signer, SigningKey};
4use zip::CompressionMethod;
5use zip::write::SimpleFileOptions;
6
7use crate::error::PackError;
8use crate::manifest::PackageManifest;
9use crate::util::sha256_hex;
10
11pub struct PackOptions {
13 pub manifest: PackageManifest,
15 pub binary_bytes: Vec<u8>,
17 pub resources: Vec<(String, Vec<u8>)>,
19 pub proto_files: Vec<(String, Vec<u8>)>,
22 pub signing_key: SigningKey,
24 pub lock_file: Option<Vec<u8>>,
26}
27
28pub fn pack(opts: &PackOptions) -> Result<Vec<u8>, PackError> {
32 let mut manifest = opts.manifest.clone();
33
34 let binary_hash = sha256_hex(&opts.binary_bytes);
36 manifest.binary.hash = binary_hash;
37 manifest.binary.size = Some(opts.binary_bytes.len() as u64);
38
39 if manifest.resources.len() != opts.resources.len() {
41 manifest.resources = opts
43 .resources
44 .iter()
45 .map(|(path, bytes)| crate::manifest::ResourceEntry {
46 path: path.clone(),
47 hash: sha256_hex(bytes),
48 })
49 .collect();
50 } else {
51 for (i, (_path, bytes)) in opts.resources.iter().enumerate() {
52 manifest.resources[i].hash = sha256_hex(bytes);
53 }
54 }
55
56 manifest.proto_files = opts
58 .proto_files
59 .iter()
60 .map(|(name, content)| crate::manifest::ProtoFileEntry {
61 name: name.clone(),
62 path: format!("proto/{}", name),
63 hash: sha256_hex(content),
64 })
65 .collect();
66
67 manifest.lock_file = opts
68 .lock_file
69 .as_ref()
70 .map(|bytes| crate::manifest::LockFileEntry {
71 path: "manifest.lock.toml".to_string(),
72 hash: sha256_hex(bytes),
73 });
74
75 let manifest_toml = manifest.to_toml()?;
77 let manifest_bytes = manifest_toml.as_bytes();
78
79 let signature = opts.signing_key.sign(manifest_bytes);
81 let sig_bytes = signature.to_bytes();
82
83 tracing::info!(
84 actr_type = %manifest.actr_type_str(),
85 binary_path = %manifest.binary.path,
86 binary_size = opts.binary_bytes.len(),
87 resources = opts.resources.len(),
88 "packing .actr file"
89 );
90
91 let buf = Cursor::new(Vec::new());
93 let mut zip = zip::ZipWriter::new(buf);
94 let store_opts = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
95
96 zip.start_file("manifest.toml", store_opts)?;
98 zip.write_all(manifest_bytes)?;
99
100 zip.start_file("manifest.sig", store_opts)?;
102 zip.write_all(&sig_bytes)?;
103
104 if let Some(lock_bytes) = &opts.lock_file {
106 zip.start_file("manifest.lock.toml", store_opts)?;
107 zip.write_all(lock_bytes)?;
108 }
109
110 zip.start_file(&manifest.binary.path, store_opts)?;
112 zip.write_all(&opts.binary_bytes)?;
113
114 for (path, bytes) in &opts.resources {
116 zip.start_file(path.as_str(), store_opts)?;
117 zip.write_all(bytes)?;
118 }
119
120 for (name, content) in &opts.proto_files {
122 let zip_path = format!("proto/{}", name);
123 zip.start_file(&zip_path, store_opts)?;
124 zip.write_all(content)?;
125 }
126
127 let cursor = zip.finish()?;
128 Ok(cursor.into_inner())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::manifest::{BinaryEntry, ManifestMetadata, PackageManifest};
135 use ed25519_dalek::SigningKey;
136 use rand::rngs::OsRng;
137
138 fn test_manifest() -> PackageManifest {
139 PackageManifest {
140 manufacturer: "test-mfr".to_string(),
141 name: "TestActor".to_string(),
142 version: "1.0.0".to_string(),
143 binary: BinaryEntry {
144 path: "bin/actor.wasm".to_string(),
145 target: "wasm32-wasip1".to_string(),
146 hash: String::new(), size: None,
148 kind: None,
149 },
150 signature_algorithm: "ed25519".to_string(),
151 signing_key_id: None,
152 resources: vec![],
153 proto_files: vec![],
154 lock_file: None,
155 metadata: ManifestMetadata::default(),
156 }
157 }
158
159 #[test]
160 fn pack_creates_valid_zip() {
161 let signing_key = SigningKey::generate(&mut OsRng);
162 let opts = PackOptions {
163 manifest: test_manifest(),
164 binary_bytes: b"fake wasm binary".to_vec(),
165 resources: vec![],
166 proto_files: vec![],
167 signing_key,
168 lock_file: None,
169 };
170 let result = pack(&opts);
171 assert!(result.is_ok());
172 let bytes = result.unwrap();
173 assert_eq!(&bytes[0..2], b"PK");
175 }
176
177 #[test]
178 fn pack_then_verify_roundtrip() {
179 let signing_key = SigningKey::generate(&mut OsRng);
180 let verifying_key = signing_key.verifying_key();
181 let opts = PackOptions {
182 manifest: test_manifest(),
183 binary_bytes: b"hello wasm".to_vec(),
184 resources: vec![],
185 proto_files: vec![],
186 signing_key: signing_key.clone(),
187 lock_file: None,
188 };
189 let package = pack(&opts).unwrap();
190 let result = crate::verify::verify(&package, &verifying_key).unwrap();
191 assert_eq!(result.manifest.manufacturer, "test-mfr");
192 assert_eq!(result.manifest.name, "TestActor");
193 assert_eq!(result.manifest.version, "1.0.0");
194 }
195}