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 mut 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 declared = entry.size();
122 let data = read_capped(&mut entry, declared, cap).map_err(|why| match why {
123 TooLarge::Io(error) => DarError::Io(error),
124 TooLarge::Refused if cap < MAX_ENTRY_BYTES => {
128 DarError::ArchiveTooLarge { name: name.clone() }
129 }
130 TooLarge::Refused => DarError::EntryTooLarge { name: name.clone() },
131 })?;
132 remaining -= data.len() as u64;
133
134 if is_manifest {
135 saw_manifest = true;
136 let text = String::from_utf8(data).map_err(|_| {
137 DarError::Manifest("META-INF/MANIFEST.MF is not valid UTF-8".to_string())
138 })?;
139 manifest = parse_manifest(&text);
140 } else {
141 dalfs.insert(name, data);
142 }
143 }
144
145 if manifest.is_empty() {
146 return Err(DarError::Manifest(if saw_manifest {
147 "META-INF/MANIFEST.MF carries no `Key: value` entries".to_string()
148 } else {
149 "no META-INF/MANIFEST.MF".to_string()
150 }));
151 }
152 if dalfs.is_empty() {
153 return Err(DarError::Manifest(
154 "archive contains no .dalf packages".to_string(),
155 ));
156 }
157 Ok(Self { manifest, dalfs })
158 }
159
160 #[must_use]
162 pub fn name(&self) -> Option<&str> {
163 self.manifest.get("Name").map(String::as_str)
164 }
165
166 #[must_use]
168 pub fn sdk_version(&self) -> Option<&str> {
169 self.manifest.get("Sdk-Version").map(String::as_str)
170 }
171
172 fn main_dalf_path(&self) -> Result<&str, DarError> {
174 self.manifest
175 .get("Main-Dalf")
176 .map(String::as_str)
177 .ok_or_else(|| DarError::Manifest("manifest has no Main-Dalf".to_string()))
178 }
179
180 pub fn main_package_bytes(&self) -> Result<&[u8], DarError> {
187 let path = self.main_dalf_path()?;
188 self.dalfs
189 .get(path)
190 .map(Vec::as_slice)
191 .ok_or_else(|| DarError::Manifest(format!("main dalf `{path}` not in archive")))
192 }
193
194 pub fn package_paths(&self) -> impl Iterator<Item = &str> {
197 self.dalfs.keys().map(String::as_str)
198 }
199
200 pub fn package_bytes(&self) -> impl Iterator<Item = &[u8]> {
205 self.dalfs.values().map(Vec::as_slice)
206 }
207
208 pub fn package_entries(&self) -> impl Iterator<Item = (&str, &[u8])> {
211 self.dalfs
212 .iter()
213 .map(|(name, bytes)| (name.as_str(), bytes.as_slice()))
214 }
215
216 #[must_use]
218 pub fn package_count(&self) -> usize {
219 self.dalfs.len()
220 }
221}
222
223fn read_capped(entry: &mut impl Read, declared: u64, cap: u64) -> Result<Vec<u8>, TooLarge> {
239 if declared > cap {
240 return Err(TooLarge::Refused);
241 }
242 let mut data = Vec::new();
243 Read::take(entry, cap + 1)
246 .read_to_end(&mut data)
247 .map_err(TooLarge::Io)?;
248 if data.len() as u64 > cap {
249 return Err(TooLarge::Refused);
250 }
251 Ok(data)
252}
253
254enum TooLarge {
256 Refused,
257 Io(std::io::Error),
258}
259
260fn parse_manifest(text: &str) -> BTreeMap<String, String> {
264 let mut logical: Vec<String> = Vec::new();
265 for raw in text.split('\n') {
266 let line = raw.strip_suffix('\r').unwrap_or(raw);
267 if let Some(continuation) = line.strip_prefix(' ')
268 && let Some(last) = logical.last_mut()
269 {
270 last.push_str(continuation);
271 continue;
272 }
273 logical.push(line.to_string());
274 }
275
276 logical
277 .iter()
278 .filter_map(|line| {
279 let (key, value) = line.split_once(": ")?;
280 Some((key.to_string(), value.to_string()))
281 })
282 .collect()
283}
284
285#[cfg(test)]
286#[allow(clippy::unwrap_used, clippy::expect_used)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn manifest_unwraps_continuation_lines() {
292 let text = concat!(
296 "Manifest-Version: 1.0\r\n",
297 "Name: splice-wallet-payments-0.1.14\r\n",
298 "Main-Dalf: pkg/main-part1\r\n",
299 " part2.dalf\r\n",
300 "Sdk-Version: 3.3.0\r\n",
301 );
302 let manifest = parse_manifest(text);
303 assert_eq!(
304 manifest.get("Name").unwrap(),
305 "splice-wallet-payments-0.1.14"
306 );
307 assert_eq!(manifest.get("Sdk-Version").unwrap(), "3.3.0");
308 assert_eq!(
310 manifest.get("Main-Dalf").unwrap(),
311 "pkg/main-part1part2.dalf"
312 );
313 }
314
315 fn zip_of(entries: &[(&str, &[u8])]) -> Vec<u8> {
317 use std::io::Write as _;
318 let mut cursor = Cursor::new(Vec::new());
319 let mut writer = zip::ZipWriter::new(&mut cursor);
320 for (name, bytes) in entries {
321 writer
322 .start_file(*name, zip::write::SimpleFileOptions::default())
323 .unwrap();
324 writer.write_all(bytes).unwrap();
325 }
326 writer.finish().unwrap();
327 cursor.into_inner()
328 }
329
330 #[test]
331 fn hostile_inputs_yield_typed_errors_not_panics() {
332 assert!(matches!(
334 Dar::from_bytes(b"just some text"),
335 Err(DarError::NotADar(_))
336 ));
337 assert!(matches!(Dar::from_bytes(b""), Err(DarError::NotADar(_))));
339 let no_manifest = zip_of(&[("pkg.dalf", b"\x01\x02")]);
341 assert!(
342 matches!(Dar::from_bytes(&no_manifest), Err(DarError::Manifest(m)) if m.contains("no META-INF"))
343 );
344 let empty_manifest = zip_of(&[
346 ("META-INF/MANIFEST.MF", b"garbage without colon-space\n"),
347 ("pkg.dalf", b"\x01"),
348 ]);
349 assert!(
350 matches!(Dar::from_bytes(&empty_manifest), Err(DarError::Manifest(m)) if m.contains("no `Key: value`"))
351 );
352 let bad_utf8 = zip_of(&[("META-INF/MANIFEST.MF", &[0xFF, 0xFE, 0x00][..])]);
354 assert!(
355 matches!(Dar::from_bytes(&bad_utf8), Err(DarError::Manifest(m)) if m.contains("UTF-8"))
356 );
357 let jarlike = zip_of(&[(
359 "META-INF/MANIFEST.MF",
360 b"Manifest-Version: 1.0\nMain-Class: com.example.App\n".as_slice(),
361 )]);
362 assert!(
363 matches!(Dar::from_bytes(&jarlike), Err(DarError::Manifest(m)) if m.contains("no .dalf"))
364 );
365 }
366
367 #[test]
368 fn oversized_entries_are_rejected_by_the_decompression_cap() {
369 use std::io::Write as _;
374 let mut cursor = Cursor::new(Vec::new());
375 let mut writer = zip::ZipWriter::new(&mut cursor);
376 writer
377 .start_file(
378 "META-INF/MANIFEST.MF",
379 zip::write::SimpleFileOptions::default(),
380 )
381 .unwrap();
382 writer.write_all(b"Main-Dalf: bomb.dalf\n").unwrap();
383 writer
384 .start_file("bomb.dalf", zip::write::SimpleFileOptions::default())
385 .unwrap();
386 let zeros = vec![0_u8; 8 * 1024 * 1024];
387 for _ in 0..33 {
388 writer.write_all(&zeros).unwrap(); }
390 writer.finish().unwrap();
391 let bytes = cursor.into_inner();
392
393 assert!(matches!(
394 Dar::from_bytes(&bytes),
395 Err(DarError::EntryTooLarge { name }) if name == "bomb.dalf"
396 ));
397 }
398
399 #[test]
406 fn many_legal_entries_that_together_exceed_the_budget_are_refused() {
407 use std::io::Write as _;
408
409 const ENTRY: usize = 512 * 1024;
410 const BUDGET: u64 = 1024 * 1024; let mut cursor = Cursor::new(Vec::new());
413 let mut writer = zip::ZipWriter::new(&mut cursor);
414 let options = zip::write::SimpleFileOptions::default();
415 writer.start_file("META-INF/MANIFEST.MF", options).unwrap();
416 writer.write_all(b"Main-Dalf: a.dalf\n").unwrap();
417 let zeros = vec![0_u8; ENTRY];
418 for name in ["a.dalf", "b.dalf", "c.dalf", "d.dalf"] {
419 writer.start_file(name, options).unwrap();
420 writer.write_all(&zeros).unwrap();
421 }
422 writer.finish().unwrap();
423 let bytes = cursor.into_inner();
424
425 assert!(bytes.len() < 64 * 1024, "archive is {} bytes", bytes.len());
428
429 let error = Dar::read(&bytes, BUDGET).expect_err("four entries exceed the budget");
430 assert!(
431 matches!(&error, DarError::ArchiveTooLarge { .. }),
432 "the total is what was exceeded, not any one entry: {error}"
433 );
434 assert!(error.to_string().contains("in total"), "{error}");
437
438 assert!(Dar::read(&bytes, BUDGET * 8).is_ok());
441 }
442
443 #[test]
446 fn a_real_dar_is_nowhere_near_the_budget() {
447 let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
448 return;
449 };
450 let Ok(bytes) = std::fs::read(&path) else {
451 return;
452 };
453 let dar = Dar::from_bytes(&bytes).expect("a real DAR");
454 let total: usize = dar.package_bytes().map(<[u8]>::len).sum();
455 assert!(
456 (total as u64) < MAX_TOTAL_BYTES / 10,
457 "{path} decompresses to {total} bytes, within 10x of the budget"
458 );
459 }
460
461 #[test]
462 fn opening_a_real_dar_reads_the_manifest_and_main_package() {
463 let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
465 eprintln!("skipping real-DAR test: set CANTON_TEST_DAR=/path/to/x.dar");
466 return;
467 };
468
469 let dar = Dar::open(&path).expect("open DAR");
470 assert!(dar.name().is_some(), "DAR should have a Name");
471 assert!(
472 dar.sdk_version().is_some(),
473 "DAR should have an Sdk-Version"
474 );
475 assert!(
476 dar.package_count() > 1,
477 "a real DAR bundles its deps (daml-prim/stdlib)"
478 );
479 let main = dar.main_package_bytes().expect("main package bytes");
480 assert!(!main.is_empty(), "main package should have LF bytes");
481 println!(
482 "DAR {} (sdk {}) — {} packages, main = {} bytes",
483 dar.name().unwrap(),
484 dar.sdk_version().unwrap(),
485 dar.package_count(),
486 main.len()
487 );
488 }
489}