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