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