1use std::{
3 collections::HashSet,
4 fs::{self, File, OpenOptions},
5 io::Write,
6 path::{Path, PathBuf},
7 sync::{Arc, Mutex},
8};
9
10use objects::store::{
11 ObjectStore,
12 pack::{PackContainerSpec, PackIndex, PackObjectId, PackReader, verify_container},
13};
14
15use crate::{
16 MAX_RECEIVED_PACK_SIZE, NativePackBundle, ProtocolError, Result, native_pack::unique_spool_dir,
17};
18
19const PACK_HEADER_LEN: usize = 16;
20const PACK_TRAILER_LEN: usize = 32;
21const PACK_SPEC: PackContainerSpec = PackContainerSpec {
22 magic: b"LMPK",
23 version: 4,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct ProviderPackIndexEntry {
28 pub id: PackObjectId,
29 pub output_offset: u64,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ProviderPackExtent {
34 pub output_offset: u64,
35 pub length: u64,
36 pub digest: [u8; 32],
37 pub objects: Vec<ProviderPackIndexEntry>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ProviderPackManifest {
42 pub header: [u8; PACK_HEADER_LEN],
43 pub output_pack_length: u64,
44 pub extents: Vec<ProviderPackExtent>,
45}
46
47#[derive(Debug)]
48pub struct ProviderPackBundle {
49 pub pack: NativePackBundle,
50 pub trailer_digest: [u8; 32],
51}
52
53#[derive(Clone, Debug)]
55pub struct ProviderPackWriter {
56 file: Arc<File>,
57 ranges: Arc<Vec<(u64, u64)>>,
58 verified: Arc<Mutex<Vec<bool>>>,
59}
60
61#[derive(Debug)]
63pub struct ProviderPackSpool {
64 dir: Option<PathBuf>,
65 pack_path: PathBuf,
66 index_path: PathBuf,
67 file: Arc<File>,
68 manifest: ProviderPackManifest,
69 verified: Arc<Mutex<Vec<bool>>>,
70}
71
72#[derive(Debug)]
74pub struct CompletedProviderPack {
75 dir: PathBuf,
76 pack_path: PathBuf,
77 index_path: PathBuf,
78 pub trailer_digest: [u8; 32],
79}
80
81impl ProviderPackSpool {
82 pub fn new_in(root: &Path, manifest: ProviderPackManifest) -> Result<Self> {
84 validate_manifest(&manifest)?;
85 let base = root.join("transfer-spool");
86 fs::create_dir_all(&base)?;
87 let dir = unique_spool_dir(&base)?;
88 let pack_path = dir.join("provider.pack");
89 let index_path = dir.join("provider.idx");
90 let create_result = (|| -> Result<File> {
91 let file = OpenOptions::new()
92 .read(true)
93 .write(true)
94 .create_new(true)
95 .open(&pack_path)?;
96 file.set_len(manifest.output_pack_length)?;
97 write_all_at(&file, &manifest.header, 0)?;
98 Ok(file)
99 })();
100 let file = match create_result {
101 Ok(file) => file,
102 Err(error) => {
103 let _ = fs::remove_dir_all(&dir);
104 return Err(error);
105 }
106 };
107 let verified = Arc::new(Mutex::new(vec![false; manifest.extents.len()]));
108 Ok(Self {
109 dir: Some(dir),
110 pack_path,
111 index_path,
112 file: Arc::new(file),
113 manifest,
114 verified,
115 })
116 }
117
118 pub fn writer(&self) -> ProviderPackWriter {
120 ProviderPackWriter {
121 file: Arc::clone(&self.file),
122 ranges: Arc::new(
123 self.manifest
124 .extents
125 .iter()
126 .map(|extent| (extent.output_offset, extent.length))
127 .collect(),
128 ),
129 verified: Arc::clone(&self.verified),
130 }
131 }
132
133 pub fn finish(mut self) -> Result<CompletedProviderPack> {
135 let verified = self.verified.lock().map_err(|_| {
136 ProtocolError::InvalidState("provider spool verification lock poisoned".to_string())
137 })?;
138 if verified.iter().any(|complete| !complete) {
139 return Err(ProtocolError::InvalidState(
140 "provider spool does not have complete verified coverage".to_string(),
141 ));
142 }
143 drop(verified);
144
145 let body_end = self.manifest.output_pack_length - PACK_TRAILER_LEN as u64;
146 let trailer_digest = hash_file_prefix(&self.file, body_end)?;
147 write_all_at(&self.file, &trailer_digest, body_end)?;
148 objects::fs_atomic::sync_file(&self.file, &self.pack_path)?;
149 write_provider_index(&self.index_path, &self.manifest)?;
150
151 let reader = PackReader::open(
152 &self.pack_path,
153 &self.index_path,
154 self.dir.as_ref().ok_or_else(|| {
155 ProtocolError::InvalidState("provider spool directory is missing".into())
156 })?,
157 )?;
158 let expected_objects = self
159 .manifest
160 .extents
161 .iter()
162 .map(|extent| extent.objects.len())
163 .sum::<usize>();
164 if reader.object_count() != expected_objects {
165 return Err(ProtocolError::InvalidState(
166 "provider pack index does not match its manifest".to_string(),
167 ));
168 }
169 drop(reader);
170
171 let dir = self.dir.take().ok_or_else(|| {
172 ProtocolError::InvalidState("provider spool directory is missing".to_string())
173 })?;
174 Ok(CompletedProviderPack {
175 dir,
176 pack_path: self.pack_path.clone(),
177 index_path: self.index_path.clone(),
178 trailer_digest,
179 })
180 }
181}
182
183impl ProviderPackWriter {
184 pub fn write_extent_chunk(
186 &self,
187 extent_index: usize,
188 relative_offset: u64,
189 data: &[u8],
190 ) -> Result<()> {
191 let (output_offset, extent_len) =
192 self.ranges.get(extent_index).copied().ok_or_else(|| {
193 ProtocolError::InvalidState("provider extent index is out of range".to_string())
194 })?;
195 let data_len = u64::try_from(data.len()).map_err(|_| {
196 ProtocolError::InvalidState("provider chunk length exceeds u64".to_string())
197 })?;
198 let relative_end = relative_offset.checked_add(data_len).ok_or_else(|| {
199 ProtocolError::InvalidState("provider extent write offset overflows".to_string())
200 })?;
201 if relative_end > extent_len {
202 return Err(ProtocolError::InvalidState(
203 "provider extent write exceeds its planned range".to_string(),
204 ));
205 }
206 let absolute_offset = output_offset.checked_add(relative_offset).ok_or_else(|| {
207 ProtocolError::InvalidState("provider spool write offset overflows".to_string())
208 })?;
209 write_all_at(&self.file, data, absolute_offset)
210 }
211
212 pub fn hash_extent_prefix(
214 &self,
215 extent_index: usize,
216 prefix_len: u64,
217 hasher: &mut blake3::Hasher,
218 ) -> Result<()> {
219 let (output_offset, extent_len) =
220 self.ranges.get(extent_index).copied().ok_or_else(|| {
221 ProtocolError::InvalidState("provider extent index is out of range".to_string())
222 })?;
223 if prefix_len > extent_len {
224 return Err(ProtocolError::InvalidState(
225 "provider retained prefix exceeds its planned extent".to_string(),
226 ));
227 }
228 let mut buffer = [0_u8; 64 * 1024];
229 let mut read = 0_u64;
230 while read < prefix_len {
231 let remaining = prefix_len - read;
232 let length = usize::try_from(remaining.min(buffer.len() as u64)).map_err(|_| {
233 ProtocolError::InvalidState("provider prefix length exceeds usize".to_string())
234 })?;
235 let offset = output_offset.checked_add(read).ok_or_else(|| {
236 ProtocolError::InvalidState("provider prefix read offset overflows".to_string())
237 })?;
238 read_exact_at(&self.file, &mut buffer[..length], offset)?;
239 hasher.update(&buffer[..length]);
240 read += length as u64;
241 }
242 Ok(())
243 }
244
245 pub fn mark_verified(&self, extent_index: usize) -> Result<()> {
247 let mut verified = self.verified.lock().map_err(|_| {
248 ProtocolError::InvalidState("provider spool verification lock poisoned".to_string())
249 })?;
250 let complete = verified.get_mut(extent_index).ok_or_else(|| {
251 ProtocolError::InvalidState("provider extent index is out of range".to_string())
252 })?;
253 if *complete {
254 return Err(ProtocolError::InvalidState(
255 "provider extent completed more than once".to_string(),
256 ));
257 }
258 *complete = true;
259 Ok(())
260 }
261}
262
263impl CompletedProviderPack {
264 pub fn artifact_paths(&self) -> (&Path, &Path) {
266 (&self.pack_path, &self.index_path)
267 }
268
269 pub fn install_into(
271 &mut self,
272 store: &impl ObjectStore,
273 ) -> Result<objects::store::pack::PackInventory> {
274 store
275 .install_pack_streaming(&self.pack_path, &self.index_path)
276 .map_err(ProtocolError::from)
277 }
278}
279
280impl Drop for ProviderPackSpool {
281 fn drop(&mut self) {
282 if let Some(dir) = self.dir.as_ref() {
283 let _ = fs::remove_dir_all(dir);
284 }
285 }
286}
287
288impl Drop for CompletedProviderPack {
289 fn drop(&mut self) {
290 let _ = fs::remove_dir_all(&self.dir);
291 }
292}
293
294fn write_provider_index(path: &Path, manifest: &ProviderPackManifest) -> Result<()> {
295 let mut index = PackIndex::new();
296 for extent in &manifest.extents {
297 for object in &extent.objects {
298 index.add(object.id, object.output_offset);
299 }
300 }
301 index.sort();
302 let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
303 file.write_all(&index.to_bytes())?;
304 file.flush()?;
305 objects::fs_atomic::sync_file(&file, path)?;
306 Ok(())
307}
308
309fn hash_file_prefix(file: &File, length: u64) -> Result<[u8; 32]> {
310 let mut hasher = blake3::Hasher::new();
311 let mut buffer = [0_u8; 64 * 1024];
312 let mut offset = 0_u64;
313 while offset < length {
314 let remaining = length - offset;
315 let read_len = usize::try_from(remaining.min(buffer.len() as u64)).map_err(|_| {
316 ProtocolError::InvalidState("provider pack hash length exceeds usize".to_string())
317 })?;
318 read_exact_at(file, &mut buffer[..read_len], offset)?;
319 hasher.update(&buffer[..read_len]);
320 offset += read_len as u64;
321 }
322 Ok(*hasher.finalize().as_bytes())
323}
324
325#[cfg(unix)]
326fn write_all_at(file: &File, mut data: &[u8], mut offset: u64) -> Result<()> {
327 use std::os::unix::fs::FileExt;
328
329 while !data.is_empty() {
330 let written = file.write_at(data, offset)?;
331 if written == 0 {
332 return Err(ProtocolError::Io(std::io::Error::new(
333 std::io::ErrorKind::WriteZero,
334 "provider positional spool write returned zero",
335 )));
336 }
337 data = &data[written..];
338 offset = offset.checked_add(written as u64).ok_or_else(|| {
339 ProtocolError::InvalidState("provider positional write offset overflows".to_string())
340 })?;
341 }
342 Ok(())
343}
344
345#[cfg(windows)]
346fn write_all_at(file: &File, mut data: &[u8], mut offset: u64) -> Result<()> {
347 use std::os::windows::fs::FileExt;
348
349 while !data.is_empty() {
350 let written = file.seek_write(data, offset)?;
351 if written == 0 {
352 return Err(ProtocolError::Io(std::io::Error::new(
353 std::io::ErrorKind::WriteZero,
354 "provider positional spool write returned zero",
355 )));
356 }
357 data = &data[written..];
358 offset = offset.checked_add(written as u64).ok_or_else(|| {
359 ProtocolError::InvalidState("provider positional write offset overflows".to_string())
360 })?;
361 }
362 Ok(())
363}
364
365#[cfg(unix)]
366fn read_exact_at(file: &File, mut data: &mut [u8], mut offset: u64) -> Result<()> {
367 use std::os::unix::fs::FileExt;
368
369 while !data.is_empty() {
370 let read = file.read_at(data, offset)?;
371 if read == 0 {
372 return Err(ProtocolError::Io(std::io::Error::new(
373 std::io::ErrorKind::UnexpectedEof,
374 "provider positional spool read ended early",
375 )));
376 }
377 data = &mut data[read..];
378 offset = offset.checked_add(read as u64).ok_or_else(|| {
379 ProtocolError::InvalidState("provider positional read offset overflows".to_string())
380 })?;
381 }
382 Ok(())
383}
384
385#[cfg(windows)]
386fn read_exact_at(file: &File, mut data: &mut [u8], mut offset: u64) -> Result<()> {
387 use std::os::windows::fs::FileExt;
388
389 while !data.is_empty() {
390 let read = file.seek_read(data, offset)?;
391 if read == 0 {
392 return Err(ProtocolError::Io(std::io::Error::new(
393 std::io::ErrorKind::UnexpectedEof,
394 "provider positional spool read ended early",
395 )));
396 }
397 data = &mut data[read..];
398 offset = offset.checked_add(read as u64).ok_or_else(|| {
399 ProtocolError::InvalidState("provider positional read offset overflows".to_string())
400 })?;
401 }
402 Ok(())
403}
404
405pub fn assemble_provider_pack(
411 manifest: &ProviderPackManifest,
412 extent_bodies: &[Vec<u8>],
413) -> Result<ProviderPackBundle> {
414 validate_manifest(manifest)?;
415 if extent_bodies.len() != manifest.extents.len() {
416 return Err(ProtocolError::InvalidState(format!(
417 "provider extent count mismatch: expected {}, got {}",
418 manifest.extents.len(),
419 extent_bodies.len()
420 )));
421 }
422
423 let mut order = (0..manifest.extents.len()).collect::<Vec<_>>();
424 order.sort_unstable_by_key(|index| manifest.extents[*index].output_offset);
425 let output_len = usize::try_from(manifest.output_pack_length).map_err(|_| {
426 ProtocolError::InvalidState("provider output pack exceeds this platform".to_string())
427 })?;
428 let mut pack_data = Vec::with_capacity(output_len);
429 pack_data.extend_from_slice(&manifest.header);
430 for index in order {
431 let extent = &manifest.extents[index];
432 let body = &extent_bodies[index];
433 let expected_len = usize::try_from(extent.length).map_err(|_| {
434 ProtocolError::InvalidState("provider extent exceeds this platform".to_string())
435 })?;
436 if body.len() != expected_len {
437 return Err(ProtocolError::InvalidState(format!(
438 "provider extent length mismatch: expected {}, got {}",
439 extent.length,
440 body.len()
441 )));
442 }
443 if blake3::hash(body).as_bytes() != &extent.digest {
444 return Err(ProtocolError::InvalidState(
445 "provider extent digest mismatch".to_string(),
446 ));
447 }
448 pack_data.extend_from_slice(body);
449 }
450
451 let trailer_digest = *blake3::hash(&pack_data).as_bytes();
452 pack_data.extend_from_slice(&trailer_digest);
453 if pack_data.len() != output_len {
454 return Err(ProtocolError::InvalidState(format!(
455 "provider output pack length mismatch: expected {}, got {}",
456 manifest.output_pack_length,
457 pack_data.len()
458 )));
459 }
460 verify_container(&pack_data, PACK_SPEC).map_err(ProtocolError::from)?;
461
462 let mut index = PackIndex::new();
463 for extent in &manifest.extents {
464 for object in &extent.objects {
465 index.add(object.id, object.output_offset);
466 }
467 }
468 index.sort();
469
470 Ok(ProviderPackBundle {
471 pack: NativePackBundle {
472 pack_data,
473 index_data: index.to_bytes(),
474 },
475 trailer_digest,
476 })
477}
478
479fn validate_manifest(manifest: &ProviderPackManifest) -> Result<()> {
480 if manifest.output_pack_length > MAX_RECEIVED_PACK_SIZE
481 || manifest.output_pack_length < (PACK_HEADER_LEN + PACK_TRAILER_LEN) as u64
482 {
483 return Err(ProtocolError::InvalidState(
484 "provider output pack length is invalid".to_string(),
485 ));
486 }
487 if &manifest.header[..4] != PACK_SPEC.magic
488 || u32::from_be_bytes(manifest.header[4..8].try_into().map_err(|_| {
489 ProtocolError::InvalidState("provider pack header is truncated".to_string())
490 })?) != PACK_SPEC.version
491 {
492 return Err(ProtocolError::InvalidState(
493 "provider pack header has invalid magic or version".to_string(),
494 ));
495 }
496 let object_count = u64::from_be_bytes(manifest.header[8..16].try_into().map_err(|_| {
497 ProtocolError::InvalidState("provider pack header is truncated".to_string())
498 })?);
499 let expected_body_end = manifest.output_pack_length - PACK_TRAILER_LEN as u64;
500 let mut order = manifest.extents.iter().collect::<Vec<_>>();
501 order.sort_unstable_by_key(|extent| extent.output_offset);
502 let mut next_offset = PACK_HEADER_LEN as u64;
503 let mut ids = HashSet::new();
504 let mut object_offsets = HashSet::new();
505 let mut actual_object_count = 0_u64;
506
507 for extent in order {
508 if extent.length == 0 || extent.output_offset != next_offset {
509 return Err(ProtocolError::InvalidState(
510 "provider extents do not exactly cover the virtual pack body".to_string(),
511 ));
512 }
513 next_offset = extent
514 .output_offset
515 .checked_add(extent.length)
516 .ok_or_else(|| {
517 ProtocolError::InvalidState("provider extent output range overflows".to_string())
518 })?;
519 if next_offset > expected_body_end {
520 return Err(ProtocolError::InvalidState(
521 "provider extent exceeds the virtual pack body".to_string(),
522 ));
523 }
524 for object in &extent.objects {
525 if object.output_offset < extent.output_offset
526 || object.output_offset >= next_offset
527 || !ids.insert(object.id)
528 || !object_offsets.insert(object.output_offset)
529 {
530 return Err(ProtocolError::InvalidState(
531 "provider object index is outside its extent or duplicated".to_string(),
532 ));
533 }
534 actual_object_count = actual_object_count.checked_add(1).ok_or_else(|| {
535 ProtocolError::InvalidState("provider object count overflows".to_string())
536 })?;
537 }
538 }
539 if next_offset != expected_body_end || actual_object_count != object_count {
540 return Err(ProtocolError::InvalidState(
541 "provider manifest body or object count is incomplete".to_string(),
542 ));
543 }
544 Ok(())
545}
546
547#[cfg(test)]
548mod tests {
549 use objects::{
550 object::ContentHash,
551 store::{
552 CompressionConfig,
553 pack::{ObjectType, PackBuilder, PackIndex, PackObjectId},
554 },
555 };
556
557 use super::*;
558
559 fn source_pack() -> (Vec<u8>, Vec<u8>, Vec<PackObjectId>) {
560 let ids = vec![
561 PackObjectId::Hash(ContentHash::from_bytes([1; 32])),
562 PackObjectId::Hash(ContentHash::from_bytes([2; 32])),
563 ];
564 let mut builder = PackBuilder::new(CompressionConfig {
565 enabled: false,
566 ..CompressionConfig::default()
567 });
568 builder.add_id(ids[0], ObjectType::Blob, b"provider-one".to_vec());
569 builder.add_id(ids[1], ObjectType::Blob, b"provider-two".to_vec());
570 let (pack, index, _) = builder.build().unwrap();
571 (pack, index, ids)
572 }
573
574 fn split_manifest() -> (ProviderPackManifest, Vec<Vec<u8>>, Vec<u8>, Vec<u8>) {
575 let (pack, index, ids) = source_pack();
576 let parsed_index = PackIndex::from_bytes(&index).unwrap();
577 let first = parsed_index
578 .find(&ids[0])
579 .unwrap()
580 .expect("first fixture object indexed");
581 let second = parsed_index
582 .find(&ids[1])
583 .unwrap()
584 .expect("second fixture object indexed");
585 let (first_id, first_offset, second_id, second_offset) = if first < second {
586 (ids[0], first, ids[1], second)
587 } else {
588 (ids[1], second, ids[0], first)
589 };
590 let body_end = pack.len() - PACK_TRAILER_LEN;
591 let first_body = pack[first_offset as usize..second_offset as usize].to_vec();
592 let second_body = pack[second_offset as usize..body_end].to_vec();
593 let manifest = ProviderPackManifest {
594 header: pack[..PACK_HEADER_LEN].try_into().unwrap(),
595 output_pack_length: pack.len() as u64,
596 extents: vec![
597 ProviderPackExtent {
598 output_offset: first_offset,
599 length: first_body.len() as u64,
600 digest: *blake3::hash(&first_body).as_bytes(),
601 objects: vec![ProviderPackIndexEntry {
602 id: first_id,
603 output_offset: first_offset,
604 }],
605 },
606 ProviderPackExtent {
607 output_offset: second_offset,
608 length: second_body.len() as u64,
609 digest: *blake3::hash(&second_body).as_bytes(),
610 objects: vec![ProviderPackIndexEntry {
611 id: second_id,
612 output_offset: second_offset,
613 }],
614 },
615 ],
616 };
617 (manifest, vec![first_body, second_body], pack, index)
618 }
619
620 #[test]
621 fn provider_and_ordinary_pack_results_are_byte_identical() {
622 let (manifest, bodies, source_pack, source_index) = split_manifest();
623
624 let assembled = assemble_provider_pack(&manifest, &bodies).unwrap();
625
626 assert_eq!(assembled.pack.pack_data, source_pack);
627 assert_eq!(assembled.pack.index_data, source_index);
628 let source_digest = blake3::Hash::from_bytes(
629 source_pack[source_pack.len() - PACK_TRAILER_LEN..]
630 .try_into()
631 .unwrap(),
632 );
633 println!(
634 "byte_identical provider_digest={} weft_digest={} pack_bytes={} index_bytes={} identical=true",
635 blake3::Hash::from_bytes(assembled.trailer_digest),
636 source_digest,
637 source_pack.len(),
638 source_index.len(),
639 );
640 }
641
642 #[test]
643 fn digest_mismatch_never_produces_an_installable_pack() {
644 let (manifest, mut bodies, _, _) = split_manifest();
645 bodies[1][0] ^= 0xff;
646
647 let error = assemble_provider_pack(&manifest, &bodies).unwrap_err();
648
649 assert!(error.to_string().contains("digest mismatch"));
650 }
651
652 #[test]
653 fn manifest_gaps_and_invalid_or_duplicate_index_entries_fail_closed() {
654 let (mut manifest, bodies, _, _) = split_manifest();
655 manifest.extents[1].output_offset += 1;
656 assert!(assemble_provider_pack(&manifest, &bodies).is_err());
657
658 let (mut manifest, bodies, _, _) = split_manifest();
659 manifest.extents[1].objects[0].output_offset = manifest.extents[0].output_offset;
660 assert!(assemble_provider_pack(&manifest, &bodies).is_err());
661
662 let (mut manifest, bodies, _, _) = split_manifest();
663 manifest.extents[1].objects[0].id = manifest.extents[0].objects[0].id;
664 assert!(assemble_provider_pack(&manifest, &bodies).is_err());
665 }
666
667 #[test]
668 fn positional_spool_accepts_out_of_order_extents_and_is_byte_identical() {
669 let (manifest, bodies, source_pack, source_index) = split_manifest();
670 let root = tempfile::tempdir().unwrap();
671 let spool = ProviderPackSpool::new_in(root.path(), manifest).unwrap();
672 assert_eq!(
673 spool.file.metadata().unwrap().len(),
674 source_pack.len() as u64,
675 "the sparse spool must be pre-sized to the exact virtual pack length"
676 );
677 let writer = spool.writer();
678
679 writer.write_extent_chunk(1, 0, &bodies[1]).unwrap();
680 writer.mark_verified(1).unwrap();
681 writer.write_extent_chunk(0, 0, &bodies[0]).unwrap();
682 writer.mark_verified(0).unwrap();
683 drop(writer);
684
685 let completed = spool.finish().unwrap();
686 assert_eq!(fs::read(&completed.pack_path).unwrap(), source_pack);
687 assert_eq!(fs::read(&completed.index_path).unwrap(), source_index);
688 }
689
690 #[test]
691 fn positional_spool_rejects_range_overrun_duplicate_and_incomplete_coverage() {
692 let (manifest, bodies, _, _) = split_manifest();
693 let root = tempfile::tempdir().unwrap();
694 let spool = ProviderPackSpool::new_in(root.path(), manifest).unwrap();
695 let spool_dir = spool.dir.clone().unwrap();
696 let writer = spool.writer();
697
698 assert!(
699 writer
700 .write_extent_chunk(0, bodies[0].len() as u64, &[1])
701 .is_err()
702 );
703 writer.write_extent_chunk(0, 0, &bodies[0]).unwrap();
704 writer.mark_verified(0).unwrap();
705 assert!(writer.mark_verified(0).is_err());
706 drop(writer);
707
708 assert!(spool.finish().is_err());
709 assert!(
710 !spool_dir.exists(),
711 "failed finalization must remove partial spool state"
712 );
713 }
714
715 #[test]
716 fn dropping_partial_spool_removes_all_transfer_state() {
717 let (manifest, bodies, _, _) = split_manifest();
718 let root = tempfile::tempdir().unwrap();
719 let spool = ProviderPackSpool::new_in(root.path(), manifest).unwrap();
720 let spool_dir = spool.dir.clone().unwrap();
721 let writer = spool.writer();
722 writer
723 .write_extent_chunk(0, 0, &bodies[0][..bodies[0].len() / 2])
724 .unwrap();
725
726 drop(writer);
727 drop(spool);
728
729 assert!(!spool_dir.exists());
730 }
731}