1use std::{
2 collections::{BTreeMap, BTreeSet},
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use async_compression::tokio::bufread::ZstdDecoder;
8use iroh_blobs::Hash;
9use iroh_docs::NamespaceId;
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use tokio::{
13 fs,
14 io::{AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader},
15 sync::Mutex,
16};
17use uuid::Uuid;
18
19use crate::{
20 daemon::ManagedPool,
21 error::{Result, SyncwebError},
22 filter::{FilterAction, FilterEngine, FilterEntry},
23 node::iroh_node::IrohNode,
24};
25
26use super::{CollectionManifest, CollectionStore, FolderManager, SyncMode};
27
28const CAR_VERSION: u64 = 1;
29const CID_VERSION: u64 = 1;
30const RAW_CODEC: u64 = 0x55;
31const BLAKE3_MULTIHASH: u64 = 0x1e;
32const HASH_SIZE: u64 = 32;
33const MAX_HEADER_SIZE: u64 = 64 * 1024;
34const MAX_MANIFEST_SIZE: u64 = 16 * 1024 * 1024;
35const COPY_BUFFER_SIZE: usize = 64 * 1024;
36
37#[derive(Clone, Debug, Default)]
39#[non_exhaustive]
40pub struct DropImportOptions {
41 filter: Option<FilterEngine>,
42 available_dependencies: BTreeMap<Uuid, Version>,
43}
44
45impl DropImportOptions {
46 #[must_use]
47 pub fn with_filter(mut self, filter: FilterEngine) -> Self {
48 self.filter = Some(filter);
49 self
50 }
51
52 #[must_use]
53 pub fn with_available_dependencies(mut self, available: BTreeMap<Uuid, Version>) -> Self {
54 self.available_dependencies = available;
55 self
56 }
57
58 #[must_use]
59 pub const fn filter(&self) -> Option<&FilterEngine> {
60 self.filter.as_ref()
61 }
62
63 #[must_use]
64 pub const fn available_dependencies(&self) -> &BTreeMap<Uuid, Version> {
65 &self.available_dependencies
66 }
67}
68
69#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
71#[non_exhaustive]
72pub struct DropImportResult {
73 pub manifest: Hash,
74 pub collection_manifest: CollectionManifest,
75 pub collection_id: Uuid,
76 pub version: String,
77 pub entry_count: usize,
78 pub imported_entry_count: usize,
79 pub skipped_entry_count: usize,
80 pub block_count: usize,
81 pub imported_block_count: usize,
82 pub namespace_id: Option<NamespaceId>,
83}
84
85#[derive(Clone)]
87pub struct DropImporter {
88 blob_store: crate::node::blob_store::BlobStore,
89 import_lock: Arc<Mutex<()>>,
90}
91
92impl DropImporter {
93 #[must_use]
94 pub fn new(blob_store: crate::node::blob_store::BlobStore) -> Self {
95 Self {
96 blob_store,
97 import_lock: Arc::new(Mutex::new(())),
98 }
99 }
100
101 pub async fn import_archive(
112 &self,
113 input: impl AsRef<Path>,
114 options: DropImportOptions,
115 pool: Option<&ManagedPool>,
116 ) -> Result<DropImportResult> {
117 let _import_guard = self.import_lock.lock().await;
118 let input_path = input.as_ref();
119 let file = fs::File::open(input_path)
120 .await
121 .map_err(|error| SyncwebError::operation("failed to open drop archive", error))?;
122 let parent = input_path.parent().unwrap_or_else(|| Path::new("."));
123 let staging = parent.join(format!(
124 "{}{}",
125 crate::constants::DROP_IMPORT_STAGING_PREFIX,
126 Uuid::new_v4()
127 ));
128 fs::create_dir(&staging)
129 .await
130 .map_err(|error| SyncwebError::operation("failed to create drop import staging directory", error))?;
131
132 let parse_result = read_archive(ZstdDecoder::new(BufReader::new(file)), &staging, &options, pool).await;
133 let parsed = match parse_result {
134 Ok(parsed) => parsed,
135 Err(error) => {
136 remove_staging(&staging).await?;
137 return Err(error);
138 }
139 };
140
141 let import_result = self.ingest(parsed, pool).await;
142 let cleanup_result = remove_staging(&staging).await;
143 match (import_result, cleanup_result) {
144 (Ok(result), Ok(())) => Ok(result),
145 (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
146 (Err(error), Err(cleanup_error)) => Err(SyncwebError::operation(
147 "failed to clean up drop import staging directory",
148 format!("{error}; cleanup failed: {cleanup_error}"),
149 )),
150 }
151 }
152
153 pub async fn materialize(&self, result: &DropImportResult, target: impl AsRef<Path>) -> Result<()> {
164 let target_path = target.as_ref();
165 if fs::try_exists(target_path).await? {
166 return Err(SyncwebError::InvalidConfig(format!(
167 "drop materialization target already exists: {}",
168 target_path.display()
169 )));
170 }
171 let parent = target_path.parent().unwrap_or_else(|| Path::new("."));
172 fs::create_dir_all(parent).await?;
173 let staging = parent.join(format!(
174 "{}{}",
175 crate::constants::MATERIALIZATION_STAGING_PREFIX,
176 Uuid::new_v4()
177 ));
178 fs::create_dir(&staging)
179 .await
180 .map_err(|error| SyncwebError::operation("failed to create materialization staging directory", error))?;
181
182 let write_result = async {
183 for entry in &result.collection_manifest.entries {
184 let destination = staging.join(&entry.logical_path);
185 let entry_parent = destination
186 .parent()
187 .ok_or_else(|| SyncwebError::InvalidConfig("drop entry has no parent directory".to_owned()))?;
188 fs::create_dir_all(entry_parent).await?;
189 let size = self.blob_store.export_to_path(entry.content_id, &destination).await?;
190 if size != entry.size {
191 return Err(SyncwebError::InvalidConfig(format!(
192 "materialized drop entry size does not match manifest: {}",
193 entry.logical_path.display()
194 )));
195 }
196 }
197 fs::rename(&staging, target_path)
198 .await
199 .map_err(|error| SyncwebError::operation("failed to finalize drop materialization", error))?;
200 Ok(())
201 }
202 .await;
203 if let Err(error) = write_result {
204 remove_staging(&staging).await?;
205 return Err(error);
206 }
207 Ok(())
208 }
209
210 async fn ingest(&self, parsed: ParsedDrop, pool: Option<&ManagedPool>) -> Result<DropImportResult> {
211 let manifest_hash = in_pool(pool, || Hash::new(&parsed.manifest_bytes));
212 let stored_manifest = self.blob_store.add_bytes(&parsed.manifest_bytes).await?;
213 if stored_manifest != manifest_hash {
214 return Err(SyncwebError::InvalidConfig(
215 "imported manifest hash does not match its content".to_owned(),
216 ));
217 }
218
219 for block in &parsed.blocks {
220 let stored = self.blob_store.add_file(&block.path).await?;
221 if stored != block.hash {
222 return Err(SyncwebError::InvalidConfig(format!(
223 "imported content hash does not match its CID: expected {}, got {}",
224 block.hash, stored
225 )));
226 }
227 }
228
229 let collection_id = parsed.manifest.collection_id;
230 let version = parsed.manifest.version.clone();
231 let entry_count = parsed.entry_count;
232 let imported_entry_count = parsed.manifest.entries.len();
233 let skipped_entry_count = entry_count.saturating_sub(imported_entry_count);
234 let block_count = parsed.block_count;
235 let imported_block_count = parsed.blocks.len().saturating_add(1);
236 let collection_manifest = parsed.manifest;
237 Ok(DropImportResult {
238 manifest: manifest_hash,
239 collection_manifest,
240 collection_id,
241 version,
242 entry_count,
243 imported_entry_count,
244 skipped_entry_count,
245 block_count,
246 imported_block_count,
247 namespace_id: None,
248 })
249 }
250}
251
252pub async fn import_archive(
262 node: &IrohNode,
263 input: impl AsRef<Path>,
264 target_dir: impl AsRef<Path>,
265 filter: Option<&FilterEngine>,
266) -> Result<DropImportResult> {
267 let options = filter.map_or_else(DropImportOptions::default, |value| {
268 DropImportOptions::default().with_filter(value.clone())
269 });
270 let importer = DropImporter::new(node.blob_store().clone());
271 let mut result = importer.import_archive(input, options, None).await?;
272 importer.materialize(&result, target_dir).await?;
273
274 let folder = FolderManager::new(node).create(SyncMode::SendReceive).await?;
275 CollectionStore::for_node(node, &folder)
276 .publish(&result.collection_manifest, 1)
277 .await?;
278 result.namespace_id = Some(folder.namespace_id());
279 Ok(result)
280}
281
282struct ParsedDrop {
283 manifest: CollectionManifest,
284 manifest_bytes: Vec<u8>,
285 blocks: Vec<StagedBlock>,
286 entry_count: usize,
287 block_count: usize,
288}
289
290struct StagedBlock {
291 hash: Hash,
292 path: PathBuf,
293}
294
295struct SectionHeader {
296 hash: Hash,
297 payload_size: u64,
298}
299
300struct PreparedManifest {
301 manifest: CollectionManifest,
302 filtered_manifest: CollectionManifest,
303 expected: BTreeMap<Hash, (u64, bool)>,
304}
305
306async fn read_archive<R>(
307 mut reader: R,
308 staging: &Path,
309 options: &DropImportOptions,
310 pool: Option<&ManagedPool>,
311) -> Result<ParsedDrop>
312where
313 R: AsyncRead + Unpin,
314{
315 let header_size = read_required_varint(&mut reader, "CAR header length").await?;
316 if header_size > MAX_HEADER_SIZE {
317 return Err(SyncwebError::InvalidConfig(format!(
318 "CAR header exceeds {MAX_HEADER_SIZE} bytes"
319 )));
320 }
321 let header_len = usize::try_from(header_size)
322 .map_err(|error| SyncwebError::operation("CAR header length is not representable", error))?;
323 let mut header = vec![0_u8; header_len];
324 read_exact(&mut reader, &mut header, "failed to read CAR header").await?;
325 let root = parse_car_header(&header)?;
326
327 let manifest_section = read_section_header(&mut reader)
328 .await?
329 .ok_or_else(|| SyncwebError::InvalidConfig("drop archive has no manifest block".to_owned()))?;
330 if manifest_section.hash != root {
331 return Err(SyncwebError::InvalidConfig(
332 "drop manifest CID does not match the CAR root".to_owned(),
333 ));
334 }
335 if manifest_section.payload_size > MAX_MANIFEST_SIZE {
336 return Err(SyncwebError::InvalidConfig(format!(
337 "drop manifest exceeds {MAX_MANIFEST_SIZE} bytes"
338 )));
339 }
340 let manifest_bytes = read_payload_to_vec(&mut reader, manifest_section.payload_size, "drop manifest").await?;
341 let prepared = prepare_manifest(&manifest_bytes, root, options, pool)?;
342
343 let mut seen = BTreeSet::new();
344 let mut blocks = Vec::new();
345 let mut block_count = 1_usize;
346 while let Some(section) = read_section_header(&mut reader).await? {
347 let (expected_size, accepted) = prepared.expected.get(§ion.hash).copied().ok_or_else(|| {
348 SyncwebError::InvalidConfig(format!(
349 "drop contains a block not referenced by the manifest: {}",
350 section.hash
351 ))
352 })?;
353 if !seen.insert(section.hash) {
354 return Err(SyncwebError::InvalidConfig(format!(
355 "drop contains duplicate content block: {}",
356 section.hash
357 )));
358 }
359 if section.payload_size != expected_size {
360 return Err(SyncwebError::InvalidConfig(format!(
361 "drop content size does not match the manifest for {}",
362 section.hash
363 )));
364 }
365 let staged_path = accepted.then(|| staging.join(format!("block-{}.blob", blocks.len())));
366 let (actual_hash, actual_size) =
367 read_payload_to_path(&mut reader, section.payload_size, staged_path.as_deref(), pool).await?;
368 if actual_hash != section.hash {
369 return Err(SyncwebError::InvalidConfig(format!(
370 "drop content hash does not match its CID: expected {}, got {}",
371 section.hash, actual_hash
372 )));
373 }
374 if actual_size != expected_size {
375 return Err(SyncwebError::InvalidConfig(format!(
376 "drop content size does not match the manifest for {}",
377 section.hash
378 )));
379 }
380 if let Some(block_path) = staged_path {
381 blocks.push(StagedBlock {
382 hash: section.hash,
383 path: block_path,
384 });
385 }
386 block_count = block_count
387 .checked_add(1)
388 .ok_or_else(|| SyncwebError::operation("drop block count overflow", "usize limit exceeded"))?;
389 }
390
391 if seen.len() != prepared.expected.len() {
392 let missing = prepared
393 .expected
394 .keys()
395 .find(|hash| !seen.contains(*hash))
396 .copied()
397 .map_or_else(|| "unknown".to_owned(), |hash| hash.to_string());
398 return Err(SyncwebError::InvalidConfig(format!(
399 "drop is missing content referenced by the manifest: {missing}"
400 )));
401 }
402
403 let filtered_manifest_bytes = prepared.filtered_manifest.to_bytes()?;
404 Ok(ParsedDrop {
405 manifest: prepared.filtered_manifest,
406 manifest_bytes: filtered_manifest_bytes,
407 blocks,
408 entry_count: prepared.manifest.entries.len(),
409 block_count,
410 })
411}
412
413fn prepare_manifest(
414 manifest_bytes: &[u8],
415 root: Hash,
416 options: &DropImportOptions,
417 pool: Option<&ManagedPool>,
418) -> Result<PreparedManifest> {
419 let manifest = in_pool(pool, || CollectionManifest::from_bytes(manifest_bytes))?;
420 in_pool(pool, || manifest.verify_signature())?;
421 let manifest_hash = in_pool(pool, || Hash::new(manifest_bytes));
422 let manifest_blob_id = in_pool(pool, || manifest.blob_id())?;
423 if manifest_hash != root || manifest_blob_id != root {
424 return Err(SyncwebError::InvalidConfig(
425 "drop manifest hash does not match the CAR root".to_owned(),
426 ));
427 }
428 if !manifest.dependencies_satisfied(options.available_dependencies())? {
429 return Err(SyncwebError::InvalidConfig(
430 "drop package has missing or incompatible dependencies".to_owned(),
431 ));
432 }
433
434 let filtered_manifest = in_pool(pool, || filter_manifest(manifest.clone(), options.filter()))?;
435 let mut expected = BTreeMap::<Hash, (u64, bool)>::new();
436 let accepted_hashes = filtered_manifest
437 .entries
438 .iter()
439 .map(|entry| entry.content_id)
440 .collect::<BTreeSet<_>>();
441 for entry in &manifest.entries {
442 match expected.entry(entry.content_id) {
443 std::collections::btree_map::Entry::Vacant(slot) => {
444 slot.insert((entry.size, accepted_hashes.contains(&entry.content_id)));
445 }
446 std::collections::btree_map::Entry::Occupied(slot) => {
447 let (size, accepted) = slot.into_mut();
448 if *size != entry.size {
449 return Err(SyncwebError::InvalidConfig(format!(
450 "manifest uses content hash {} with inconsistent sizes",
451 entry.content_id
452 )));
453 }
454 *accepted |= accepted_hashes.contains(&entry.content_id);
455 }
456 }
457 }
458 Ok(PreparedManifest {
459 manifest,
460 filtered_manifest,
461 expected,
462 })
463}
464
465fn filter_manifest(manifest: CollectionManifest, filter_option: Option<&FilterEngine>) -> Result<CollectionManifest> {
466 let Some(filter) = filter_option else {
467 return Ok(manifest);
468 };
469 let version = Version::parse(&manifest.version)
470 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid collection version: {error}")))?;
471 let mut filtered = manifest;
472 filtered.entries.retain(|entry| {
473 let filter_entry = FilterEntry::new(&entry.logical_path, entry.size).with_version(version.clone());
474 filter.evaluate(&filter_entry) == FilterAction::Accept
475 });
476 filtered.signature = None;
477 filtered.public_key = None;
478 filtered.validate()?;
479 Ok(filtered)
480}
481
482async fn read_payload_to_vec<R>(reader: &mut R, size: u64, context: &'static str) -> Result<Vec<u8>>
483where
484 R: AsyncRead + Unpin,
485{
486 let capacity =
487 usize::try_from(size).map_err(|error| SyncwebError::operation("drop payload is too large", error))?;
488 let mut bytes = Vec::with_capacity(capacity);
489 let mut buffer = vec![0_u8; COPY_BUFFER_SIZE];
490 let mut remaining = size;
491 while remaining > 0 {
492 let requested = usize::try_from(
493 remaining.min(
494 u64::try_from(buffer.len())
495 .map_err(|error| SyncwebError::operation("drop buffer length is not representable", error))?,
496 ),
497 )
498 .map_err(|error| SyncwebError::operation("drop payload size is not representable", error))?;
499 let chunk = buffer
500 .get_mut(..requested)
501 .ok_or_else(|| SyncwebError::operation("drop payload range is invalid", requested))?;
502 read_exact(reader, chunk, context).await?;
503 bytes.extend_from_slice(chunk);
504 remaining = remaining
505 .checked_sub(
506 u64::try_from(requested)
507 .map_err(|error| SyncwebError::operation("drop payload is too large", error))?,
508 )
509 .ok_or_else(|| SyncwebError::operation("drop payload size underflow", context))?;
510 }
511 Ok(bytes)
512}
513
514async fn read_payload_to_path<R>(
515 reader: &mut R,
516 size: u64,
517 destination: Option<&Path>,
518 pool: Option<&ManagedPool>,
519) -> Result<(Hash, u64)>
520where
521 R: AsyncRead + Unpin,
522{
523 let mut output = match destination {
524 Some(destination_path) => Some(
525 fs::File::create(destination_path)
526 .await
527 .map_err(|error| SyncwebError::operation("failed to stage imported content", error))?,
528 ),
529 None => None,
530 };
531 let mut buffer = vec![0_u8; COPY_BUFFER_SIZE];
532 let mut remaining = size;
533 let mut copied = 0_u64;
534 let mut hasher = blake3::Hasher::new();
535 while remaining > 0 {
536 let requested = usize::try_from(
537 remaining.min(
538 u64::try_from(buffer.len())
539 .map_err(|error| SyncwebError::operation("drop buffer length is not representable", error))?,
540 ),
541 )
542 .map_err(|error| SyncwebError::operation("drop payload size is not representable", error))?;
543 let chunk = buffer
544 .get_mut(..requested)
545 .ok_or_else(|| SyncwebError::operation("drop payload range is invalid", requested))?;
546 read_exact(reader, chunk, "failed to read drop content").await?;
547 if let Some(output_file) = &mut output {
548 output_file.write_all(chunk).await?;
549 }
550 in_pool(pool, || hasher.update(chunk));
551 let requested_u64 =
552 u64::try_from(requested).map_err(|error| SyncwebError::operation("drop payload is too large", error))?;
553 remaining = remaining
554 .checked_sub(requested_u64)
555 .ok_or_else(|| SyncwebError::operation("drop payload size underflow", "invalid section length"))?;
556 copied = copied
557 .checked_add(requested_u64)
558 .ok_or_else(|| SyncwebError::operation("drop payload size overflow", "u64 limit exceeded"))?;
559 }
560 if let Some(output_file) = &mut output {
561 output_file.flush().await?;
562 }
563 let hash = in_pool(pool, || Hash::from_bytes(*hasher.finalize().as_bytes()));
564 Ok((hash, copied))
565}
566
567fn in_pool<F, R>(pool: Option<&ManagedPool>, operation: F) -> R
568where
569 F: FnOnce() -> R + Send,
570 R: Send,
571{
572 match pool {
573 Some(managed_pool) => managed_pool.install(operation),
574 None => operation(),
575 }
576}
577
578async fn read_section_header<R>(reader: &mut R) -> Result<Option<SectionHeader>>
579where
580 R: AsyncRead + Unpin,
581{
582 let Some(section_size) = read_varint(reader, true, "CAR section length").await? else {
583 return Ok(None);
584 };
585 let mut remaining = section_size;
586 let version = read_section_varint(reader, &mut remaining, "CID version").await?;
587 let codec = read_section_varint(reader, &mut remaining, "CID codec").await?;
588 let hash_code = read_section_varint(reader, &mut remaining, "CID multihash code").await?;
589 let hash_size = read_section_varint(reader, &mut remaining, "CID multihash length").await?;
590 if version != CID_VERSION || codec != RAW_CODEC || hash_code != BLAKE3_MULTIHASH || hash_size != HASH_SIZE {
591 return Err(SyncwebError::InvalidConfig(
592 "drop contains an unsupported content identifier".to_owned(),
593 ));
594 }
595 let mut hash_bytes = [0_u8; 32];
596 read_section_bytes(reader, &mut remaining, &mut hash_bytes, "CID digest").await?;
597 Ok(Some(SectionHeader {
598 hash: Hash::from_bytes(hash_bytes),
599 payload_size: remaining,
600 }))
601}
602
603async fn read_required_varint<R>(reader: &mut R, name: &'static str) -> Result<u64>
604where
605 R: AsyncRead + Unpin,
606{
607 read_varint(reader, false, name)
608 .await?
609 .ok_or_else(|| SyncwebError::InvalidConfig(format!("drop archive ended before {name}")))
610}
611
612async fn read_varint<R>(reader: &mut R, allow_eof: bool, name: &'static str) -> Result<Option<u64>>
613where
614 R: AsyncRead + Unpin,
615{
616 let mut byte = [0_u8; 1];
617 let count = reader
618 .read(&mut byte)
619 .await
620 .map_err(|error| SyncwebError::operation("failed to read drop archive", error))?;
621 if count == 0 {
622 if allow_eof {
623 return Ok(None);
624 }
625 return Err(SyncwebError::InvalidConfig(format!("drop archive ended before {name}")));
626 }
627
628 let mut value = 0_u64;
629 let mut shift = 0_u32;
630 for index in 0..10 {
631 if index > 0 {
632 read_exact(reader, &mut byte, "failed to read drop archive").await?;
633 }
634 let current = byte[0];
635 let bits = u64::from(current & 0x7f);
636 if shift == 63 && bits > 1 {
637 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
638 }
639 value |= bits << shift;
640 if current & 0x80 == 0 {
641 return Ok(Some(value));
642 }
643 if index == 9 {
644 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
645 }
646 shift = shift
647 .checked_add(7)
648 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
649 }
650 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
651}
652
653async fn read_section_varint<R>(reader: &mut R, remaining: &mut u64, name: &'static str) -> Result<u64>
654where
655 R: AsyncRead + Unpin,
656{
657 let mut value = 0_u64;
658 let mut shift = 0_u32;
659 for index in 0..10 {
660 let byte = read_section_byte(reader, remaining, name).await?;
661 let bits = u64::from(byte & 0x7f);
662 if shift == 63 && bits > 1 {
663 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
664 }
665 value |= bits << shift;
666 if byte & 0x80 == 0 {
667 return Ok(value);
668 }
669 if index == 9 {
670 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
671 }
672 shift = shift
673 .checked_add(7)
674 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
675 }
676 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
677}
678
679async fn read_section_byte<R>(reader: &mut R, remaining: &mut u64, name: &'static str) -> Result<u8>
680where
681 R: AsyncRead + Unpin,
682{
683 if *remaining == 0 {
684 return Err(SyncwebError::InvalidConfig(format!("CAR section ends before {name}")));
685 }
686 let mut byte = [0_u8; 1];
687 read_exact(reader, &mut byte, "failed to read drop section").await?;
688 *remaining = remaining
689 .checked_sub(1)
690 .ok_or_else(|| SyncwebError::operation("CAR section length underflow", name))?;
691 Ok(byte[0])
692}
693
694async fn read_section_bytes<R>(reader: &mut R, remaining: &mut u64, bytes: &mut [u8], name: &'static str) -> Result<()>
695where
696 R: AsyncRead + Unpin,
697{
698 let length = u64::try_from(bytes.len())
699 .map_err(|error| SyncwebError::operation("CAR field length is not representable", error))?;
700 if length > *remaining {
701 return Err(SyncwebError::InvalidConfig(format!("CAR section ends before {name}")));
702 }
703 read_exact(reader, bytes, "failed to read drop section").await?;
704 *remaining = remaining
705 .checked_sub(length)
706 .ok_or_else(|| SyncwebError::operation("CAR section length underflow", name))?;
707 Ok(())
708}
709
710async fn read_exact<R>(reader: &mut R, bytes: &mut [u8], context: &'static str) -> Result<()>
711where
712 R: AsyncRead + Unpin,
713{
714 reader
715 .read_exact(bytes)
716 .await
717 .map(|_| ())
718 .map_err(|error| SyncwebError::operation(context, error))
719}
720
721fn parse_car_header(header: &[u8]) -> Result<Hash> {
722 let mut offset = 0_usize;
723 if read_slice_byte(header, &mut offset, "CAR root field")? != 0x0a {
724 return Err(SyncwebError::InvalidConfig(
725 "CAR header does not contain a root CID".to_owned(),
726 ));
727 }
728 let cid_length = usize::try_from(read_slice_varint(header, &mut offset, "CAR root CID length")?)
729 .map_err(|error| SyncwebError::operation("CAR root CID length is not representable", error))?;
730 let cid_end = offset
731 .checked_add(cid_length)
732 .ok_or_else(|| SyncwebError::operation("CAR root CID range overflow", "usize limit exceeded"))?;
733 let cid = header
734 .get(offset..cid_end)
735 .ok_or_else(|| SyncwebError::InvalidConfig("CAR root CID is truncated".to_owned()))?;
736 offset = cid_end;
737 if read_slice_byte(header, &mut offset, "CAR version field")? != 0x10 {
738 return Err(SyncwebError::InvalidConfig(
739 "CAR header does not contain a version".to_owned(),
740 ));
741 }
742 if read_slice_varint(header, &mut offset, "CAR version")? != CAR_VERSION || offset != header.len() {
743 return Err(SyncwebError::InvalidConfig(
744 "unsupported or malformed CAR header".to_owned(),
745 ));
746 }
747 parse_cid(cid)
748}
749
750fn parse_cid(cid: &[u8]) -> Result<Hash> {
751 let mut offset = 0_usize;
752 let version = read_slice_varint(cid, &mut offset, "CID version")?;
753 let codec = read_slice_varint(cid, &mut offset, "CID codec")?;
754 let hash_code = read_slice_varint(cid, &mut offset, "CID multihash code")?;
755 let hash_size = read_slice_varint(cid, &mut offset, "CID multihash length")?;
756 if version != CID_VERSION || codec != RAW_CODEC || hash_code != BLAKE3_MULTIHASH || hash_size != HASH_SIZE {
757 return Err(SyncwebError::InvalidConfig(
758 "drop contains an unsupported root content identifier".to_owned(),
759 ));
760 }
761 let hash_end = offset
762 .checked_add(
763 usize::try_from(HASH_SIZE)
764 .map_err(|error| SyncwebError::operation("CID hash length is not representable", error))?,
765 )
766 .ok_or_else(|| SyncwebError::operation("CID hash range overflow", "usize limit exceeded"))?;
767 let digest = cid
768 .get(offset..hash_end)
769 .ok_or_else(|| SyncwebError::InvalidConfig("CID digest is truncated".to_owned()))?;
770 if hash_end != cid.len() {
771 return Err(SyncwebError::InvalidConfig("CID contains trailing bytes".to_owned()));
772 }
773 let hash_bytes: [u8; 32] = digest
774 .try_into()
775 .map_err(|error| SyncwebError::InvalidConfig(format!("CID digest must be 32 bytes: {error}")))?;
776 Ok(Hash::from_bytes(hash_bytes))
777}
778
779fn read_slice_byte(bytes: &[u8], offset: &mut usize, name: &'static str) -> Result<u8> {
780 let byte = bytes
781 .get(*offset)
782 .copied()
783 .ok_or_else(|| SyncwebError::InvalidConfig(format!("{name} is truncated")))?;
784 *offset = offset
785 .checked_add(1)
786 .ok_or_else(|| SyncwebError::operation("CAR offset overflow", name))?;
787 Ok(byte)
788}
789
790fn read_slice_varint(bytes: &[u8], offset: &mut usize, name: &'static str) -> Result<u64> {
791 let mut value = 0_u64;
792 let mut shift = 0_u32;
793 for index in 0..10 {
794 let byte = read_slice_byte(bytes, offset, name)?;
795 let bits = u64::from(byte & 0x7f);
796 if shift == 63 && bits > 1 {
797 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
798 }
799 value |= bits << shift;
800 if byte & 0x80 == 0 {
801 return Ok(value);
802 }
803 if index == 9 {
804 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
805 }
806 shift = shift
807 .checked_add(7)
808 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
809 }
810 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
811}
812
813async fn remove_staging(path: &Path) -> Result<()> {
814 match fs::remove_dir_all(path).await {
815 Ok(()) => Ok(()),
816 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
817 Err(error) => Err(SyncwebError::operation(
818 "failed to remove drop staging directory",
819 error,
820 )),
821 }
822}