1use std::fs::{self, File};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7use sha2::{Digest, Sha256};
8use tempfile::NamedTempFile;
9
10use super::{
11 ChunkDescriptor, ChunkSpool, EWF1_TABLE_GROUP_MAX_ENTRIES, Ewf1SegmentSections,
12 Ewf1SegmentWriteContext, TerminalSection, WriteCompression, WriteHashState, WriteOptions,
13 WriteResult, effective_write_hashes, encode_chunk, header_payload, header2_payload,
14 publication_segment_paths, segment_path, validate_options, write_ewf1_segment,
15 writer_chunk_geometry, xheader_payload,
16};
17use crate::publication::{OutputLock, acquisition_path, sync_dir};
18use crate::{AcquisitionError, EwfError, EwfMetadata, Result};
19
20mod checkpoint;
21pub use checkpoint::{
22 AcquisitionCheckpoint, AcquisitionOperationPhase, AcquisitionOperationProgress,
23};
24use checkpoint::{load_checkpoint, operation_progress};
25use std::ops::ControlFlow;
26mod source;
27pub use source::{
28 AcquisitionOutcome, AcquisitionProgress, AcquisitionReadOptions, AcquisitionStatus,
29 UnreadableSectorPolicy,
30};
31
32#[cfg(test)]
33mod tests;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct AcquisitionOptions {
39 pub source_size: u64,
41 pub bytes_per_sector: u32,
43 pub sectors_per_chunk: u32,
45 pub chunks_per_segment: u32,
48 pub compression: WriteCompression,
50 pub metadata: EwfMetadata,
52}
53
54impl AcquisitionOptions {
55 pub fn new(source_size: u64) -> Self {
57 Self {
58 source_size,
59 bytes_per_sector: 512,
60 sectors_per_chunk: 64,
61 chunks_per_segment: EWF1_TABLE_GROUP_MAX_ENTRIES as u32,
62 compression: WriteCompression::Zlib,
63 metadata: EwfMetadata::default(),
64 }
65 }
66
67 fn writer_options(&self) -> Result<WriteOptions> {
68 let (chunk_size, _) = writer_chunk_geometry(self.sectors_per_chunk, self.bytes_per_sector)?;
69 if self.source_size == 0
70 || !self
71 .source_size
72 .is_multiple_of(u64::from(self.bytes_per_sector))
73 || chunk_size > 16 * 1024 * 1024
74 || self.chunks_per_segment == 0
75 || self.chunks_per_segment as usize > EWF1_TABLE_GROUP_MAX_ENTRIES
76 || self.source_size.div_ceil(chunk_size) > u64::from(u32::MAX)
77 {
78 return Err(EwfError::Unsupported(
79 "unsupported acquisition geometry".into(),
80 ));
81 }
82 let options = WriteOptions {
83 sectors_per_chunk: self.sectors_per_chunk,
84 bytes_per_sector: self.bytes_per_sector,
85 compression: self.compression,
86 media_size: Some(self.source_size),
87 metadata: self.metadata.clone(),
88 ..WriteOptions::default()
89 };
90 validate_options(&options)?;
91 Ok(options)
92 }
93}
94
95pub struct AcquisitionWriter {
117 first: PathBuf,
118 state: PathBuf,
119 options: WriteOptions,
120 source_size: u64,
121 chunk_size: usize,
122 chunks_per_segment: usize,
123 offset: u64,
124 pending: Vec<u8>,
125 chunks: Vec<ChunkDescriptor>,
126 spool: Option<ChunkSpool>,
127 hashes: WriteHashState,
128 sealed: Vec<Seal>,
129 errors: Vec<AcquisitionError>,
130 substituted_sectors: u64,
131 failed: bool,
132 #[cfg(test)]
133 fail_seal: bool,
134 _lock: OutputLock,
135}
136
137#[derive(Clone)]
138struct Seal {
139 end: u64,
140 size: u64,
141 digest: [u8; 32],
142}
143
144impl AcquisitionWriter {
145 pub fn create(
148 first: impl AsRef<Path>,
149 options: &AcquisitionOptions,
150 source_identity: [u8; 32],
151 ) -> Result<Self> {
152 let write_options = options.writer_options()?;
153 let first = normalize_first(first.as_ref())?;
154 validate_segment_budget(&first, options)?;
155 let lock = OutputLock::acquire(&first)?;
156 crate::publication::ensure_no_pending(&first)?;
157 ensure_output_absent(&first)?;
158 let state = acquisition_path(&first)?;
159 let init = tempfile::Builder::new()
160 .prefix(".ewf-acquisition-init-")
161 .tempdir_in(crate::segment::segment_dir(&first))?;
162 fs::create_dir(init.path().join("scratch"))?;
163 atomic_file(
164 init.path(),
165 &init.path().join("config"),
166 &fingerprint(&first, options, &write_options, source_identity)?,
167 )?;
168 let probe = init.path().join("hard-link-probe");
171 fs::hard_link(init.path().join("config"), &probe)?;
172 fs::remove_file(probe)?;
173 sync_dir(init.path())?;
174 fs::rename(init.path(), &state)?;
175 sync_dir(crate::segment::segment_dir(&first))?;
176 Self::new(first, state, write_options, options, lock)
177 }
178
179 pub fn resume(
184 first: impl AsRef<Path>,
185 options: &AcquisitionOptions,
186 source_identity: [u8; 32],
187 ) -> Result<Self> {
188 Self::resume_with_progress(first, options, source_identity, |_| {
189 ControlFlow::Continue(())
190 })
191 }
192
193 pub fn resume_with_progress(
196 first: impl AsRef<Path>,
197 options: &AcquisitionOptions,
198 source_identity: [u8; 32],
199 mut on_progress: impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
200 ) -> Result<Self> {
201 let write_options = options.writer_options()?;
202 let first = normalize_first(first.as_ref())?;
203 validate_segment_budget(&first, options)?;
204 let lock = OutputLock::acquire(&first)?;
205 let checkpoint = load_checkpoint(
206 &first,
207 options,
208 &write_options,
209 source_identity,
210 true,
211 &mut on_progress,
212 )?;
213 let state = checkpoint.state;
214 let sealed = checkpoint.sealed;
215 let previous = checkpoint.report.checkpoint_bytes;
216 let count = sealed.len();
217 let errors = checkpoint.report.acquisition_errors;
218 let mut hashes = WriteHashState::new();
219 if let Some(image) = checkpoint.image {
220 let mut progress =
221 operation_progress(AcquisitionOperationPhase::RehashingMedia, previous, count);
222 checkpoint::notify(progress, &mut on_progress)?;
223 let mut cursor = image.cursor();
224 let mut buffer = vec![0; image.chunk_size() as usize];
225 loop {
226 let size = cursor.read(&mut buffer)?;
227 if size == 0 {
228 break;
229 }
230 hashes.update(&buffer[..size]);
231 progress.bytes_processed += size as u64;
232 progress.segments_processed =
233 sealed.partition_point(|seal| seal.end <= progress.bytes_processed);
234 checkpoint::notify(progress, &mut on_progress)?;
235 }
236 }
237 if previous < options.source_size {
240 let unsealed = staged_path(&first, &state, count + 1)?;
241 match fs::symlink_metadata(&unsealed) {
242 Ok(metadata) => {
243 require_file_type(&metadata)?;
244 fs::remove_file(unsealed)?;
245 }
246 Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
247 Err(error) => return Err(error.into()),
248 }
249 }
250 for entry in fs::read_dir(state.join("scratch"))? {
251 let entry = entry?;
252 require_file_type(&fs::symlink_metadata(entry.path())?)?;
253 fs::remove_file(entry.path())?;
254 }
255 let mut writer = Self::new(first, state, write_options, options, lock)?;
256 writer.offset = previous;
257 writer.hashes = hashes;
258 writer.sealed = sealed;
259 writer.substituted_sectors = errors.iter().map(|range| range.sector_count).sum();
260 writer.errors = errors;
261 Ok(writer)
262 }
263
264 fn new(
265 first: PathBuf,
266 state: PathBuf,
267 options: WriteOptions,
268 acquisition: &AcquisitionOptions,
269 lock: OutputLock,
270 ) -> Result<Self> {
271 let chunk_size =
272 (u64::from(options.bytes_per_sector) * u64::from(options.sectors_per_chunk)) as usize;
273 let spool = ChunkSpool {
274 file: NamedTempFile::new_in(state.join("scratch"))?,
275 len: 0,
276 };
277 Ok(Self {
278 first,
279 state,
280 options,
281 source_size: acquisition.source_size,
282 chunk_size,
283 chunks_per_segment: acquisition.chunks_per_segment as usize,
284 offset: 0,
285 pending: Vec::with_capacity(chunk_size),
286 chunks: Vec::with_capacity(acquisition.chunks_per_segment as usize),
287 spool: Some(spool),
288 hashes: WriteHashState::new(),
289 sealed: Vec::new(),
290 errors: Vec::new(),
291 substituted_sectors: 0,
292 failed: false,
293 #[cfg(test)]
294 fail_seal: false,
295 _lock: lock,
296 })
297 }
298
299 pub fn position(&self) -> u64 {
301 self.offset
302 }
303
304 pub fn checkpoint_offset(&self) -> u64 {
306 self.sealed.last().map_or(0, |seal| seal.end)
307 }
308
309 pub fn sealed_segments(&self) -> usize {
311 self.sealed.len()
312 }
313
314 pub fn acquisition_errors(&self) -> &[AcquisitionError] {
317 &self.errors
318 }
319
320 pub fn checkpoint(&mut self) -> Result<u64> {
323 self.ensure_healthy()?;
324 if !self.chunks.is_empty() {
325 let result = self.seal(false);
326 self.failed = result.is_err();
327 result?;
328 }
329 Ok(self.checkpoint_offset())
330 }
331
332 pub fn finish(self) -> Result<WriteResult> {
338 self.finish_with_progress(|_| ControlFlow::Continue(()))
339 }
340
341 pub fn finish_with_progress(
346 mut self,
347 mut on_progress: impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
348 ) -> Result<WriteResult> {
349 self.ensure_healthy()?;
350 if self.checkpoint_offset() != self.source_size {
351 return Err(EwfError::Malformed(
352 "acquisition source is incomplete".into(),
353 ));
354 }
355 let paths = (1..=self.sealed.len())
356 .map(|index| segment_path(&self.first, index))
357 .collect::<Result<Vec<_>>>()?;
358 let stored_bytes = self
359 .sealed
360 .iter()
361 .try_fold(0_u64, |total, seal| total.checked_add(seal.size))
362 .ok_or_else(|| EwfError::Malformed("acquisition stored-size overflow".into()))?;
363 let mut progress = operation_progress(
364 AcquisitionOperationPhase::ValidatingSegments,
365 stored_bytes,
366 self.sealed.len(),
367 );
368 checkpoint::notify(progress, &mut on_progress)?;
369 for (index, seal) in self.sealed.iter().enumerate() {
370 seal.validate_with_progress(
371 &staged_path(&self.first, &self.state, index + 1)?,
372 &mut progress,
373 &mut on_progress,
374 )?;
375 }
376 let mut progress = operation_progress(
377 AcquisitionOperationPhase::Publishing,
378 stored_bytes,
379 self.sealed.len(),
380 );
381 checkpoint::notify(progress, &mut on_progress)?;
382 let publishing = self.state.join("publishing");
383 if !publishing.try_exists()? {
384 ensure_output_absent(&self.first)?;
385 atomic_file(&self.state.join("scratch"), &publishing, &[])?;
386 sync_dir(&self.state)?;
387 }
388 #[cfg(test)]
389 tests::crash_at("publishing", 0);
390 if publication_segment_paths(&paths, false)?.len() != paths.len() {
391 return Err(EwfError::Malformed(
392 "unexpected acquisition output segments".into(),
393 ));
394 }
395 for (index, (path, seal)) in paths.iter().zip(&self.sealed).enumerate() {
396 let staged = staged_path(&self.first, &self.state, index + 1)?;
397 match fs::hard_link(staged, path) {
398 Ok(()) => (),
399 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
400 let mut validation = operation_progress(
403 AcquisitionOperationPhase::ValidatingSegments,
404 seal.size,
405 1,
406 );
407 seal.validate_with_progress(path, &mut validation, &mut on_progress)?;
408 }
409 Err(error) => return Err(error.into()),
410 }
411 progress.bytes_processed += seal.size;
412 progress.segments_processed += 1;
413 checkpoint::notify(progress, &mut on_progress)?;
414 #[cfg(test)]
415 tests::crash_at("linked", index + 1);
416 }
417 sync_dir(crate::segment::segment_dir(&self.first))?;
418 #[cfg(test)]
419 tests::crash_at("published", 0);
420 drop(self.spool.take());
423 let cleanup = tempfile::Builder::new()
424 .prefix(".ewf-acquisition-cleanup-")
425 .tempdir_in(crate::segment::segment_dir(&self.first))?;
426 fs::rename(&self.state, cleanup.path().join("retired"))?;
427 sync_dir(crate::segment::segment_dir(&self.first))?;
428 #[cfg(test)]
429 tests::crash_at("retired", 0);
430 cleanup.close()?;
431 let (_, _, computed_sha256) = self.hashes.finalize();
432 Ok(WriteResult {
433 segment_paths: paths,
434 secondary_segment_paths: Vec::new(),
435 logical_size: self.source_size,
436 chunk_size: self.chunk_size as u64,
437 chunk_count: self.source_size.div_ceil(self.chunk_size as u64),
438 computed_sha256,
439 })
440 }
441
442 fn ensure_healthy(&self) -> Result<()> {
443 if self.failed {
444 return Err(EwfError::Unsupported(
445 "acquisition failed; drop and resume it".into(),
446 ));
447 }
448 Ok(())
449 }
450
451 fn append(&mut self, bytes: &[u8]) -> Result<usize> {
452 self.ensure_healthy()?;
453 if bytes.is_empty() {
454 return Ok(0);
455 }
456 if self.offset == self.source_size {
457 return Err(EwfError::Malformed(
458 "input exceeds declared acquisition size".into(),
459 ));
460 }
461 let take = bytes
462 .len()
463 .min(self.chunk_size - self.pending.len())
464 .min((self.source_size - self.offset).min(usize::MAX as u64) as usize);
465 self.pending.extend_from_slice(&bytes[..take]);
466 self.offset += take as u64;
467 if self.pending.len() == self.chunk_size || self.offset == self.source_size {
468 self.hashes.update(&self.pending);
469 let data = std::mem::replace(&mut self.pending, Vec::with_capacity(self.chunk_size));
470 let encoded = encode_chunk(
471 data,
472 self.options.compression,
473 self.options.compression_values,
474 self.chunk_size as u64,
475 false,
476 false,
477 )?;
478 self.chunks
479 .push(self.spool.as_mut().expect("active spool").append(encoded)?);
480 if self.chunks.len() == self.chunks_per_segment || self.offset == self.source_size {
481 self.seal(self.offset == self.source_size)?;
482 }
483 }
484 Ok(take)
485 }
486
487 fn seal(&mut self, final_segment: bool) -> Result<()> {
488 let index = self.sealed.len() + 1;
489 let target = staged_path(&self.first, &self.state, index)?;
490 let mut options = self.options.clone();
491 let sector_size = u64::from(options.bytes_per_sector);
492 let end_sector = (self.offset - self.pending.len() as u64) / sector_size;
493 options.acquisition_errors = self
494 .errors
495 .iter()
496 .filter_map(|range| {
497 let start = range.first_sector;
501 let end = (range.first_sector + range.sector_count).min(end_sector);
502 (start < end).then(|| AcquisitionError {
503 first_sector: start,
504 sector_count: end - start,
505 })
506 })
507 .collect();
508 let mut sections = Ewf1SegmentSections::for_segment(index == 1, final_segment);
509 sections.bits |= Ewf1SegmentSections::ERRORS;
510 if final_segment {
511 let (md5, sha1, sha256) = self.hashes.clone().finalize();
512 options.hashes = effective_write_hashes(&options.hashes, md5, sha1, sha256)?;
513 }
514 let mut segment = NamedTempFile::new_in(self.state.join("scratch"))?;
515 let spool = self.spool.as_mut().expect("active spool");
516 write_ewf1_segment(
517 segment.as_file_mut(),
518 spool,
519 &self.chunks,
520 &options,
521 Ewf1SegmentWriteContext {
522 segment_number: u16::try_from(index)
523 .map_err(|_| EwfError::Unsupported("too many acquisition segments".into()))?,
524 chunk_count: self.source_size.div_ceil(self.chunk_size as u64) as u32,
525 sector_count: self.source_size / u64::from(options.bytes_per_sector),
526 sections,
527 terminal_section: if final_segment {
528 TerminalSection::Done
529 } else {
530 TerminalSection::Next
531 },
532 },
533 )?;
534 segment.as_file().sync_all()?;
535 #[cfg(test)]
536 {
537 tests::crash_at("segment-synced", index);
538 if self.fail_seal {
539 return Err(std::io::Error::new(
540 std::io::ErrorKind::StorageFull,
541 "injected acquisition storage failure",
542 )
543 .into());
544 }
545 }
546 let size = segment.as_file().metadata()?.len();
547 let digest = file_digest(segment.path())?;
548 segment
549 .persist_noclobber(&target)
550 .map_err(|error| error.error)?;
551 sync_dir(&self.state)?;
552 #[cfg(test)]
553 tests::crash_at("segment-installed", index);
554 let seal = Seal {
555 end: self.offset - self.pending.len() as u64,
556 size,
557 digest,
558 };
559 atomic_file(
560 &self.state.join("scratch"),
561 &checkpoint_path(&self.state, index),
562 &seal.bytes(),
563 )?;
564 #[cfg(test)]
565 tests::crash_at("record-installed", index);
566 sync_dir(&self.state)?;
567 #[cfg(test)]
568 tests::crash_at("checkpoint-synced", index);
569 self.sealed.push(seal);
570 self.chunks.clear();
571 spool.file.as_file_mut().set_len(0)?;
572 spool.len = 0;
573 Ok(())
574 }
575}
576
577fn normalize_errors(
578 errors: &[AcquisitionError],
579 bytes: u64,
580 sector_size: u32,
581) -> Result<Vec<AcquisitionError>> {
582 let mut errors = errors.to_vec();
583 errors.sort_unstable_by_key(|range| range.first_sector);
584 let mut normalized: Vec<AcquisitionError> = Vec::new();
585 for range in errors {
586 let end = range
587 .first_sector
588 .checked_add(range.sector_count)
589 .filter(|end| *end <= bytes / u64::from(sector_size))
590 .ok_or_else(|| {
591 EwfError::Malformed("acquisition error range exceeds checkpoint".into())
592 })?;
593 if range.sector_count == 0 {
594 return Err(EwfError::Malformed("empty acquisition error range".into()));
595 }
596 if let Some(previous) = normalized.last_mut()
597 && range.first_sector <= previous.first_sector + previous.sector_count
598 {
599 previous.sector_count =
600 end.max(previous.first_sector + previous.sector_count) - previous.first_sector;
601 } else {
602 normalized.push(range);
603 }
604 }
605 Ok(normalized)
606}
607
608impl Write for AcquisitionWriter {
609 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
610 let result = self.append(bytes);
611 self.failed |= result.is_err();
612 result.map_err(std::io::Error::other)
613 }
614
615 fn flush(&mut self) -> std::io::Result<()> {
616 self.checkpoint().map(|_| ()).map_err(std::io::Error::other)
617 }
618}
619
620impl Seal {
621 fn bytes(&self) -> Vec<u8> {
622 let mut bytes = Vec::with_capacity(80);
623 bytes.extend_from_slice(&self.end.to_le_bytes());
624 bytes.extend_from_slice(&self.size.to_le_bytes());
625 bytes.extend_from_slice(&self.digest);
626 bytes.extend_from_slice(&Sha256::digest(&bytes));
627 bytes
628 }
629
630 fn read(path: &Path) -> Result<Self> {
631 let bytes = read_fixed::<80>(path)?;
632 if Sha256::digest(&bytes[..48])[..] != bytes[48..] {
633 return Err(EwfError::Malformed("damaged acquisition checkpoint".into()));
634 }
635 Ok(Self {
636 end: u64::from_le_bytes(bytes[..8].try_into().expect("fixed length")),
637 size: u64::from_le_bytes(bytes[8..16].try_into().expect("fixed length")),
638 digest: bytes[16..48].try_into().expect("fixed length"),
639 })
640 }
641
642 fn validate_with_progress(
643 &self,
644 path: &Path,
645 progress: &mut AcquisitionOperationProgress,
646 callback: &mut impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
647 ) -> Result<()> {
648 let metadata = fs::symlink_metadata(path)?;
649 require_file_type(&metadata)?;
650 if metadata.len() != self.size {
651 return Err(EwfError::Malformed(
652 "sealed acquisition segment changed".into(),
653 ));
654 }
655 let mut file = File::open(path)?;
656 let mut hash = Sha256::new();
657 let mut buffer = [0; 16 * 1024];
658 let mut remaining = self.size;
659 while remaining > 0 {
660 let take = remaining.min(buffer.len() as u64) as usize;
661 file.read_exact(&mut buffer[..take])?;
662 hash.update(&buffer[..take]);
663 remaining -= take as u64;
664 progress.bytes_processed += take as u64;
665 checkpoint::notify(*progress, callback)?;
666 }
667 if <[u8; 32]>::from(hash.finalize()) != self.digest {
668 return Err(EwfError::Malformed(
669 "sealed acquisition segment changed".into(),
670 ));
671 }
672 progress.segments_processed += 1;
673 checkpoint::notify(*progress, callback)
674 }
675}
676
677fn normalize_first(first: &Path) -> Result<PathBuf> {
678 if first.extension().and_then(|name| name.to_str()) != Some("E01") {
679 return Err(EwfError::Unsupported(
680 "acquisition requires an .E01 first segment".into(),
681 ));
682 }
683 Ok(crate::segment::segment_dir(first)
684 .canonicalize()?
685 .join(first.file_name().expect("E01 filename")))
686}
687
688fn validate_segment_budget(first: &Path, options: &AcquisitionOptions) -> Result<()> {
689 let chunk_size = u64::from(options.bytes_per_sector) * u64::from(options.sectors_per_chunk);
690 let segments = options
691 .source_size
692 .div_ceil(chunk_size)
693 .div_ceil(u64::from(options.chunks_per_segment));
694 segment_path(
695 first,
696 usize::try_from(segments)
697 .map_err(|_| EwfError::Unsupported("too many acquisition segments".into()))?,
698 )?;
699 Ok(())
700}
701
702fn fingerprint(
703 first: &Path,
704 acquisition: &AcquisitionOptions,
705 options: &WriteOptions,
706 source: [u8; 32],
707) -> Result<[u8; 32]> {
708 let mut hash = Sha256::new();
709 hash.update(b"ewf-image-acquisition-v1");
710 hash.update(source);
711 hash.update(acquisition.source_size.to_le_bytes());
712 hash.update(acquisition.bytes_per_sector.to_le_bytes());
713 hash.update(acquisition.sectors_per_chunk.to_le_bytes());
714 hash.update(acquisition.chunks_per_segment.to_le_bytes());
715 hash.update([u8::from(acquisition.compression == WriteCompression::Zlib)]);
716 let path = first.as_os_str().as_encoded_bytes();
717 hash.update((path.len() as u64).to_le_bytes());
718 hash.update(path);
719 for payload in [
720 header_payload(
721 &options.metadata,
722 options.header_codepage,
723 options.compression_values.level,
724 )?,
725 header2_payload(&options.metadata, options.compression_values.level)?,
726 xheader_payload(&options.metadata, options.compression_values.level)?,
727 ] {
728 let payload = payload.unwrap_or_default();
729 hash.update((payload.len() as u64).to_le_bytes());
730 hash.update(payload);
731 }
732 Ok(hash.finalize().into())
733}
734
735fn staged_path(first: &Path, state: &Path, index: usize) -> Result<PathBuf> {
736 Ok(state.join(
737 segment_path(first, index)?
738 .file_name()
739 .expect("segment filename"),
740 ))
741}
742
743fn checkpoint_path(state: &Path, index: usize) -> PathBuf {
744 state.join(format!("checkpoint-{index:05}"))
745}
746
747fn checkpoint_count(state: &Path) -> Result<usize> {
748 let mut indices = Vec::new();
749 for entry in fs::read_dir(state)? {
750 let entry = entry?;
751 let name = entry.file_name();
752 if let Some(index) = name
753 .to_str()
754 .and_then(|name| name.strip_prefix("checkpoint-"))
755 {
756 let index = index
757 .parse::<u16>()
758 .map_err(|_| EwfError::Malformed("invalid acquisition checkpoint name".into()))?;
759 if entry.path() != checkpoint_path(state, usize::from(index)) {
760 return Err(EwfError::Malformed(
761 "invalid acquisition checkpoint name".into(),
762 ));
763 }
764 indices.push(index);
765 }
766 }
767 indices.sort_unstable();
768 if indices
769 .iter()
770 .enumerate()
771 .any(|(index, value)| index + 1 != usize::from(*value))
772 {
773 return Err(EwfError::Malformed("missing acquisition checkpoint".into()));
774 }
775 Ok(indices.len())
776}
777
778fn atomic_file(scratch: &Path, target: &Path, bytes: &[u8]) -> Result<()> {
779 let mut file = NamedTempFile::new_in(scratch)?;
780 file.write_all(bytes)?;
781 file.as_file().sync_all()?;
782 file.persist_noclobber(target)
783 .map_err(|error| error.error)?;
784 Ok(())
785}
786
787fn read_fixed<const N: usize>(path: &Path) -> Result<[u8; N]> {
788 let metadata = fs::symlink_metadata(path)?;
789 require_file_type(&metadata)?;
790 if metadata.len() != N as u64 {
791 return Err(EwfError::Malformed(
792 "invalid acquisition record length".into(),
793 ));
794 }
795 let mut bytes = [0; N];
796 File::open(path)?.read_exact(&mut bytes)?;
797 Ok(bytes)
798}
799
800fn require_file_type(metadata: &fs::Metadata) -> Result<()> {
801 if !metadata.file_type().is_file() {
802 return Err(EwfError::Malformed(
803 "acquisition path is not a regular file".into(),
804 ));
805 }
806 Ok(())
807}
808
809fn require_directory(path: &Path) -> Result<()> {
810 if !fs::symlink_metadata(path)?.file_type().is_dir() {
811 return Err(EwfError::Malformed(
812 "acquisition path is not a directory".into(),
813 ));
814 }
815 Ok(())
816}
817
818fn file_digest(path: &Path) -> Result<[u8; 32]> {
819 let mut file = File::open(path)?;
820 let mut hash = Sha256::new();
821 let mut buffer = [0; 16 * 1024];
822 loop {
823 let size = file.read(&mut buffer)?;
824 if size == 0 {
825 break;
826 }
827 hash.update(&buffer[..size]);
828 }
829 Ok(hash.finalize().into())
830}
831
832fn ensure_output_absent(first: &Path) -> Result<()> {
833 for path in publication_segment_paths(&[first.to_path_buf()], false)? {
834 match fs::symlink_metadata(path) {
835 Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
836 Err(error) => return Err(error.into()),
837 Ok(_) => {
838 return Err(std::io::Error::new(
839 std::io::ErrorKind::AlreadyExists,
840 "acquisition output already exists",
841 )
842 .into());
843 }
844 }
845 }
846 Ok(())
847}