1use std::collections::BTreeMap;
9use std::io::{Cursor, Read};
10use std::path::Path;
11
12const MAX_ENTRY_BYTES: u64 = 256 * 1024 * 1024;
17
18const MAX_TOTAL_BYTES: u64 = 2 * 1024 * 1024 * 1024;
30
31#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum DarError {
35 #[error("reading DAR failed: {0}")]
37 Io(#[from] std::io::Error),
38 #[error("malformed DAR archive: {0}")]
40 Zip(#[from] zip::result::ZipError),
41 #[error("not a DAR ({0}) — expected a .dar built with `daml build`")]
43 NotADar(String),
44 #[error("invalid DAR manifest: {0}")]
46 Manifest(String),
47 #[error("archive entry `{name}` exceeds the {MAX_ENTRY_BYTES}-byte limit")]
49 EntryTooLarge {
50 name: String,
52 },
53 #[error(
56 "DAR decompresses to more than the {MAX_TOTAL_BYTES}-byte limit in total \
57 (reached at entry `{name}`)"
58 )]
59 ArchiveTooLarge {
60 name: String,
63 },
64}
65
66#[derive(Debug)]
68pub struct Dar {
69 manifest: BTreeMap<String, String>,
70 dalfs: BTreeMap<String, Vec<u8>>,
71}
72
73impl Dar {
74 pub fn open(path: impl AsRef<Path>) -> Result<Self, DarError> {
79 let bytes = std::fs::read(path)?;
80 Self::from_bytes(&bytes)
81 }
82
83 pub fn from_bytes(bytes: &[u8]) -> Result<Self, DarError> {
88 Self::read(bytes, MAX_TOTAL_BYTES)
89 }
90
91 fn read(bytes: &[u8], total_budget: u64) -> Result<Self, DarError> {
95 let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
96 .map_err(|error| DarError::NotADar(error.to_string()))?;
97
98 let mut manifest = BTreeMap::new();
99 let mut saw_manifest = false;
100 let mut dalfs = BTreeMap::new();
101 let mut remaining = total_budget;
105
106 for index in 0..archive.len() {
107 let entry = archive.by_index(index)?;
108 if !entry.is_file() {
109 continue;
110 }
111 let name = entry.name().to_string();
112 let is_manifest = name == "META-INF/MANIFEST.MF";
113 let is_dalf = Path::new(&name)
114 .extension()
115 .is_some_and(|ext| ext.eq_ignore_ascii_case("dalf"));
116 if !is_manifest && !is_dalf {
117 continue;
118 }
119
120 let cap = MAX_ENTRY_BYTES.min(remaining);
121 let data = read_capped(entry, cap).map_err(|why| match why {
122 TooLarge::Io(error) => DarError::Io(error),
123 TooLarge::Refused if cap < MAX_ENTRY_BYTES => {
127 DarError::ArchiveTooLarge { name: name.clone() }
128 }
129 TooLarge::Refused => DarError::EntryTooLarge { name: name.clone() },
130 })?;
131 remaining -= data.len() as u64;
132
133 if is_manifest {
134 saw_manifest = true;
135 let text = String::from_utf8(data).map_err(|_| {
136 DarError::Manifest("META-INF/MANIFEST.MF is not valid UTF-8".to_string())
137 })?;
138 manifest = parse_manifest(&text);
139 } else {
140 dalfs.insert(name, data);
141 }
142 }
143
144 if manifest.is_empty() {
145 return Err(DarError::Manifest(if saw_manifest {
146 "META-INF/MANIFEST.MF carries no `Key: value` entries".to_string()
147 } else {
148 "no META-INF/MANIFEST.MF".to_string()
149 }));
150 }
151 if dalfs.is_empty() {
152 return Err(DarError::Manifest(
153 "archive contains no .dalf packages".to_string(),
154 ));
155 }
156 Ok(Self { manifest, dalfs })
157 }
158
159 #[must_use]
161 pub fn name(&self) -> Option<&str> {
162 self.manifest.get("Name").map(String::as_str)
163 }
164
165 #[must_use]
167 pub fn sdk_version(&self) -> Option<&str> {
168 self.manifest.get("Sdk-Version").map(String::as_str)
169 }
170
171 fn main_dalf_path(&self) -> Result<&str, DarError> {
173 self.manifest
174 .get("Main-Dalf")
175 .map(String::as_str)
176 .ok_or_else(|| DarError::Manifest("manifest has no Main-Dalf".to_string()))
177 }
178
179 pub fn main_package_bytes(&self) -> Result<&[u8], DarError> {
186 let path = self.main_dalf_path()?;
187 self.dalfs
188 .get(path)
189 .map(Vec::as_slice)
190 .ok_or_else(|| DarError::Manifest(format!("main dalf `{path}` not in archive")))
191 }
192
193 pub fn package_paths(&self) -> impl Iterator<Item = &str> {
196 self.dalfs.keys().map(String::as_str)
197 }
198
199 pub fn package_bytes(&self) -> impl Iterator<Item = &[u8]> {
204 self.dalfs.values().map(Vec::as_slice)
205 }
206
207 pub fn package_entries(&self) -> impl Iterator<Item = (&str, &[u8])> {
210 self.dalfs
211 .iter()
212 .map(|(name, bytes)| (name.as_str(), bytes.as_slice()))
213 }
214
215 #[must_use]
217 pub fn package_count(&self) -> usize {
218 self.dalfs.len()
219 }
220}
221
222fn read_capped(mut entry: zip::read::ZipFile<'_>, cap: u64) -> Result<Vec<u8>, TooLarge> {
234 if entry.size() > cap {
235 return Err(TooLarge::Refused);
236 }
237 let mut data = Vec::new();
238 Read::take(&mut entry, cap + 1)
241 .read_to_end(&mut data)
242 .map_err(TooLarge::Io)?;
243 if data.len() as u64 > cap {
244 return Err(TooLarge::Refused);
245 }
246 Ok(data)
247}
248
249enum TooLarge {
251 Refused,
252 Io(std::io::Error),
253}
254
255fn parse_manifest(text: &str) -> BTreeMap<String, String> {
259 let mut logical: Vec<String> = Vec::new();
260 for raw in text.split('\n') {
261 let line = raw.strip_suffix('\r').unwrap_or(raw);
262 if let Some(continuation) = line.strip_prefix(' ')
263 && let Some(last) = logical.last_mut()
264 {
265 last.push_str(continuation);
266 continue;
267 }
268 logical.push(line.to_string());
269 }
270
271 logical
272 .iter()
273 .filter_map(|line| {
274 let (key, value) = line.split_once(": ")?;
275 Some((key.to_string(), value.to_string()))
276 })
277 .collect()
278}
279
280#[cfg(test)]
281#[allow(clippy::unwrap_used, clippy::expect_used)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn manifest_unwraps_continuation_lines() {
287 let text = concat!(
291 "Manifest-Version: 1.0\r\n",
292 "Name: splice-wallet-payments-0.1.14\r\n",
293 "Main-Dalf: pkg/main-part1\r\n",
294 " part2.dalf\r\n",
295 "Sdk-Version: 3.3.0\r\n",
296 );
297 let manifest = parse_manifest(text);
298 assert_eq!(
299 manifest.get("Name").unwrap(),
300 "splice-wallet-payments-0.1.14"
301 );
302 assert_eq!(manifest.get("Sdk-Version").unwrap(), "3.3.0");
303 assert_eq!(
305 manifest.get("Main-Dalf").unwrap(),
306 "pkg/main-part1part2.dalf"
307 );
308 }
309
310 fn zip_of(entries: &[(&str, &[u8])]) -> Vec<u8> {
312 use std::io::Write as _;
313 let mut cursor = Cursor::new(Vec::new());
314 let mut writer = zip::ZipWriter::new(&mut cursor);
315 for (name, bytes) in entries {
316 writer
317 .start_file(*name, zip::write::SimpleFileOptions::default())
318 .unwrap();
319 writer.write_all(bytes).unwrap();
320 }
321 writer.finish().unwrap();
322 cursor.into_inner()
323 }
324
325 #[test]
326 fn hostile_inputs_yield_typed_errors_not_panics() {
327 assert!(matches!(
329 Dar::from_bytes(b"just some text"),
330 Err(DarError::NotADar(_))
331 ));
332 assert!(matches!(Dar::from_bytes(b""), Err(DarError::NotADar(_))));
334 let no_manifest = zip_of(&[("pkg.dalf", b"\x01\x02")]);
336 assert!(
337 matches!(Dar::from_bytes(&no_manifest), Err(DarError::Manifest(m)) if m.contains("no META-INF"))
338 );
339 let empty_manifest = zip_of(&[
341 ("META-INF/MANIFEST.MF", b"garbage without colon-space\n"),
342 ("pkg.dalf", b"\x01"),
343 ]);
344 assert!(
345 matches!(Dar::from_bytes(&empty_manifest), Err(DarError::Manifest(m)) if m.contains("no `Key: value`"))
346 );
347 let bad_utf8 = zip_of(&[("META-INF/MANIFEST.MF", &[0xFF, 0xFE, 0x00][..])]);
349 assert!(
350 matches!(Dar::from_bytes(&bad_utf8), Err(DarError::Manifest(m)) if m.contains("UTF-8"))
351 );
352 let jarlike = zip_of(&[(
354 "META-INF/MANIFEST.MF",
355 b"Manifest-Version: 1.0\nMain-Class: com.example.App\n".as_slice(),
356 )]);
357 assert!(
358 matches!(Dar::from_bytes(&jarlike), Err(DarError::Manifest(m)) if m.contains("no .dalf"))
359 );
360 }
361
362 #[test]
363 fn oversized_entries_are_rejected_by_the_decompression_cap() {
364 use std::io::Write as _;
369 let mut cursor = Cursor::new(Vec::new());
370 let mut writer = zip::ZipWriter::new(&mut cursor);
371 writer
372 .start_file(
373 "META-INF/MANIFEST.MF",
374 zip::write::SimpleFileOptions::default(),
375 )
376 .unwrap();
377 writer.write_all(b"Main-Dalf: bomb.dalf\n").unwrap();
378 writer
379 .start_file("bomb.dalf", zip::write::SimpleFileOptions::default())
380 .unwrap();
381 let zeros = vec![0_u8; 8 * 1024 * 1024];
382 for _ in 0..33 {
383 writer.write_all(&zeros).unwrap(); }
385 writer.finish().unwrap();
386 let bytes = cursor.into_inner();
387
388 assert!(matches!(
389 Dar::from_bytes(&bytes),
390 Err(DarError::EntryTooLarge { name }) if name == "bomb.dalf"
391 ));
392 }
393
394 #[test]
401 fn many_legal_entries_that_together_exceed_the_budget_are_refused() {
402 use std::io::Write as _;
403
404 const ENTRY: usize = 512 * 1024;
405 const BUDGET: u64 = 1024 * 1024; let mut cursor = Cursor::new(Vec::new());
408 let mut writer = zip::ZipWriter::new(&mut cursor);
409 let options = zip::write::SimpleFileOptions::default();
410 writer.start_file("META-INF/MANIFEST.MF", options).unwrap();
411 writer.write_all(b"Main-Dalf: a.dalf\n").unwrap();
412 let zeros = vec![0_u8; ENTRY];
413 for name in ["a.dalf", "b.dalf", "c.dalf", "d.dalf"] {
414 writer.start_file(name, options).unwrap();
415 writer.write_all(&zeros).unwrap();
416 }
417 writer.finish().unwrap();
418 let bytes = cursor.into_inner();
419
420 assert!(bytes.len() < 64 * 1024, "archive is {} bytes", bytes.len());
423
424 let error = Dar::read(&bytes, BUDGET).expect_err("four entries exceed the budget");
425 assert!(
426 matches!(&error, DarError::ArchiveTooLarge { .. }),
427 "the total is what was exceeded, not any one entry: {error}"
428 );
429 assert!(error.to_string().contains("in total"), "{error}");
432
433 assert!(Dar::read(&bytes, BUDGET * 8).is_ok());
436 }
437
438 #[test]
441 fn a_real_dar_is_nowhere_near_the_budget() {
442 let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
443 return;
444 };
445 let Ok(bytes) = std::fs::read(&path) else {
446 return;
447 };
448 let dar = Dar::from_bytes(&bytes).expect("a real DAR");
449 let total: usize = dar.package_bytes().map(<[u8]>::len).sum();
450 assert!(
451 (total as u64) < MAX_TOTAL_BYTES / 10,
452 "{path} decompresses to {total} bytes, within 10x of the budget"
453 );
454 }
455
456 #[test]
457 fn opening_a_real_dar_reads_the_manifest_and_main_package() {
458 let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
460 eprintln!("skipping real-DAR test: set CANTON_TEST_DAR=/path/to/x.dar");
461 return;
462 };
463
464 let dar = Dar::open(&path).expect("open DAR");
465 assert!(dar.name().is_some(), "DAR should have a Name");
466 assert!(
467 dar.sdk_version().is_some(),
468 "DAR should have an Sdk-Version"
469 );
470 assert!(
471 dar.package_count() > 1,
472 "a real DAR bundles its deps (daml-prim/stdlib)"
473 );
474 let main = dar.main_package_bytes().expect("main package bytes");
475 assert!(!main.is_empty(), "main package should have LF bytes");
476 println!(
477 "DAR {} (sdk {}) — {} packages, main = {} bytes",
478 dar.name().unwrap(),
479 dar.sdk_version().unwrap(),
480 dar.package_count(),
481 main.len()
482 );
483 }
484}