Skip to main content

ewf_image/
image.rs

1use std::collections::{HashMap, VecDeque};
2use std::fs::File;
3use std::io::{self, Read, Seek, SeekFrom};
4use std::num::NonZeroUsize;
5use std::path::{Path, PathBuf};
6use std::sync::{
7    Arc, Condvar, Mutex,
8    atomic::{AtomicBool, Ordering},
9};
10use std::time::Instant;
11
12use lru::LruCache;
13pub(crate) mod recovery;
14use md5::Digest as _;
15
16use crate::codepage::decode_header_bytes;
17use crate::decode::{
18    ChunkEncoding, decode_chunk_with_padding, raw_chunk_size_cap, validate_encoded_size,
19    zlib_compressed_chunk_size_cap,
20};
21use crate::encryption::EncryptionContext;
22use crate::format::{ewf1, ewf2};
23use crate::index::{LazyChunkIndex, TableRange, TableRangeKind};
24use crate::metadata::{
25    Ewf2Geometry, detect_ewf1_header_profile, detect_ewf1_header2_profile, parse_error2_data,
26    parse_ewf2_case_data, parse_ewf2_device_info, parse_ewf2_device_info_values,
27    parse_ewf2_error_table_data, parse_header_data, parse_header2_data, parse_session_data,
28    parse_xhash_data, parse_xheader_data,
29};
30use crate::reader_cache::{TABLE_PAGE_SIZE, TablePageCache, TablePageKey};
31use crate::reader_statistics::{ReaderCacheInfo, ReaderStatistics, ReaderStatisticsCollector};
32use crate::segment::discover_segments;
33use crate::signature::{check_segment_files_corruption, check_segment_files_encryption};
34use crate::single_files::{parse_ewf1_single_files_data, parse_ewf2_single_files_data};
35use crate::types::{
36    AcquisitionError, ChunkCacheCapacity, CompressionLevel, CompressionMethod, CompressionValues,
37    DataChunk, DataChunkEncoding, EncodedDataChunk, EwfMetadata, Format, FormatProfile,
38    HeaderCodepage, HeaderDateFormat, ImageInfo, MediaFlags, MediaInfo, MediaType, MemoryExtent,
39    OpenOptions, OpenStrictness, SectorRange, SegmentFileVersion, SingleFileEntry,
40    SingleFilePermission, SingleFileSource, SingleFileSubject, SingleFilesAuxTables,
41    SingleFilesInfo, StoredHashes,
42};
43use crate::{
44    EncryptionInfo, EwfError, EwfPassword, Result, SectionInfo, SectionKind, SegmentSource,
45};
46
47const MAX_DECOMPRESSED_METADATA: u64 = 16 * 1024 * 1024;
48const MAX_CHUNK_SIZE: u64 = 128 * 1024 * 1024;
49const EWF1_HASH_SECTION_SIZE: u64 = 36;
50const EWF1_DIGEST_SECTION_SIZE: u64 = 80;
51const EWF1_LTREE_HEADER_SIZE: usize = 48;
52const EWF1_LOGICAL_SPARSE_FLAG: u32 = 0x0400_0000;
53const EWF2_HASH_SECTION_SIZE: u64 = 32;
54const EWF2_TABLE_HEADER_V2_SIZE: u64 = 32;
55const EWF2_TABLE_FOOTER_SIZE: u64 = 16;
56const TABLE_CHECKSUM_BUFFER_SIZE: usize = 64 * 1024;
57
58/// Reader type accepted by [`Image::open_readers`].
59///
60/// Implemented automatically for any `Read + Seek + Send` type. Override
61/// [`SegmentReader::segment_len`] when obtaining the length by seeking to the
62/// end is not appropriate.
63pub trait SegmentReader: Read + Seek + Send {
64    /// Returns the total length of this segment in bytes.
65    fn segment_len(&mut self) -> io::Result<u64> {
66        let position = self.stream_position()?;
67        let len = self.seek(SeekFrom::End(0))?;
68        self.seek(SeekFrom::Start(position))?;
69        Ok(len)
70    }
71}
72
73impl<T> SegmentReader for T where T: Read + Seek + Send {}
74
75type SuppliedSegmentReader = Box<dyn SegmentReader>;
76
77#[derive(Debug, Clone)]
78/// Opened EWF image and logical media reader.
79pub struct Image {
80    inner: Arc<ImageInner>,
81}
82
83#[derive(Debug)]
84struct ImageInner {
85    info: ImageInfo,
86    sections: Vec<SectionInfo>,
87    open_strictness: OpenStrictness,
88    encryption_info: Option<EncryptionInfo>,
89    encryption_contexts: Vec<Option<EncryptionContext>>,
90    segments: Mutex<SegmentFilePool>,
91    segment_handle_available: Condvar,
92    positioned_sources: Option<Vec<SegmentSource>>,
93    index: LazyChunkIndex,
94    chunk_cache: Mutex<LruCache<u64, Arc<Vec<u8>>>>,
95    chunk_in_flight: Mutex<HashMap<u64, Arc<ChunkFlight>>>,
96    chunk_cache_capacity_bytes: u64,
97    table_page_cache: Mutex<TablePageCache>,
98    statistics: Arc<ReaderStatisticsCollector>,
99    checksum_errors: Mutex<Vec<SectorRange>>,
100    read_zero_chunk_on_error: AtomicBool,
101    abort_signaled: AtomicBool,
102}
103
104struct SegmentFilePool {
105    files: Vec<Option<SegmentFileHandle>>,
106    lengths: Vec<Option<u64>>,
107    ever_opened: Vec<bool>,
108    open_order: VecDeque<usize>,
109    maximum_open_handles: Option<usize>,
110    mode: SegmentFilePoolMode,
111    statistics: Arc<ReaderStatisticsCollector>,
112}
113
114enum SegmentFileHandle {
115    Path(PathSegmentReader),
116    Supplied(SuppliedSegmentReader),
117}
118
119struct PathSegmentReader {
120    file: Arc<File>,
121    position: u64,
122}
123
124struct PositionedSegmentLease<'a> {
125    file: Option<Arc<File>>,
126    segments: &'a Mutex<SegmentFilePool>,
127    available: &'a Condvar,
128}
129
130#[derive(Debug)]
131struct ChunkFlight {
132    complete: Mutex<bool>,
133    ready: Condvar,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137enum SegmentFilePoolMode {
138    ReopenFromPath,
139    SuppliedReaders,
140}
141
142impl std::fmt::Debug for SegmentFilePool {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("SegmentFilePool")
145            .field("segment_count", &self.files.len())
146            .field(
147                "known_length_count",
148                &self
149                    .lengths
150                    .iter()
151                    .filter(|length| length.is_some())
152                    .count(),
153            )
154            .field(
155                "ever_opened_count",
156                &self.ever_opened.iter().filter(|opened| **opened).count(),
157            )
158            .field("open_count", &self.open_count())
159            .field("open_order", &self.open_order)
160            .field("maximum_open_handles", &self.maximum_open_handles)
161            .field("mode", &self.mode)
162            .field("statistics_enabled", &self.statistics.enabled())
163            .finish()
164    }
165}
166
167impl SegmentFileHandle {
168    fn as_reader_mut(&mut self) -> &mut dyn SegmentReader {
169        match self {
170            Self::Path(reader) => reader,
171            Self::Supplied(reader) => reader.as_mut(),
172        }
173    }
174
175    fn path_file(&self) -> Option<&Arc<File>> {
176        match self {
177            Self::Path(reader) => Some(&reader.file),
178            Self::Supplied(_) => None,
179        }
180    }
181
182    fn idle_path_handle(&self) -> bool {
183        self.path_file()
184            .is_some_and(|file| Arc::strong_count(file) == 1)
185    }
186}
187
188impl Read for PathSegmentReader {
189    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
190        let count = read_file_at(&self.file, buffer, self.position)?;
191        self.position = self.position.saturating_add(count as u64);
192        Ok(count)
193    }
194}
195
196impl Seek for PathSegmentReader {
197    fn seek(&mut self, seek: SeekFrom) -> io::Result<u64> {
198        let position = match seek {
199            SeekFrom::Start(offset) => Some(offset),
200            SeekFrom::Current(offset) => self.position.checked_add_signed(offset),
201            SeekFrom::End(offset) => self.file.metadata()?.len().checked_add_signed(offset),
202        }
203        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "segment seek out of range"))?;
204        self.position = position;
205        Ok(position)
206    }
207}
208
209impl Drop for PositionedSegmentLease<'_> {
210    fn drop(&mut self) {
211        // Release the active lease before waking a waiter. Otherwise the waiter
212        // can observe this handle as busy and go back to sleep after the only
213        // notification associated with the release.
214        self.file.take();
215        // Synchronize the predicate change with the mutex used by waiters. A
216        // waiter cannot slip between checking the pool and going to sleep.
217        let _segments = self
218            .segments
219            .lock()
220            .unwrap_or_else(std::sync::PoisonError::into_inner);
221        self.available.notify_one();
222    }
223}
224
225impl PositionedSegmentLease<'_> {
226    fn file(&self) -> &File {
227        self.file
228            .as_deref()
229            .expect("positioned segment lease retains its file until drop")
230    }
231}
232
233#[derive(Debug, Clone, Copy)]
234struct Chunk {
235    segment_index: usize,
236    offset: u64,
237    encoded_size: u64,
238    logical_size: usize,
239    encoding: ChunkEncoding,
240    validate_checksum: bool,
241}
242
243#[derive(Clone, Copy)]
244struct Ewf1DecodedEntry {
245    compressed: bool,
246    offset: u64,
247}
248
249#[derive(Debug, Clone)]
250/// Seekable cursor over an [`Image`] logical media stream.
251pub struct ImageCursor {
252    image: Image,
253    position: u64,
254}
255
256#[derive(Debug, Clone)]
257/// Seekable cursor over one logical single-file catalog entry.
258pub struct SingleFileCursor {
259    image: Image,
260    entry: SingleFileEntry,
261    position: u64,
262}
263
264impl Image {
265    /// Opens an EWF image from the first segment path.
266    ///
267    /// Adjacent segments are discovered automatically from the first path.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if no segment set can be discovered, a segment cannot
272    /// be read, or the image is unsupported or malformed.
273    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
274        Self::open_with_options(path, OpenOptions::default())
275    }
276
277    /// Opens an EWF image from the first segment path with explicit options.
278    ///
279    /// # Errors
280    ///
281    /// Returns an error if no segment set can be discovered, a segment cannot
282    /// be read, or the image is unsupported or malformed.
283    pub fn open_with_options(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
284        let paths = discover_segments(path.as_ref())?;
285        Self::open_segment_paths(paths, options, None)
286    }
287
288    /// Opens an EWF image using caller-supplied password bytes.
289    ///
290    /// Supplying a password for an unencrypted image is harmless.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if no segment set can be discovered, a segment cannot
295    /// be read, the password is rejected, or the image is unsupported or
296    /// malformed.
297    pub fn open_with_password(path: impl AsRef<Path>, password: &EwfPassword) -> Result<Self> {
298        Self::open_with_options_and_password(path, OpenOptions::default(), password)
299    }
300
301    /// Opens an EWF image using explicit options and caller-supplied password
302    /// bytes.
303    ///
304    /// Supplying a password for an unencrypted image is harmless.
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if no segment set can be discovered, a segment cannot
309    /// be read, the password is rejected, or the image is unsupported or
310    /// malformed.
311    pub fn open_with_options_and_password(
312        path: impl AsRef<Path>,
313        options: OpenOptions,
314        password: &EwfPassword,
315    ) -> Result<Self> {
316        let paths = discover_segments(path.as_ref())?;
317        Self::open_segment_paths(paths, options, Some(password))
318    }
319
320    /// Opens an EWF image from an explicit ordered segment path list.
321    ///
322    /// # Errors
323    ///
324    /// Returns an error if the list is empty, a segment cannot be read, or the
325    /// image is unsupported or malformed.
326    pub fn open_segments<P, I>(paths: I) -> Result<Self>
327    where
328        P: AsRef<Path>,
329        I: IntoIterator<Item = P>,
330    {
331        Self::open_segments_with_options(paths, OpenOptions::default())
332    }
333
334    /// Opens an EWF image from explicit ordered segment paths with options.
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if the list is empty, a segment cannot be read, or the
339    /// image is unsupported or malformed.
340    pub fn open_segments_with_options<P, I>(paths: I, options: OpenOptions) -> Result<Self>
341    where
342        P: AsRef<Path>,
343        I: IntoIterator<Item = P>,
344    {
345        let paths = paths
346            .into_iter()
347            .map(|path| path.as_ref().to_path_buf())
348            .collect();
349        Self::open_segment_paths(paths, options, None)
350    }
351
352    /// Opens an explicit ordered segment path list using explicit options and
353    /// caller-supplied password bytes.
354    ///
355    /// Supplying a password for an unencrypted image is harmless.
356    ///
357    /// # Errors
358    ///
359    /// Returns an error if the list is empty, a segment cannot be read, the
360    /// password is rejected, or the image is unsupported or malformed.
361    pub fn open_segments_with_options_and_password<P, I>(
362        paths: I,
363        options: OpenOptions,
364        password: &EwfPassword,
365    ) -> Result<Self>
366    where
367        P: AsRef<Path>,
368        I: IntoIterator<Item = P>,
369    {
370        let paths = paths
371            .into_iter()
372            .map(|path| path.as_ref().to_path_buf())
373            .collect();
374        Self::open_segment_paths(paths, options, Some(password))
375    }
376
377    /// Opens an EWF image from supplied readers and segment labels.
378    ///
379    /// Supplied readers are kept by the image and are not reopened from the
380    /// filesystem. Labels are used anywhere this crate reports segment paths.
381    ///
382    /// # Errors
383    ///
384    /// Returns an error if the list is empty, a reader fails, or the image is
385    /// unsupported or malformed.
386    pub fn open_readers<N, R, I>(segments: I) -> Result<Self>
387    where
388        N: Into<PathBuf>,
389        R: SegmentReader + 'static,
390        I: IntoIterator<Item = (N, R)>,
391    {
392        Self::open_readers_with_options(segments, OpenOptions::default())
393    }
394
395    /// Opens an EWF image from supplied readers with explicit options.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if the list is empty, a reader fails, or the image is
400    /// unsupported or malformed.
401    pub fn open_readers_with_options<N, R, I>(segments: I, options: OpenOptions) -> Result<Self>
402    where
403        N: Into<PathBuf>,
404        R: SegmentReader + 'static,
405        I: IntoIterator<Item = (N, R)>,
406    {
407        let mut paths = Vec::new();
408        let mut readers = Vec::new();
409        for (name, reader) in segments {
410            paths.push(name.into());
411            readers.push(Box::new(reader) as SuppliedSegmentReader);
412        }
413        Self::open_segment_readers(paths, readers, options, None)
414    }
415
416    /// Opens supplied readers using explicit options and caller-supplied
417    /// password bytes.
418    ///
419    /// Supplying a password for an unencrypted image is harmless. Supplied
420    /// readers are retained by the image and are never reopened by path.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the list is empty, a reader fails, the password is
425    /// rejected, or the image is unsupported or malformed.
426    pub fn open_readers_with_options_and_password<N, R, I>(
427        segments: I,
428        options: OpenOptions,
429        password: &EwfPassword,
430    ) -> Result<Self>
431    where
432        N: Into<PathBuf>,
433        R: SegmentReader + 'static,
434        I: IntoIterator<Item = (N, R)>,
435    {
436        let mut paths = Vec::new();
437        let mut readers = Vec::new();
438        for (name, reader) in segments {
439            paths.push(name.into());
440            readers.push(Box::new(reader) as SuppliedSegmentReader);
441        }
442        Self::open_segment_readers(paths, readers, options, Some(password))
443    }
444
445    fn open_segment_paths(
446        paths: Vec<PathBuf>,
447        options: OpenOptions,
448        password: Option<&EwfPassword>,
449    ) -> Result<Self> {
450        if paths.is_empty() {
451            return Err(EwfError::NoSegments("empty segment list".into()));
452        }
453
454        crate::publication::ensure_no_pending(&paths[0])?;
455
456        let statistics = Arc::new(ReaderStatisticsCollector::new(
457            options.reader_statistics_enabled(),
458        ));
459        let segments =
460            SegmentFilePool::new_path(paths.len(), options.maximum_open_handles(), statistics)?;
461        Self::open_segment_sources(paths, segments, options, password, None, None)
462    }
463
464    // Only the acquisition checkpoint reader may open a sealed prefix whose
465    // volume declares the eventual full size. Public opens keep exact coverage.
466    pub(crate) fn open_acquisition_prefix(
467        paths: Vec<PathBuf>,
468        expected_size: u64,
469        committed_size: u64,
470    ) -> Result<Self> {
471        let options = OpenOptions::default().with_chunk_cache_size(1);
472        let statistics = Arc::new(ReaderStatisticsCollector::new(false));
473        let segments =
474            SegmentFilePool::new_path(paths.len(), options.maximum_open_handles(), statistics)?;
475        Self::open_segment_sources(
476            paths,
477            segments,
478            options,
479            None,
480            None,
481            Some((expected_size, committed_size)),
482        )
483    }
484
485    fn open_segment_readers(
486        paths: Vec<PathBuf>,
487        readers: Vec<SuppliedSegmentReader>,
488        options: OpenOptions,
489        password: Option<&EwfPassword>,
490    ) -> Result<Self> {
491        if paths.is_empty() {
492            return Err(EwfError::NoSegments("empty segment list".into()));
493        }
494        if paths.len() != readers.len() {
495            return Err(EwfError::Malformed(
496                "segment reader count does not match segment labels".into(),
497            ));
498        }
499
500        let statistics = Arc::new(ReaderStatisticsCollector::new(
501            options.reader_statistics_enabled(),
502        ));
503        let segments =
504            SegmentFilePool::new_readers(readers, options.maximum_open_handles(), statistics)?;
505        Self::open_segment_sources(paths, segments, options, password, None, None)
506    }
507
508    /// Opens explicitly ordered, named positioned sources with default options.
509    /// Supports EWF1 and EWF2. Names are labels, never reopened as filesystem paths.
510    pub fn open_sources<N: Into<PathBuf>>(
511        sources: impl IntoIterator<Item = (N, SegmentSource)>,
512    ) -> Result<Self> {
513        Self::open_sources_with_options(sources, OpenOptions::default())
514    }
515
516    /// Opens positioned sources with reader controls. All supplied backings stay
517    /// owned for the image lifetime; an open-handle limit below their count is rejected.
518    pub fn open_sources_with_options<N: Into<PathBuf>>(
519        sources: impl IntoIterator<Item = (N, SegmentSource)>,
520        options: OpenOptions,
521    ) -> Result<Self> {
522        Self::open_positioned_sources(sources, options, None)
523    }
524
525    /// Opens encrypted positioned sources using one caller-supplied password.
526    pub fn open_sources_with_options_and_password<N: Into<PathBuf>>(
527        sources: impl IntoIterator<Item = (N, SegmentSource)>,
528        options: OpenOptions,
529        password: &EwfPassword,
530    ) -> Result<Self> {
531        Self::open_positioned_sources(sources, options, Some(password))
532    }
533
534    fn open_positioned_sources<N: Into<PathBuf>>(
535        sources: impl IntoIterator<Item = (N, SegmentSource)>,
536        options: OpenOptions,
537        password: Option<&EwfPassword>,
538    ) -> Result<Self> {
539        let (paths, sources): (Vec<_>, Vec<_>) = sources
540            .into_iter()
541            .map(|(name, source)| (name.into(), source))
542            .unzip();
543        if paths.is_empty() {
544            return Err(EwfError::NoSegments("empty segment list".into()));
545        }
546        let statistics = Arc::new(ReaderStatisticsCollector::new(
547            options.reader_statistics_enabled(),
548        ));
549        let readers = sources
550            .iter()
551            .map(|source| Box::new(source.cursor()) as SuppliedSegmentReader)
552            .collect();
553        let segments =
554            SegmentFilePool::new_readers(readers, options.maximum_open_handles(), statistics)?;
555        Self::open_segment_sources(paths, segments, options, password, Some(sources), None)
556    }
557
558    fn open_segment_sources(
559        paths: Vec<PathBuf>,
560        mut segments: SegmentFilePool,
561        options: OpenOptions,
562        password: Option<&EwfPassword>,
563        positioned_sources: Option<Vec<SegmentSource>>,
564        acquisition_prefix: Option<(u64, u64)>,
565    ) -> Result<Self> {
566        let statistics = Arc::clone(&segments.statistics);
567        let mut ranges = Vec::new();
568        let mut section_summaries = Vec::new();
569        let mut metadata = EwfMetadata::default();
570        let mut acquisition_errors = Vec::new();
571        let mut memory_extents = Vec::new();
572        let mut single_files = None;
573        let mut ewf2_single_files_tables = SingleFilesAuxTables::default();
574        let mut ewf2_increment_data = Vec::new();
575        let mut ewf2_final_information = None;
576        let mut ewf2_restart_data = None;
577        let mut ewf2_analytical_data = None;
578        let mut sessions = Vec::new();
579        let mut tracks = Vec::new();
580        let mut stored_hashes = StoredHashes::default();
581        let mut media = MediaInfo::default();
582        let mut chunk_size = 0;
583        let mut logical_size = 0;
584        let mut acquisition_complete = true;
585        let mut format = None;
586        let mut format_profile = None;
587        let mut format_profile_hint_only = false;
588        let mut next_ewf1_chunk = 0_u64;
589        let mut discovered_table_chunks = 0_u64;
590        let mut expected_set_identifier: Option<[u8; 16]> = None;
591        let mut expected_ewf1_media = None;
592        let mut expected_ewf1_compression_method = None;
593        let mut expected_ewf2_header_profile = None;
594        let mut expected_ewf2_device_information = None;
595        let mut expected_ewf2_case_data = None;
596        let mut encryption_info = None;
597        let mut encryption_contexts = Vec::with_capacity(paths.len());
598
599        for (segment_index, path) in paths.iter().enumerate() {
600            statistics.record_segment_parse();
601            let (parsed, encryption_context, observed_encryption_info) = {
602                let file = segments.file_mut(segment_index, path)?;
603                let encryption_metadata = ewf1::read_xways_encryption(file.as_reader_mut())?;
604                let observed_encryption_info =
605                    encryption_metadata.as_ref().map(EncryptionInfo::from_xways);
606                let encryption_context = encryption_metadata
607                    .as_ref()
608                    .map(|metadata| {
609                        let password = password.ok_or(EwfError::PasswordRequired)?;
610                        EncryptionContext::derive(metadata, password)
611                    })
612                    .transpose()?;
613                let parsed = parse_segment(
614                    file.as_reader_mut(),
615                    &paths[0],
616                    segment_index,
617                    Ewf1SegmentContext {
618                        first_chunk: next_ewf1_chunk,
619                        inherited_media_geometry: expected_ewf1_media,
620                    },
621                    options.strictness(),
622                    options.header_codepage(),
623                    &statistics,
624                )?;
625                (parsed, encryption_context, observed_encryption_info)
626            };
627            if segment_index == 0 {
628                encryption_info = observed_encryption_info;
629            } else if observed_encryption_info != encryption_info {
630                return Err(EwfError::Malformed(
631                    "X-Ways EWF1 encryption settings differ across segments".into(),
632                ));
633            }
634            encryption_contexts.push(encryption_context);
635            section_summaries.extend(parsed.sections);
636            let expected_segment_number = u64::try_from(segment_index + 1)
637                .map_err(|_| EwfError::Malformed("segment index overflow".into()))?;
638            if parsed.segment_number != expected_segment_number {
639                return Err(EwfError::Malformed(format!(
640                    "segment {} declares segment number {}, expected {}",
641                    segment_index + 1,
642                    parsed.segment_number,
643                    expected_segment_number
644                )));
645            }
646            validate_set_identifier(&mut expected_set_identifier, parsed.set_identifier)?;
647            if let Some(observed) = parsed.ewf1_compression_method {
648                if expected_ewf1_compression_method.is_some_and(|expected| expected != observed) {
649                    return Err(EwfError::Malformed(
650                        "EWF1 compression method mismatch across segments".into(),
651                    ));
652                }
653                expected_ewf1_compression_method.get_or_insert(observed);
654            }
655            if let Some(observed) = parsed.ewf1_media_geometry {
656                if expected_ewf1_media
657                    .as_ref()
658                    .is_some_and(|expected| expected != &observed)
659                {
660                    return Err(EwfError::Malformed(
661                        "EWF1 media geometry mismatch across segments".into(),
662                    ));
663                }
664                expected_ewf1_media.get_or_insert(observed);
665            }
666            validate_ewf2_header_profile(
667                &mut expected_ewf2_header_profile,
668                parsed.ewf2_header_profile,
669            )?;
670            if let Some(device_information) = parsed.ewf2_device_information.as_deref() {
671                remember_ewf2_metadata_payload(
672                    &mut expected_ewf2_device_information,
673                    device_information,
674                    "device information",
675                )?;
676            }
677            if let Some(case_data) = parsed.ewf2_case_data.as_deref() {
678                remember_ewf2_metadata_payload(
679                    &mut expected_ewf2_case_data,
680                    case_data,
681                    "case data",
682                )?;
683            }
684            if segment_index == 0 {
685                chunk_size = parsed.chunk_size;
686                logical_size = parsed.logical_size;
687                acquisition_complete = parsed.acquisition_complete;
688                media = parsed.media;
689                metadata = parsed.metadata;
690                acquisition_errors = parsed.acquisition_errors;
691                memory_extents = parsed.memory_extents;
692                single_files = parsed.single_files;
693                ewf2_single_files_tables = parsed.ewf2_single_files_tables;
694                ewf2_increment_data = parsed.ewf2_increment_data;
695                ewf2_final_information = parsed.ewf2_final_information;
696                ewf2_restart_data = parsed.ewf2_restart_data;
697                ewf2_analytical_data = parsed.ewf2_analytical_data;
698                sessions = parsed.sessions;
699                tracks = parsed.tracks;
700                format = Some(parsed.format);
701                format_profile = Some(parsed.format_profile);
702                format_profile_hint_only = parsed.format_profile_hint_only;
703            } else {
704                merge_segment_format_profile(
705                    &mut format_profile,
706                    &mut format_profile_hint_only,
707                    parsed.format_profile,
708                    parsed.format_profile_hint_only,
709                )?;
710                merge_acquisition_errors(&mut acquisition_errors, parsed.acquisition_errors)?;
711                memory_extents.extend(parsed.memory_extents);
712                merge_single_files(&mut single_files, parsed.single_files)?;
713                merge_single_files_aux_tables(
714                    &mut ewf2_single_files_tables,
715                    parsed.ewf2_single_files_tables,
716                )?;
717                ewf2_increment_data.extend(parsed.ewf2_increment_data);
718                merge_optional_ewf2_raw_section(
719                    &mut ewf2_final_information,
720                    parsed.ewf2_final_information,
721                    "final information",
722                )?;
723                merge_optional_ewf2_string_section(
724                    &mut ewf2_restart_data,
725                    parsed.ewf2_restart_data,
726                    "restart data",
727                )?;
728                merge_optional_ewf2_string_section(
729                    &mut ewf2_analytical_data,
730                    parsed.ewf2_analytical_data,
731                    "analytical data",
732                )?;
733                sessions.extend(parsed.sessions);
734                tracks.extend(parsed.tracks);
735                acquisition_complete = parsed.acquisition_complete;
736            }
737            merge_hashes(&mut stored_hashes, &parsed.stored_hashes)?;
738            if parsed.format == Format::Ewf1 {
739                next_ewf1_chunk = next_ewf1_chunk
740                    .checked_add(parsed.table_chunk_count)
741                    .ok_or_else(|| EwfError::Malformed("EWF1 chunk count overflow".into()))?;
742            }
743            for range in &parsed.ranges {
744                discovered_table_chunks = discovered_table_chunks.max(
745                    range
746                        .first_chunk
747                        .checked_add(range.chunk_count)
748                        .ok_or_else(|| {
749                            EwfError::Malformed("table range chunk count overflow".into())
750                        })?,
751                );
752            }
753            ranges.extend(parsed.ranges);
754        }
755
756        let format = format.ok_or_else(|| EwfError::Malformed("image has no segments".into()))?;
757        if format == Format::Ewf1
758            && matches!(
759                format_profile,
760                Some(
761                    FormatProfile::LogicalEnCase5
762                        | FormatProfile::LogicalEnCase6
763                        | FormatProfile::LogicalEnCase7
764                )
765            )
766            && let Some(catalog) = single_files.as_ref()
767        {
768            logical_size = catalog.data_size;
769        }
770        if format == Format::Ewf2
771            && expected_ewf2_device_information.is_none()
772            && expected_ewf2_case_data.is_none()
773        {
774            return Err(EwfError::Malformed(
775                "missing EWF2 device information or case data section".into(),
776            ));
777        }
778
779        if logical_size == 0 && chunk_size > 0 {
780            logical_size = chunk_size
781                .checked_mul(discovered_table_chunks)
782                .ok_or_else(|| EwfError::Malformed("logical size overflow".into()))?;
783        }
784        if discovered_table_chunks > 0 {
785            media.chunk_count = Some(discovered_table_chunks);
786        }
787        if let Some((expected_size, committed_size)) = acquisition_prefix {
788            if format != Format::Ewf1
789                || logical_size != expected_size
790                || committed_size == 0
791                || committed_size > expected_size
792            {
793                return Err(EwfError::Malformed(
794                    "invalid acquisition prefix geometry".into(),
795                ));
796            }
797            logical_size = committed_size;
798        }
799
800        let info = ImageInfo {
801            format,
802            format_profile: format_profile.unwrap_or_default(),
803            segment_count: paths.len(),
804            segment_paths: paths,
805            chunk_size,
806            logical_size,
807            acquisition_complete,
808            header_codepage: options.header_codepage(),
809            header_values_date_format: options.header_values_date_format(),
810            media,
811            metadata,
812            stored_hashes,
813            acquisition_errors,
814            memory_extents,
815            single_files,
816            ewf2_single_files_tables,
817            ewf2_increment_data,
818            ewf2_final_information,
819            ewf2_restart_data,
820            ewf2_analytical_data,
821            sessions,
822            tracks,
823        };
824        let index = LazyChunkIndex::new(ranges, info.logical_size, info.chunk_size)?;
825        let chunk_size = usize::try_from(info.chunk_size)
826            .map_err(|_| EwfError::Malformed("chunk size does not fit usize".into()))?;
827        let cache_entries = match options.chunk_cache_capacity() {
828            ChunkCacheCapacity::Chunks(entries) => entries.max(1),
829            ChunkCacheCapacity::Bytes(bytes) => bytes.checked_div(chunk_size).unwrap_or(0).max(1),
830        };
831        let cache_size =
832            NonZeroUsize::new(cache_entries).expect("chunk cache size is at least one");
833        let chunk_cache_capacity_bytes = u64::try_from(cache_entries)
834            .unwrap_or(u64::MAX)
835            .saturating_mul(info.chunk_size);
836
837        let image = Self {
838            inner: Arc::new(ImageInner {
839                info,
840                sections: section_summaries,
841                open_strictness: options.strictness(),
842                encryption_info,
843                encryption_contexts,
844                segments: Mutex::new(segments),
845                segment_handle_available: Condvar::new(),
846                positioned_sources,
847                index,
848                chunk_cache: Mutex::new(LruCache::new(cache_size)),
849                chunk_in_flight: Mutex::new(HashMap::new()),
850                chunk_cache_capacity_bytes,
851                table_page_cache: Mutex::new(TablePageCache::new(
852                    options.table_entry_cache_size_bytes(),
853                )),
854                statistics,
855                checksum_errors: Mutex::new(Vec::new()),
856                read_zero_chunk_on_error: AtomicBool::new(options.read_zero_chunk_on_error()),
857                abort_signaled: AtomicBool::new(false),
858            }),
859        };
860        if image
861            .encryption_info()
862            .is_some_and(|info| !info.password_verifier_present())
863            && image.info().logical_size > 0
864        {
865            image.read_data_chunk(0)?;
866        }
867        Ok(image)
868    }
869
870    /// Returns parsed image metadata and geometry.
871    pub fn info(&self) -> &ImageInfo {
872        &self.inner.info
873    }
874
875    /// Returns descriptor summaries in segment order and parser traversal order.
876    /// Summary offsets are segment-relative, not logical media offsets.
877    pub fn sections(&self) -> &[SectionInfo] {
878        &self.inner.sections
879    }
880
881    /// Returns the structural policy used when opening this image.
882    pub fn open_strictness(&self) -> OpenStrictness {
883        self.inner.open_strictness
884    }
885
886    /// Returns non-secret encryption status for the opened image.
887    #[must_use]
888    pub fn encryption_info(&self) -> Option<EncryptionInfo> {
889        self.inner.encryption_info
890    }
891
892    /// Returns cumulative reader counters when statistics were enabled at open time.
893    ///
894    /// Snapshots are shared across clones and cursors created from this image.
895    pub fn reader_statistics(&self) -> Option<ReaderStatistics> {
896        self.inner.statistics.snapshot()
897    }
898
899    /// Returns configured capacities and retained table-page payload bytes.
900    pub fn reader_cache_info(&self) -> ReaderCacheInfo {
901        let cache = self
902            .inner
903            .table_page_cache
904            .lock()
905            .unwrap_or_else(std::sync::PoisonError::into_inner);
906        ReaderCacheInfo::new(
907            self.inner.chunk_cache_capacity_bytes,
908            cache.capacity_bytes(),
909            cache.cached_bytes(),
910            cache.peak_bytes(),
911        )
912    }
913
914    /// Returns the first segment filename or supplied-reader label.
915    pub fn filename(&self) -> &Path {
916        self.inner.info.segment_paths[0].as_path()
917    }
918
919    /// Returns the number of segments in the opened image.
920    pub fn number_of_segments(&self) -> usize {
921        self.inner.info.segment_count
922    }
923
924    /// Returns the configured maximum number of open segment handles.
925    ///
926    /// # Errors
927    ///
928    /// Returns an error if the internal segment pool lock is poisoned.
929    pub fn maximum_number_of_open_handles(&self) -> Result<Option<usize>> {
930        Ok(self
931            .inner
932            .segments
933            .lock()
934            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?
935            .maximum_open_handles())
936    }
937
938    /// Updates the maximum number of open segment handles.
939    ///
940    /// `None` allows all handles to remain open. Supplied-reader images cannot
941    /// evict and reopen readers, so this value is validated against the reader
942    /// set.
943    ///
944    /// # Errors
945    ///
946    /// Returns an error if the value is invalid for supplied readers or if the
947    /// internal segment pool lock is poisoned.
948    pub fn set_maximum_number_of_open_handles(
949        &self,
950        maximum_open_handles: Option<usize>,
951    ) -> Result<()> {
952        self.inner
953            .segments
954            .lock()
955            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?
956            .set_maximum_open_handles(maximum_open_handles)
957    }
958
959    /// Returns the current number of open segment handles.
960    ///
961    /// # Errors
962    ///
963    /// Returns an error if the internal segment pool lock is poisoned.
964    pub fn number_of_open_segment_handles(&self) -> Result<usize> {
965        Ok(self
966            .inner
967            .segments
968            .lock()
969            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?
970            .open_count())
971    }
972
973    /// Returns all segment filenames or supplied-reader labels.
974    pub fn segment_filenames(&self) -> &[PathBuf] {
975        &self.inner.info.segment_paths
976    }
977
978    /// Returns one segment filename or supplied-reader label by index.
979    pub fn segment_filename(&self, index: usize) -> Option<&Path> {
980        self.inner
981            .info
982            .segment_paths
983            .get(index)
984            .map(PathBuf::as_path)
985    }
986
987    /// Returns the total byte size of all opened segment containers.
988    ///
989    /// For supplied readers, this uses [`SegmentReader::segment_len`].
990    ///
991    /// # Errors
992    ///
993    /// Returns an error if a segment length cannot be read, the internal segment
994    /// pool lock is poisoned, or the total size overflows `u64`.
995    pub fn segment_set_size(&self) -> Result<u64> {
996        let paths = self.inner.info.segment_paths.clone();
997        let mut segments = self
998            .inner
999            .segments
1000            .lock()
1001            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?;
1002        let mut size = 0_u64;
1003        for (segment_index, path) in paths.iter().enumerate() {
1004            size = size
1005                .checked_add(
1006                    segments
1007                        .file_mut(segment_index, path)?
1008                        .as_reader_mut()
1009                        .segment_len()?,
1010                )
1011                .ok_or_else(|| EwfError::Malformed("segment set size overflow".into()))?;
1012        }
1013        Ok(size)
1014    }
1015
1016    /// Probes path-backed segment files for corruption-style structural errors.
1017    ///
1018    /// Images opened from supplied readers return `false` because there are no
1019    /// filesystem paths to reprobe.
1020    ///
1021    /// # Errors
1022    ///
1023    /// Returns an error if a path-backed probe fails unexpectedly.
1024    pub fn segment_files_corrupted(&self) -> Result<bool> {
1025        if self.has_supplied_segment_readers()? {
1026            return Ok(false);
1027        }
1028        check_segment_files_corruption(self.segment_filenames())
1029    }
1030
1031    /// Probes path-backed segment files for EWF2 encryption markers.
1032    ///
1033    /// Images opened from supplied readers return `false` because there are no
1034    /// filesystem paths to reprobe.
1035    ///
1036    /// # Errors
1037    ///
1038    /// Returns an error if a path-backed probe fails unexpectedly.
1039    pub fn segment_files_encrypted(&self) -> Result<bool> {
1040        if self.has_supplied_segment_readers()? {
1041            return Ok(false);
1042        }
1043        check_segment_files_encryption(self.segment_filenames())
1044    }
1045
1046    /// Returns the segment containing a logical chunk.
1047    ///
1048    /// # Errors
1049    ///
1050    /// Returns an error if `chunk_index` is outside the parsed chunk index or if
1051    /// the chunk index references a missing segment.
1052    pub fn segment_filename_for_chunk(&self, chunk_index: u64) -> Result<&Path> {
1053        let chunk = self.lookup_chunk(chunk_index)?;
1054        self.segment_filename(chunk.segment_index)
1055            .ok_or_else(|| EwfError::Malformed("chunk references missing segment".into()))
1056    }
1057
1058    /// Returns the segment containing a logical byte offset.
1059    ///
1060    /// Offsets at or beyond the logical media size return `Ok(None)`.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns an error if the chunk geometry is malformed.
1065    pub fn segment_filename_for_offset(&self, offset: u64) -> Result<Option<&Path>> {
1066        if offset >= self.inner.info.logical_size {
1067            return Ok(None);
1068        }
1069        let chunk_size = self.inner.info.chunk_size;
1070        if chunk_size == 0 {
1071            return Err(EwfError::Malformed("chunk size is zero".into()));
1072        }
1073        self.segment_filename_for_chunk(offset / chunk_size)
1074            .map(Some)
1075    }
1076
1077    /// Returns the top-level EWF format generation.
1078    pub fn format(&self) -> Format {
1079        self.inner.info.format
1080    }
1081
1082    /// Returns the inferred format profile.
1083    pub fn format_profile(&self) -> FormatProfile {
1084        self.inner.info.format_profile
1085    }
1086
1087    /// Returns the logical chunk size in bytes.
1088    pub fn chunk_size(&self) -> u64 {
1089        self.inner.info.chunk_size
1090    }
1091
1092    /// Returns the logical media size in bytes.
1093    pub fn media_size(&self) -> u64 {
1094        self.inner.info.logical_size
1095    }
1096
1097    /// Returns the header codepage used for decoded EWF1 values.
1098    pub fn header_codepage(&self) -> HeaderCodepage {
1099        self.inner.info.header_codepage
1100    }
1101
1102    /// Returns the date format applied to header date values.
1103    pub fn header_values_date_format(&self) -> HeaderDateFormat {
1104        self.inner.info.header_values_date_format
1105    }
1106
1107    /// Returns sectors per chunk from media metadata.
1108    pub fn sectors_per_chunk(&self) -> Option<u64> {
1109        self.inner.info.media.sectors_per_chunk
1110    }
1111
1112    /// Returns bytes per sector from media metadata.
1113    pub fn bytes_per_sector(&self) -> Option<u64> {
1114        self.inner.info.media.bytes_per_sector
1115    }
1116
1117    /// Returns the logical sector count from media metadata.
1118    pub fn number_of_sectors(&self) -> Option<u64> {
1119        self.inner.info.media.sector_count
1120    }
1121
1122    /// Returns the logical chunk count from media metadata.
1123    pub fn number_of_chunks(&self) -> Option<u64> {
1124        self.inner.info.media.chunk_count
1125    }
1126
1127    /// Returns error granularity in sectors from media metadata.
1128    pub fn error_granularity(&self) -> Option<u64> {
1129        self.inner.info.media.error_granularity
1130    }
1131
1132    /// Returns the segment set identifier.
1133    pub fn segment_file_set_identifier(&self) -> Option<[u8; 16]> {
1134        self.inner.info.media.set_identifier
1135    }
1136
1137    /// Returns the EWF2 segment file version.
1138    pub fn segment_file_version(&self) -> Option<SegmentFileVersion> {
1139        self.inner.info.media.ewf2_segment_file_version
1140    }
1141
1142    /// Returns the stored compression method metadata.
1143    pub fn compression_method(&self) -> Option<CompressionMethod> {
1144        self.inner.info.media.compression_method
1145    }
1146
1147    /// Returns stored compression level and flags metadata.
1148    pub fn compression_values(&self) -> CompressionValues {
1149        self.inner.info.media.compression_values
1150    }
1151
1152    /// Returns media type metadata.
1153    pub fn media_type(&self) -> Option<MediaType> {
1154        self.inner.info.media.media_type
1155    }
1156
1157    /// Returns media flag metadata.
1158    pub fn media_flags(&self) -> MediaFlags {
1159        self.inner.info.media.media_flags
1160    }
1161
1162    /// Returns memory acquisition extents.
1163    pub fn memory_extents(&self) -> &[MemoryExtent] {
1164        &self.inner.info.memory_extents
1165    }
1166
1167    /// Returns the number of memory acquisition extents.
1168    pub fn number_of_memory_extents(&self) -> usize {
1169        self.inner.info.memory_extents.len()
1170    }
1171
1172    /// Returns one memory acquisition extent by index.
1173    pub fn memory_extent(&self, index: usize) -> Option<&MemoryExtent> {
1174        self.inner.info.memory_extents.get(index)
1175    }
1176
1177    /// Returns raw EWF2 increment data sections.
1178    pub fn ewf2_increment_data(&self) -> &[Vec<u8>] {
1179        &self.inner.info.ewf2_increment_data
1180    }
1181
1182    /// Returns the number of EWF2 increment data sections.
1183    pub fn number_of_ewf2_increment_data_sections(&self) -> usize {
1184        self.inner.info.ewf2_increment_data.len()
1185    }
1186
1187    /// Returns one raw EWF2 increment data section by index.
1188    pub fn ewf2_increment_data_section(&self, index: usize) -> Option<&[u8]> {
1189        self.inner
1190            .info
1191            .ewf2_increment_data
1192            .get(index)
1193            .map(Vec::as_slice)
1194    }
1195
1196    /// Returns the raw EWF2 final information section.
1197    pub fn ewf2_final_information(&self) -> Option<&[u8]> {
1198        self.inner.info.ewf2_final_information.as_deref()
1199    }
1200
1201    /// Returns EWF2 restart data text.
1202    pub fn ewf2_restart_data(&self) -> Option<&str> {
1203        self.inner.info.ewf2_restart_data.as_deref()
1204    }
1205
1206    /// Returns EWF2 analytical data text.
1207    pub fn ewf2_analytical_data(&self) -> Option<&str> {
1208        self.inner.info.ewf2_analytical_data.as_deref()
1209    }
1210
1211    /// Returns a header value by its EWF identifier.
1212    pub fn header_value(&self, identifier: &str) -> Option<std::borrow::Cow<'_, str>> {
1213        self.inner
1214            .info
1215            .metadata
1216            .header_value_with_date_format(identifier, self.inner.info.header_values_date_format)
1217    }
1218
1219    /// Returns the number of available header values.
1220    pub fn number_of_header_values(&self) -> usize {
1221        self.inner.info.metadata.number_of_header_values()
1222    }
1223
1224    /// Returns a header value identifier by enumeration index.
1225    pub fn header_value_identifier(&self, index: usize) -> Option<&str> {
1226        self.inner.info.metadata.header_value_identifier(index)
1227    }
1228
1229    /// Returns a stored hash string by identifier.
1230    pub fn hash_value(&self, identifier: &str) -> Option<&str> {
1231        self.inner.info.stored_hashes.hash_value(identifier)
1232    }
1233
1234    /// Returns the number of stored hash strings.
1235    pub fn number_of_hash_values(&self) -> usize {
1236        self.inner.info.stored_hashes.number_of_hash_values()
1237    }
1238
1239    /// Returns a stored hash identifier by enumeration index.
1240    pub fn hash_value_identifier(&self, index: usize) -> Option<&str> {
1241        self.inner.info.stored_hashes.hash_value_identifier(index)
1242    }
1243
1244    /// Returns a seekable cursor over the logical media stream.
1245    pub fn cursor(&self) -> ImageCursor {
1246        self.inner.statistics.record_cursor_created();
1247        ImageCursor {
1248            image: self.clone(),
1249            position: 0,
1250        }
1251    }
1252
1253    /// Reads logical media bytes at an absolute byte offset.
1254    ///
1255    /// Returns `Ok(0)` when `buf` is empty or `offset` is at or beyond the
1256    /// logical media size.
1257    ///
1258    /// # Errors
1259    ///
1260    /// Returns an error if chunk lookup, decoding, checksum validation, or
1261    /// segment I/O fails, or if [`Image::signal_abort`] has been called.
1262    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<usize> {
1263        self.read_at_impl(buf, offset, false)
1264    }
1265
1266    fn read_at_impl(&self, buf: &mut [u8], offset: u64, strict: bool) -> Result<usize> {
1267        self.ensure_not_aborted()?;
1268        if buf.is_empty() || offset >= self.inner.info.logical_size {
1269            return Ok(0);
1270        }
1271
1272        let chunk_size = self.inner.info.chunk_size;
1273        if chunk_size == 0 {
1274            return Err(EwfError::Malformed("chunk size is zero".into()));
1275        }
1276
1277        let available = self.inner.info.logical_size - offset;
1278        let requested = u64::try_from(buf.len())
1279            .map_err(|_| EwfError::Malformed("read buffer length does not fit u64".into()))?;
1280        let to_read = available.min(requested);
1281        let mut copied = 0_usize;
1282        let mut current = offset;
1283
1284        while u64::try_from(copied).expect("usize fits u64") < to_read {
1285            self.ensure_not_aborted()?;
1286            let chunk_id = current / chunk_size;
1287            let page_offset = usize::try_from(current % chunk_size)
1288                .map_err(|_| EwfError::Malformed("page offset does not fit usize".into()))?;
1289            let decoded = if strict {
1290                Arc::new(self.verification_chunk(chunk_id)?)
1291            } else {
1292                self.read_chunk(chunk_id)?
1293            };
1294            let remaining =
1295                usize::try_from(to_read - u64::try_from(copied).unwrap()).map_err(|_| {
1296                    EwfError::Malformed("remaining read size does not fit usize".into())
1297                })?;
1298            let page_available = decoded.len().saturating_sub(page_offset);
1299            let n = remaining.min(page_available);
1300            if n == 0 {
1301                break;
1302            }
1303
1304            buf[copied..copied + n].copy_from_slice(&decoded[page_offset..page_offset + n]);
1305            copied += n;
1306            current += u64::try_from(n).expect("usize fits u64");
1307        }
1308
1309        Ok(copied)
1310    }
1311
1312    /// Alias for [`Image::read_at`].
1313    ///
1314    /// # Errors
1315    ///
1316    /// Returns the same errors as [`Image::read_at`].
1317    pub fn read_buffer_at_offset(&self, buf: &mut [u8], offset: u64) -> Result<usize> {
1318        self.read_at(buf, offset)
1319    }
1320
1321    /// Reads bytes from a logical single-file catalog entry.
1322    ///
1323    /// Sparse extents read as zeroes. EWF1 logical sparse entries repeat a
1324    /// stored byte or read from their duplicate offset.
1325    ///
1326    /// # Errors
1327    ///
1328    /// Returns an error if the entry metadata is inconsistent, the requested
1329    /// file range is not covered by extents, underlying image reads fail, or
1330    /// [`Image::signal_abort`] has been called.
1331    pub fn read_single_file_at(
1332        &self,
1333        entry: &SingleFileEntry,
1334        buf: &mut [u8],
1335        offset: u64,
1336    ) -> Result<usize> {
1337        self.read_single_file_at_impl(entry, buf, offset, false)
1338    }
1339
1340    /// Reads a logical file with strict chunk validation, bypassing decoded
1341    /// caches and the zero-on-error policy. Sparse entry handling is unchanged.
1342    /// Does not verify stored file hashes; use `verify_single_file` for that.
1343    pub fn read_single_file_at_strict(
1344        &self,
1345        entry: &SingleFileEntry,
1346        buf: &mut [u8],
1347        offset: u64,
1348    ) -> Result<usize> {
1349        self.read_single_file_at_impl(entry, buf, offset, true)
1350    }
1351
1352    fn read_single_file_at_impl(
1353        &self,
1354        entry: &SingleFileEntry,
1355        buf: &mut [u8],
1356        offset: u64,
1357        strict: bool,
1358    ) -> Result<usize> {
1359        self.ensure_not_aborted()?;
1360        if buf.is_empty() {
1361            return Ok(0);
1362        }
1363
1364        let file_size = single_file_size(entry)?;
1365        if offset >= file_size {
1366            return Ok(0);
1367        }
1368
1369        let requested = u64::try_from(buf.len())
1370            .map_err(|_| EwfError::Malformed("read buffer length does not fit u64".into()))?;
1371        let to_read = requested.min(file_size - offset);
1372        let mut copied = 0_usize;
1373        let mut file_position = 0_u64;
1374        let ewf1_logical_sparse = self.inner.info.format == Format::Ewf1
1375            && matches!(
1376                self.inner.info.format_profile,
1377                FormatProfile::LogicalEnCase5
1378                    | FormatProfile::LogicalEnCase6
1379                    | FormatProfile::LogicalEnCase7
1380            )
1381            && entry
1382                .flags
1383                .is_some_and(|flags| flags & EWF1_LOGICAL_SPARSE_FLAG != 0);
1384
1385        if (entry.extents.is_empty() || ewf1_logical_sparse)
1386            && let Some(duplicate_data_offset) = entry.duplicate_data_offset
1387            && duplicate_data_offset >= 0
1388        {
1389            let duplicate_data_offset = u64::try_from(duplicate_data_offset).map_err(|_| {
1390                EwfError::Malformed("single file duplicate data offset does not fit u64".into())
1391            })?;
1392            let image_offset = duplicate_data_offset.checked_add(offset).ok_or_else(|| {
1393                EwfError::Malformed("single file duplicate data offset overflow".into())
1394            })?;
1395            let read_size = usize::try_from(to_read).map_err(|_| {
1396                EwfError::Malformed("single file duplicate read size does not fit usize".into())
1397            })?;
1398            let out = &mut buf[..read_size];
1399            let read = self.read_at_impl(out, image_offset, strict)?;
1400            if read != read_size {
1401                return Err(EwfError::Malformed(
1402                    "single file duplicate data read was truncated".into(),
1403                ));
1404            }
1405            return Ok(read);
1406        }
1407
1408        if ewf1_logical_sparse {
1409            let extent = entry
1410                .extents
1411                .first()
1412                .filter(|extent| !extent.sparse && extent.data_size >= 1)
1413                .ok_or_else(|| {
1414                    EwfError::Malformed("single file sparse entry has no stored byte".into())
1415                })?;
1416            let mut byte = [0_u8; 1];
1417            if self.read_at_impl(&mut byte, extent.data_offset, strict)? != 1 {
1418                return Err(EwfError::Malformed(
1419                    "single file sparse entry stored byte was truncated".into(),
1420                ));
1421            }
1422            let read_size = usize::try_from(to_read).map_err(|_| {
1423                EwfError::Malformed("single file sparse read size does not fit usize".into())
1424            })?;
1425            buf[..read_size].fill(byte[0]);
1426            return Ok(read_size);
1427        }
1428
1429        for extent in &entry.extents {
1430            let extent_start = file_position;
1431            let extent_end = extent_start
1432                .checked_add(extent.data_size)
1433                .ok_or_else(|| EwfError::Malformed("single file extent range overflow".into()))?;
1434            file_position = extent_end;
1435
1436            let read_start = offset.max(extent_start);
1437            let read_end = (offset + to_read).min(extent_end);
1438            if read_start >= read_end {
1439                continue;
1440            }
1441
1442            let extent_relative_offset = read_start
1443                .checked_sub(extent_start)
1444                .ok_or_else(|| EwfError::Malformed("single file extent offset underflow".into()))?;
1445            let output_offset = usize::try_from(read_start - offset).map_err(|_| {
1446                EwfError::Malformed("single file output offset does not fit usize".into())
1447            })?;
1448            let read_size = usize::try_from(read_end - read_start).map_err(|_| {
1449                EwfError::Malformed("single file read size does not fit usize".into())
1450            })?;
1451            let out = &mut buf[output_offset..output_offset + read_size];
1452
1453            if extent.sparse {
1454                out.fill(0);
1455                copied += read_size;
1456                continue;
1457            }
1458
1459            let image_offset = extent
1460                .data_offset
1461                .checked_add(extent_relative_offset)
1462                .ok_or_else(|| {
1463                    EwfError::Malformed("single file extent data offset overflow".into())
1464                })?;
1465            let read = self.read_at_impl(out, image_offset, strict)?;
1466            if read != read_size {
1467                return Err(EwfError::Malformed(
1468                    "single file extent read was truncated".into(),
1469                ));
1470            }
1471            copied += read_size;
1472        }
1473
1474        if u64::try_from(copied).expect("usize fits u64") != to_read {
1475            return Err(EwfError::Malformed(
1476                "single file extents do not cover requested range".into(),
1477            ));
1478        }
1479        Ok(copied)
1480    }
1481
1482    /// Alias for [`Image::read_single_file_at`].
1483    ///
1484    /// # Errors
1485    ///
1486    /// Returns the same errors as [`Image::read_single_file_at`].
1487    pub fn read_file_entry_at(
1488        &self,
1489        entry: &SingleFileEntry,
1490        buf: &mut [u8],
1491        offset: u64,
1492    ) -> Result<usize> {
1493        self.read_single_file_at(entry, buf, offset)
1494    }
1495
1496    /// Returns the root logical single-file entry, if present.
1497    pub fn root_file_entry(&self) -> Option<&SingleFileEntry> {
1498        self.inner
1499            .info
1500            .single_files
1501            .as_ref()
1502            .map(|single_files| &single_files.root)
1503    }
1504
1505    /// Returns a logical single-file entry by path.
1506    ///
1507    /// # Errors
1508    ///
1509    /// Returns an error if `path` contains an empty entry name.
1510    pub fn file_entry_by_path(&self, path: &str) -> Result<Option<&SingleFileEntry>> {
1511        self.inner
1512            .info
1513            .single_files
1514            .as_ref()
1515            .map_or(Ok(None), |single_files| single_files.entry_by_path(path))
1516    }
1517
1518    /// Returns the source record associated with a logical single-file entry.
1519    pub fn source_for_file_entry(&self, entry: &SingleFileEntry) -> Option<&SingleFileSource> {
1520        self.inner
1521            .info
1522            .single_files
1523            .as_ref()?
1524            .source_for_entry(entry)
1525    }
1526
1527    /// Returns the subject record associated with a logical single-file entry.
1528    pub fn subject_for_file_entry(&self, entry: &SingleFileEntry) -> Option<&SingleFileSubject> {
1529        self.inner
1530            .info
1531            .single_files
1532            .as_ref()?
1533            .subject_for_entry(entry)
1534    }
1535
1536    /// Returns access-control entries associated with a logical single-file entry.
1537    pub fn access_control_entries_for_file_entry(
1538        &self,
1539        entry: &SingleFileEntry,
1540    ) -> &[SingleFilePermission] {
1541        self.inner
1542            .info
1543            .single_files
1544            .as_ref()
1545            .map_or(&[], |single_files| {
1546                single_files.access_control_entries_for_entry(entry)
1547            })
1548    }
1549
1550    /// Returns the number of access-control entries for a logical single-file entry.
1551    pub fn number_of_access_control_entries_for_file_entry(
1552        &self,
1553        entry: &SingleFileEntry,
1554    ) -> usize {
1555        self.access_control_entries_for_file_entry(entry).len()
1556    }
1557
1558    /// Returns one access-control entry for a logical single-file entry by index.
1559    pub fn access_control_entry_for_file_entry(
1560        &self,
1561        entry: &SingleFileEntry,
1562        index: usize,
1563    ) -> Option<&SingleFilePermission> {
1564        self.access_control_entries_for_file_entry(entry).get(index)
1565    }
1566
1567    /// Returns the stored MD5 hash bytes.
1568    pub fn md5_hash(&self) -> Option<[u8; 16]> {
1569        self.inner.info.stored_hashes.md5
1570    }
1571
1572    /// Returns the stored SHA1 hash bytes.
1573    pub fn sha1_hash(&self) -> Option<[u8; 20]> {
1574        self.inner.info.stored_hashes.sha1
1575    }
1576
1577    /// Returns acquisition error ranges.
1578    pub fn acquisition_errors(&self) -> &[AcquisitionError] {
1579        &self.inner.info.acquisition_errors
1580    }
1581
1582    /// Returns the number of acquisition error ranges.
1583    pub fn number_of_acquisition_errors(&self) -> usize {
1584        self.inner.info.acquisition_errors.len()
1585    }
1586
1587    /// Returns one acquisition error range by index.
1588    pub fn acquisition_error(&self, index: usize) -> Option<&AcquisitionError> {
1589        self.inner.info.acquisition_errors.get(index)
1590    }
1591
1592    /// Returns session sector ranges.
1593    pub fn sessions(&self) -> &[SectorRange] {
1594        &self.inner.info.sessions
1595    }
1596
1597    /// Returns the number of session sector ranges.
1598    pub fn number_of_sessions(&self) -> usize {
1599        self.inner.info.sessions.len()
1600    }
1601
1602    /// Returns one session sector range by index.
1603    pub fn session(&self, index: usize) -> Option<&SectorRange> {
1604        self.inner.info.sessions.get(index)
1605    }
1606
1607    /// Returns track sector ranges.
1608    pub fn tracks(&self) -> &[SectorRange] {
1609        &self.inner.info.tracks
1610    }
1611
1612    /// Returns the number of track sector ranges.
1613    pub fn number_of_tracks(&self) -> usize {
1614        self.inner.info.tracks.len()
1615    }
1616
1617    /// Returns one track sector range by index.
1618    pub fn track(&self, index: usize) -> Option<&SectorRange> {
1619        self.inner.info.tracks.get(index)
1620    }
1621
1622    /// Returns whether checksum-failed chunks are read as zero-filled data.
1623    pub fn read_zero_chunk_on_error(&self) -> bool {
1624        self.inner.read_zero_chunk_on_error.load(Ordering::Relaxed)
1625    }
1626
1627    /// Sets whether checksum-failed chunks are read as zero-filled data.
1628    pub fn set_read_zero_chunk_on_error(&self, zero_on_error: bool) {
1629        self.inner
1630            .read_zero_chunk_on_error
1631            .store(zero_on_error, Ordering::Relaxed);
1632    }
1633
1634    /// Signals future reads and verification to abort with [`EwfError::Aborted`].
1635    pub fn signal_abort(&self) {
1636        self.inner.abort_signaled.store(true, Ordering::Relaxed);
1637    }
1638
1639    /// Returns checksum error ranges observed while reading.
1640    ///
1641    /// # Errors
1642    ///
1643    /// Returns an error if the internal checksum-error lock is poisoned.
1644    pub fn checksum_errors(&self) -> Result<Vec<SectorRange>> {
1645        Ok(self
1646            .inner
1647            .checksum_errors
1648            .lock()
1649            .map_err(|_| EwfError::Malformed("checksum errors lock poisoned".into()))?
1650            .clone())
1651    }
1652
1653    /// Returns the number of checksum error ranges observed while reading.
1654    ///
1655    /// # Errors
1656    ///
1657    /// Returns an error if the internal checksum-error lock is poisoned.
1658    pub fn number_of_checksum_errors(&self) -> Result<usize> {
1659        Ok(self
1660            .inner
1661            .checksum_errors
1662            .lock()
1663            .map_err(|_| EwfError::Malformed("checksum errors lock poisoned".into()))?
1664            .len())
1665    }
1666
1667    /// Returns one checksum error range observed while reading.
1668    ///
1669    /// # Errors
1670    ///
1671    /// Returns an error if the internal checksum-error lock is poisoned.
1672    pub fn checksum_error(&self, index: usize) -> Result<Option<SectorRange>> {
1673        Ok(self
1674            .inner
1675            .checksum_errors
1676            .lock()
1677            .map_err(|_| EwfError::Malformed("checksum errors lock poisoned".into()))?
1678            .get(index)
1679            .cloned())
1680    }
1681
1682    /// Returns a seekable cursor over a logical single-file entry.
1683    pub fn single_file_cursor(&self, entry: &SingleFileEntry) -> SingleFileCursor {
1684        self.inner.statistics.record_cursor_created();
1685        SingleFileCursor {
1686            image: self.clone(),
1687            entry: entry.clone(),
1688            position: 0,
1689        }
1690    }
1691
1692    /// Returns a seekable cursor over a logical single-file entry by path.
1693    ///
1694    /// # Errors
1695    ///
1696    /// Returns an error if `path` contains an empty entry name.
1697    pub fn single_file_cursor_by_path(&self, path: &str) -> Result<Option<SingleFileCursor>> {
1698        self.file_entry_by_path(path)
1699            .map(|entry| entry.map(|entry| self.single_file_cursor(entry)))
1700    }
1701
1702    /// Reads and decodes one logical data chunk.
1703    ///
1704    /// # Errors
1705    ///
1706    /// Returns an error if the chunk index is invalid, segment I/O fails,
1707    /// decoding fails, checksum validation fails under the current policy, or
1708    /// [`Image::signal_abort`] has been called.
1709    pub fn read_data_chunk(&self, chunk_index: u64) -> Result<DataChunk> {
1710        self.ensure_not_aborted()?;
1711        let chunk = self.lookup_chunk(chunk_index)?;
1712        let (data, corrupted) = self.decode_chunk_with_policy(chunk_index, chunk)?;
1713        let logical_offset = chunk_index
1714            .checked_mul(self.inner.info.chunk_size)
1715            .ok_or_else(|| EwfError::Malformed("data chunk logical offset overflow".into()))?;
1716
1717        Ok(DataChunk {
1718            chunk_index,
1719            logical_offset,
1720            logical_size: chunk.logical_size,
1721            encoded_size: chunk.encoded_size,
1722            encoding: data_chunk_encoding(chunk.encoding),
1723            corrupted,
1724            data,
1725        })
1726    }
1727
1728    /// Reads one encoded data chunk without decoding the payload.
1729    ///
1730    /// # Errors
1731    ///
1732    /// Returns an error if the chunk index is invalid, segment I/O fails, or
1733    /// [`Image::signal_abort`] has been called.
1734    pub fn read_encoded_data_chunk(&self, chunk_index: u64) -> Result<EncodedDataChunk> {
1735        self.ensure_not_aborted()?;
1736        let chunk = self.lookup_chunk(chunk_index)?;
1737        let data = self.read_encoded_chunk_bytes(chunk_index, chunk)?;
1738        let logical_offset = chunk_index
1739            .checked_mul(self.inner.info.chunk_size)
1740            .ok_or_else(|| EwfError::Malformed("data chunk logical offset overflow".into()))?;
1741
1742        Ok(EncodedDataChunk {
1743            chunk_index,
1744            logical_offset,
1745            logical_size: chunk.logical_size,
1746            encoded_size: chunk.encoded_size,
1747            encoding: data_chunk_encoding(chunk.encoding),
1748            has_checksum: chunk.validate_checksum,
1749            data,
1750        })
1751    }
1752
1753    fn read_chunk(&self, chunk_id: u64) -> Result<Arc<Vec<u8>>> {
1754        self.ensure_not_aborted()?;
1755        let mut first_cache_check = true;
1756        loop {
1757            let cached = self
1758                .inner
1759                .chunk_cache
1760                .lock()
1761                .map_err(|_| EwfError::Malformed("chunk cache lock poisoned".into()))?
1762                .get(&chunk_id)
1763                .cloned();
1764            if first_cache_check {
1765                self.inner
1766                    .statistics
1767                    .record_chunk_cache_access(cached.is_some());
1768                first_cache_check = false;
1769            }
1770            if let Some(cached) = cached {
1771                return Ok(cached);
1772            }
1773
1774            let (flight, decode_here) = {
1775                let mut in_flight = self
1776                    .inner
1777                    .chunk_in_flight
1778                    .lock()
1779                    .map_err(|_| EwfError::Malformed("chunk flight lock poisoned".into()))?;
1780                if let Some(flight) = in_flight.get(&chunk_id) {
1781                    (Arc::clone(flight), false)
1782                } else {
1783                    let flight = Arc::new(ChunkFlight {
1784                        complete: Mutex::new(false),
1785                        ready: Condvar::new(),
1786                    });
1787                    in_flight.insert(chunk_id, Arc::clone(&flight));
1788                    (flight, true)
1789                }
1790            };
1791
1792            if !decode_here {
1793                self.inner.statistics.record_chunk_cache_coalesced();
1794                let mut complete = flight
1795                    .complete
1796                    .lock()
1797                    .map_err(|_| EwfError::Malformed("chunk flight lock poisoned".into()))?;
1798                while !*complete {
1799                    complete = flight
1800                        .ready
1801                        .wait(complete)
1802                        .map_err(|_| EwfError::Malformed("chunk flight lock poisoned".into()))?;
1803                }
1804                continue;
1805            }
1806
1807            let decoded = (|| {
1808                let chunk = self.lookup_chunk(chunk_id)?;
1809                let (decoded, _) = self.decode_chunk_with_policy(chunk_id, chunk)?;
1810                let decoded = Arc::new(decoded);
1811                self.inner
1812                    .chunk_cache
1813                    .lock()
1814                    .map_err(|_| EwfError::Malformed("chunk cache lock poisoned".into()))?
1815                    .put(chunk_id, Arc::clone(&decoded));
1816                Ok(decoded)
1817            })();
1818            let mut complete = flight
1819                .complete
1820                .lock()
1821                .map_err(|_| EwfError::Malformed("chunk flight lock poisoned".into()))?;
1822            *complete = true;
1823            flight.ready.notify_all();
1824            drop(complete);
1825            self.inner
1826                .chunk_in_flight
1827                .lock()
1828                .map_err(|_| EwfError::Malformed("chunk flight lock poisoned".into()))?
1829                .remove(&chunk_id);
1830            return decoded;
1831        }
1832    }
1833
1834    fn lookup_chunk(&self, chunk_id: u64) -> Result<Chunk> {
1835        let (_, range) = self.inner.index.range_index_for(chunk_id)?;
1836        let local_index = chunk_id
1837            .checked_sub(range.first_chunk)
1838            .ok_or_else(|| EwfError::Malformed("chunk range underflow".into()))?;
1839        let logical_size = logical_chunk_size(
1840            self.inner.info.logical_size,
1841            self.inner.info.chunk_size,
1842            chunk_id,
1843        )?;
1844
1845        match range.kind {
1846            TableRangeKind::Ewf1 => self.lookup_ewf1_chunk(range, local_index, logical_size),
1847            TableRangeKind::Ewf2 => self.lookup_ewf2_chunk(range, local_index, logical_size),
1848        }
1849    }
1850
1851    fn lookup_ewf1_chunk(
1852        &self,
1853        range: &TableRange,
1854        local_index: u64,
1855        logical_size: usize,
1856    ) -> Result<Chunk> {
1857        let raw = self.read_u32_at(
1858            range.segment_index,
1859            table_entry_offset(range, local_index, 4)?,
1860        )?;
1861        let next_raw = if local_index + 1 < range.chunk_count {
1862            Some(self.read_u32_at(
1863                range.segment_index,
1864                table_entry_offset(range, local_index + 1, 4)?,
1865            )?)
1866        } else {
1867            None
1868        };
1869        let entry = decode_ewf1_entry(
1870            range,
1871            raw,
1872            self.inner.info.chunk_size,
1873            next_raw,
1874            local_index + 1 == range.chunk_count,
1875        )?;
1876
1877        let next_offset = if let Some(next_raw) = next_raw {
1878            decode_ewf1_entry(
1879                range,
1880                next_raw,
1881                self.inner.info.chunk_size,
1882                None,
1883                local_index + 2 == range.chunk_count,
1884            )?
1885            .offset
1886        } else {
1887            range
1888                .data_end
1889                .ok_or_else(|| EwfError::Malformed("EWF1 table range has no data end".into()))?
1890        };
1891
1892        if next_offset <= entry.offset {
1893            return Err(EwfError::Malformed(
1894                "EWF1 chunk offsets are not ordered".into(),
1895            ));
1896        }
1897        let encoded_size = next_offset - entry.offset;
1898        let compression_method = range.ewf1_compression_method.ok_or_else(|| {
1899            EwfError::Malformed("EWF1 table range has no compression method".into())
1900        })?;
1901        let encoding = ewf1_chunk_encoding(
1902            entry.compressed,
1903            encoded_size,
1904            self.inner.info.chunk_size,
1905            compression_method,
1906        )?;
1907        validate_ewf1_encoded_size(
1908            encoded_size,
1909            self.inner.info.chunk_size,
1910            encoding,
1911            range.ewf1_allow_large_compressed_chunks,
1912        )?;
1913        Ok(Chunk {
1914            segment_index: range.segment_index,
1915            offset: entry.offset,
1916            encoded_size,
1917            logical_size,
1918            encoding,
1919            validate_checksum: encoding == ChunkEncoding::Raw
1920                && u64::try_from(logical_size)
1921                    .ok()
1922                    .and_then(|size| size.checked_add(4))
1923                    == Some(encoded_size),
1924        })
1925    }
1926
1927    fn lookup_ewf2_chunk(
1928        &self,
1929        range: &TableRange,
1930        local_index: u64,
1931        logical_size: usize,
1932    ) -> Result<Chunk> {
1933        let entry_offset = table_entry_offset(range, local_index, ewf2::TABLE_ENTRY_SIZE as u64)?;
1934        let entry_data = self.read_table_bytes_at(
1935            range.segment_index,
1936            entry_offset,
1937            ewf2::TABLE_ENTRY_SIZE as u64,
1938        )?;
1939        let compression_method = range
1940            .ewf2_compression_method
1941            .map(ewf2::CompressionMethod::from)
1942            .ok_or_else(|| EwfError::Malformed("EWF2 range has no compression method".into()))?;
1943        let entry = ewf2::TableEntry::parse(&entry_data, compression_method)?;
1944        let encoding = match entry.kind {
1945            ewf2::ChunkKind::Raw | ewf2::ChunkKind::Compressed(ewf2::CompressionMethod::None) => {
1946                ChunkEncoding::Raw
1947            }
1948            ewf2::ChunkKind::Compressed(ewf2::CompressionMethod::Zlib) => ChunkEncoding::Zlib,
1949            ewf2::ChunkKind::Compressed(ewf2::CompressionMethod::Bzip2) => ChunkEncoding::Bzip2,
1950            ewf2::ChunkKind::Compressed(ewf2::CompressionMethod::Unknown(method)) => {
1951                return Err(EwfError::Unsupported(format!(
1952                    "unknown EWF2 compression method {method}"
1953                )));
1954            }
1955            ewf2::ChunkKind::PatternFill => ChunkEncoding::PatternFill(entry.chunk_data_offset),
1956        };
1957        let validate_checksum = matches!(encoding, ChunkEncoding::Raw)
1958            && entry.flags & ewf2::CHUNK_FLAG_HAS_CHECKSUM != 0;
1959        validate_encoded_size(
1960            u64::from(entry.chunk_data_size),
1961            self.inner.info.chunk_size,
1962            encoding,
1963        )?;
1964        Ok(Chunk {
1965            segment_index: range.segment_index,
1966            offset: entry.chunk_data_offset,
1967            encoded_size: u64::from(entry.chunk_data_size),
1968            logical_size,
1969            encoding,
1970            validate_checksum,
1971        })
1972    }
1973
1974    fn read_u32_at(&self, segment_index: usize, offset: u64) -> Result<u32> {
1975        let data = self.read_table_bytes_at(segment_index, offset, 4)?;
1976        Ok(u32::from_le_bytes(
1977            data[..4].try_into().expect("slice length checked"),
1978        ))
1979    }
1980
1981    fn read_table_bytes_at(&self, segment_index: usize, offset: u64, size: u64) -> Result<Vec<u8>> {
1982        let requested = usize::try_from(size)
1983            .map_err(|_| EwfError::Malformed("table read size does not fit usize".into()))?;
1984        let cache_disabled = self
1985            .inner
1986            .table_page_cache
1987            .lock()
1988            .map_err(|_| EwfError::Malformed("table page cache lock poisoned".into()))?
1989            .is_disabled();
1990        if cache_disabled {
1991            self.inner.statistics.record_table_page_cache_access(false);
1992            return self.read_segment_range(segment_index, offset, size, false);
1993        }
1994        let mut output = Vec::with_capacity(requested);
1995        let mut current = offset;
1996        while output.len() < requested {
1997            let page_offset = current - current % TABLE_PAGE_SIZE;
1998            let key = TablePageKey {
1999                segment_index,
2000                page_offset,
2001            };
2002            let cached = self
2003                .inner
2004                .table_page_cache
2005                .lock()
2006                .map_err(|_| EwfError::Malformed("table page cache lock poisoned".into()))?
2007                .get(&key);
2008            self.inner
2009                .statistics
2010                .record_table_page_cache_access(cached.is_some());
2011            let page = if let Some(page) = cached {
2012                page
2013            } else {
2014                let bytes =
2015                    self.read_segment_range(segment_index, page_offset, TABLE_PAGE_SIZE, true)?;
2016                self.inner
2017                    .table_page_cache
2018                    .lock()
2019                    .map_err(|_| EwfError::Malformed("table page cache lock poisoned".into()))?
2020                    .insert(key, bytes)
2021            };
2022            let within_page = usize::try_from(current - page_offset)
2023                .map_err(|_| EwfError::Malformed("table page offset does not fit usize".into()))?;
2024            let available = page.len().saturating_sub(within_page);
2025            if available == 0 {
2026                return Err(EwfError::Malformed("table read crosses segment end".into()));
2027            }
2028            let take = available.min(requested - output.len());
2029            output.extend_from_slice(&page[within_page..within_page + take]);
2030            current = current
2031                .checked_add(u64::try_from(take).expect("usize fits u64"))
2032                .ok_or_else(|| EwfError::Malformed("table read offset overflow".into()))?;
2033        }
2034        Ok(output)
2035    }
2036
2037    pub(crate) fn ensure_not_aborted(&self) -> Result<()> {
2038        if self.inner.abort_signaled.load(Ordering::Relaxed) {
2039            return Err(EwfError::Aborted);
2040        }
2041        Ok(())
2042    }
2043
2044    pub(crate) fn read_segment_range(
2045        &self,
2046        segment_index: usize,
2047        offset: u64,
2048        size: u64,
2049        clamp: bool,
2050    ) -> Result<Vec<u8>> {
2051        let bounded_size = |length: u64| -> Result<u64> {
2052            if clamp {
2053                let size = size.min(length.saturating_sub(offset));
2054                if size == 0 {
2055                    return Err(EwfError::Malformed(
2056                        "table page starts beyond segment end".into(),
2057                    ));
2058                }
2059                return Ok(size);
2060            }
2061            if offset.checked_add(size).is_none_or(|end| end > length) {
2062                return Err(EwfError::Malformed(
2063                    "requested byte range exceeds segment size".into(),
2064                ));
2065            }
2066            Ok(size)
2067        };
2068        if let Some(sources) = &self.inner.positioned_sources {
2069            let source = sources
2070                .get(segment_index)
2071                .ok_or_else(|| EwfError::Malformed("missing segment source".into()))?;
2072            let size = bounded_size(source.len())?;
2073            let mut bytes = vec![
2074                0;
2075                usize::try_from(size).map_err(|_| EwfError::Malformed(
2076                    "segment read size exceeds usize".into()
2077                ))?
2078            ];
2079            source.read_exact_at(&mut bytes, offset)?;
2080            return Ok(bytes);
2081        }
2082        let path = self
2083            .inner
2084            .info
2085            .segment_paths
2086            .get(segment_index)
2087            .ok_or_else(|| EwfError::Malformed("missing segment source".into()))?;
2088
2089        #[cfg(any(unix, windows))]
2090        if !self.has_supplied_segment_readers()? {
2091            let wait_started = Instant::now();
2092            let (file, length) = {
2093                let mut segments =
2094                    self.inner.segments.lock().map_err(|_| {
2095                        EwfError::Malformed("segment file pool lock poisoned".into())
2096                    })?;
2097                loop {
2098                    if let Some(file) = segments.acquire_path_file(segment_index, path)? {
2099                        let length = file.metadata()?.len();
2100                        break (file, length);
2101                    }
2102                    segments =
2103                        self.inner
2104                            .segment_handle_available
2105                            .wait(segments)
2106                            .map_err(|_| {
2107                                EwfError::Malformed("segment file pool lock poisoned".into())
2108                            })?;
2109                }
2110            };
2111            self.inner
2112                .statistics
2113                .record_segment_pool_wait(wait_started.elapsed());
2114            let lease = PositionedSegmentLease {
2115                file: Some(file),
2116                segments: &self.inner.segments,
2117                available: &self.inner.segment_handle_available,
2118            };
2119            let size = bounded_size(length)?;
2120            let mut bytes = vec![
2121                0;
2122                usize::try_from(size).map_err(|_| EwfError::Malformed(
2123                    "segment read size exceeds usize".into()
2124                ))?
2125            ];
2126            let read_started = Instant::now();
2127            read_file_exact_at(lease.file(), &mut bytes, offset)?;
2128            self.inner
2129                .statistics
2130                .record_segment_read(bytes.len(), read_started.elapsed());
2131            return Ok(bytes);
2132        }
2133
2134        let mut segments = self
2135            .inner
2136            .segments
2137            .lock()
2138            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?;
2139        let size = bounded_size(segments.segment_len(segment_index, path)?)?;
2140        read_exact_at(
2141            segments.file_mut(segment_index, path)?.as_reader_mut(),
2142            offset,
2143            size,
2144        )
2145    }
2146
2147    fn has_supplied_segment_readers(&self) -> Result<bool> {
2148        Ok(self
2149            .inner
2150            .segments
2151            .lock()
2152            .map_err(|_| EwfError::Malformed("segment file pool lock poisoned".into()))?
2153            .has_supplied_readers())
2154    }
2155
2156    fn read_encoded_chunk_bytes(&self, chunk_id: u64, chunk: Chunk) -> Result<Vec<u8>> {
2157        self.ensure_not_aborted()?;
2158        if matches!(chunk.encoding, ChunkEncoding::PatternFill(_)) {
2159            return Ok(Vec::new());
2160        }
2161
2162        let mut encoded =
2163            self.read_segment_range(chunk.segment_index, chunk.offset, chunk.encoded_size, false)?;
2164        self.inner
2165            .statistics
2166            .record_encoded_bytes_read(chunk.encoded_size);
2167        if let Some(context) = self
2168            .inner
2169            .encryption_contexts
2170            .get(chunk.segment_index)
2171            .and_then(Option::as_ref)
2172        {
2173            let stream_offset = chunk_id
2174                .checked_mul(self.inner.info.chunk_size)
2175                .ok_or_else(|| EwfError::Malformed("encrypted chunk offset overflow".into()))?;
2176            context.apply_keystream(stream_offset, &mut encoded)?;
2177        }
2178        Ok(encoded)
2179    }
2180
2181    fn decode_chunk(&self, chunk_id: u64, chunk: Chunk) -> Result<Vec<u8>> {
2182        let encoded = self.read_encoded_chunk_bytes(chunk_id, chunk)?;
2183        if chunk.validate_checksum {
2184            validate_raw_chunk_checksum(&encoded, chunk.logical_size)?;
2185        }
2186        let decompression_started = (self.inner.statistics.enabled()
2187            && matches!(
2188                chunk.encoding,
2189                ChunkEncoding::Zlib | ChunkEncoding::Zstd | ChunkEncoding::Bzip2
2190            ))
2191        .then(Instant::now);
2192        let maximum_decoded_size = if self.inner.info.format == Format::Ewf1
2193            && matches!(
2194                self.inner.info.format_profile,
2195                FormatProfile::LogicalEnCase5
2196                    | FormatProfile::LogicalEnCase6
2197                    | FormatProfile::LogicalEnCase7
2198            ) {
2199            usize::try_from(self.inner.info.chunk_size)
2200                .map_err(|_| EwfError::Malformed("chunk size does not fit usize".into()))?
2201        } else {
2202            chunk.logical_size
2203        };
2204        let decoded = decode_chunk_with_padding(
2205            &encoded,
2206            chunk.encoding,
2207            chunk.logical_size,
2208            maximum_decoded_size,
2209        )?;
2210        if let Some(started) = decompression_started {
2211            self.inner
2212                .statistics
2213                .record_decompression(started.elapsed());
2214        }
2215        self.inner.statistics.record_decoded_bytes(decoded.len());
2216        Ok(decoded)
2217    }
2218
2219    pub(crate) fn verification_chunk(&self, chunk_id: u64) -> Result<Vec<u8>> {
2220        self.ensure_not_aborted()?;
2221        let chunk = self.lookup_chunk(chunk_id)?;
2222        // Verification never consumes cached or zero-filled recovery bytes.
2223        self.decode_chunk(chunk_id, chunk).map_err(|error| {
2224            if matches!(error, EwfError::Malformed(_)) && self.encryption_info().is_some() {
2225                EwfError::DecryptionValidationFailed
2226            } else {
2227                error
2228            }
2229        })
2230    }
2231
2232    fn decode_chunk_with_policy(&self, chunk_id: u64, chunk: Chunk) -> Result<(Vec<u8>, bool)> {
2233        match self.decode_chunk(chunk_id, chunk) {
2234            Ok(decoded) => Ok((decoded, false)),
2235            Err(EwfError::Malformed(_))
2236                if self
2237                    .inner
2238                    .encryption_contexts
2239                    .get(chunk.segment_index)
2240                    .is_some_and(Option::is_some) =>
2241            {
2242                Err(EwfError::DecryptionValidationFailed)
2243            }
2244            Err(EwfError::Malformed(_)) if self.read_zero_chunk_on_error() => {
2245                self.record_checksum_error(chunk_id, chunk.logical_size)?;
2246                Ok((vec![0; chunk.logical_size], true))
2247            }
2248            Err(err) => Err(err),
2249        }
2250    }
2251
2252    fn record_checksum_error(&self, chunk_id: u64, logical_size: usize) -> Result<()> {
2253        let range = checksum_error_range(&self.inner.info, chunk_id, logical_size)?;
2254        let mut errors = self
2255            .inner
2256            .checksum_errors
2257            .lock()
2258            .map_err(|_| EwfError::Malformed("checksum errors lock poisoned".into()))?;
2259        if !errors.contains(&range) {
2260            errors.push(range);
2261        }
2262        Ok(())
2263    }
2264}
2265
2266impl SegmentFilePool {
2267    fn new_path(
2268        segment_count: usize,
2269        maximum_open_handles: Option<usize>,
2270        statistics: Arc<ReaderStatisticsCollector>,
2271    ) -> Result<Self> {
2272        validate_maximum_open_handles(maximum_open_handles)?;
2273        Ok(Self {
2274            files: (0..segment_count).map(|_| None).collect(),
2275            lengths: vec![None; segment_count],
2276            ever_opened: vec![false; segment_count],
2277            open_order: VecDeque::new(),
2278            maximum_open_handles,
2279            mode: SegmentFilePoolMode::ReopenFromPath,
2280            statistics,
2281        })
2282    }
2283
2284    fn new_readers(
2285        readers: Vec<SuppliedSegmentReader>,
2286        maximum_open_handles: Option<usize>,
2287        statistics: Arc<ReaderStatisticsCollector>,
2288    ) -> Result<Self> {
2289        validate_maximum_open_handles(maximum_open_handles)?;
2290        let segment_count = readers.len();
2291        if maximum_open_handles.is_some_and(|maximum| maximum < segment_count) {
2292            return Err(EwfError::Unsupported(
2293                "maximum open handles cannot evict supplied segment readers".into(),
2294            ));
2295        }
2296
2297        statistics.record_segment_handle_open(segment_count);
2298        Ok(Self {
2299            files: readers
2300                .into_iter()
2301                .map(|reader| Some(SegmentFileHandle::Supplied(reader)))
2302                .collect(),
2303            lengths: vec![None; segment_count],
2304            ever_opened: vec![true; segment_count],
2305            open_order: (0..segment_count).collect(),
2306            maximum_open_handles,
2307            mode: SegmentFilePoolMode::SuppliedReaders,
2308            statistics,
2309        })
2310    }
2311
2312    fn maximum_open_handles(&self) -> Option<usize> {
2313        self.maximum_open_handles
2314    }
2315
2316    fn segment_len(&mut self, segment_index: usize, path: &Path) -> Result<u64> {
2317        if let Some(length) = self
2318            .lengths
2319            .get(segment_index)
2320            .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?
2321        {
2322            return Ok(*length);
2323        }
2324
2325        let length = self
2326            .file_mut(segment_index, path)?
2327            .as_reader_mut()
2328            .segment_len()?;
2329        let cached = self
2330            .lengths
2331            .get_mut(segment_index)
2332            .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2333        *cached = Some(length);
2334        Ok(length)
2335    }
2336
2337    fn set_maximum_open_handles(&mut self, maximum_open_handles: Option<usize>) -> Result<()> {
2338        validate_maximum_open_handles(maximum_open_handles)?;
2339        if !self.can_close_handles()
2340            && maximum_open_handles.is_some_and(|maximum| maximum < self.open_count())
2341        {
2342            return Err(EwfError::Unsupported(
2343                "maximum open handles cannot evict supplied segment readers".into(),
2344            ));
2345        }
2346        let previous_maximum_open_handles = self.maximum_open_handles;
2347        self.maximum_open_handles = maximum_open_handles;
2348        if let Err(err) = self.enforce_limit() {
2349            self.maximum_open_handles = previous_maximum_open_handles;
2350            return Err(err);
2351        }
2352        Ok(())
2353    }
2354
2355    fn open_count(&self) -> usize {
2356        self.files.iter().filter(|file| file.is_some()).count()
2357    }
2358
2359    fn reserve_handle(&mut self) -> Result<()> {
2360        if let Some(maximum_open_handles) = self.maximum_open_handles {
2361            if self.open_count() >= maximum_open_handles && !self.can_close_handles() {
2362                return Err(EwfError::Unsupported(
2363                    "maximum open handles cannot evict supplied segment readers".into(),
2364                ));
2365            }
2366            while self.open_count() >= maximum_open_handles {
2367                if !self.close_least_recently_used()? {
2368                    return Err(EwfError::Unsupported(
2369                        "maximum open handles are all serving active reads".into(),
2370                    ));
2371                }
2372            }
2373        }
2374        Ok(())
2375    }
2376
2377    fn file_mut(&mut self, segment_index: usize, path: &Path) -> Result<&mut SegmentFileHandle> {
2378        let slot = self
2379            .files
2380            .get(segment_index)
2381            .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2382        if slot.is_none() {
2383            self.reserve_handle()?;
2384            let was_opened = *self
2385                .ever_opened
2386                .get(segment_index)
2387                .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2388            let file = match self.mode {
2389                SegmentFilePoolMode::ReopenFromPath => {
2390                    Some(SegmentFileHandle::Path(PathSegmentReader {
2391                        file: Arc::new(File::open(path)?),
2392                        position: 0,
2393                    }))
2394                }
2395                SegmentFilePoolMode::SuppliedReaders => None,
2396            }
2397            .ok_or_else(|| {
2398                EwfError::Malformed("supplied segment reader was unexpectedly closed".into())
2399            })?;
2400            let slot = self
2401                .files
2402                .get_mut(segment_index)
2403                .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2404            *slot = Some(file);
2405            self.ever_opened[segment_index] = true;
2406            if was_opened {
2407                self.statistics.record_segment_handle_reopen();
2408            } else {
2409                self.statistics.record_segment_handle_open(1);
2410            }
2411        }
2412        self.mark_used(segment_index);
2413        self.files[segment_index]
2414            .as_mut()
2415            .ok_or_else(|| EwfError::Malformed("segment file was not opened".into()))
2416    }
2417
2418    fn acquire_path_file(
2419        &mut self,
2420        segment_index: usize,
2421        path: &Path,
2422    ) -> Result<Option<Arc<File>>> {
2423        if self.mode != SegmentFilePoolMode::ReopenFromPath {
2424            return Err(EwfError::Malformed(
2425                "positioned file requested for a supplied segment reader".into(),
2426            ));
2427        }
2428        let slot = self
2429            .files
2430            .get(segment_index)
2431            .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2432        if slot.is_none() {
2433            if let Some(maximum_open_handles) = self.maximum_open_handles {
2434                while self.open_count() >= maximum_open_handles {
2435                    if !self.close_least_recently_used()? {
2436                        return Ok(None);
2437                    }
2438                }
2439            }
2440            let was_opened = *self
2441                .ever_opened
2442                .get(segment_index)
2443                .ok_or_else(|| EwfError::Malformed("segment index out of range".into()))?;
2444            self.files[segment_index] = Some(SegmentFileHandle::Path(PathSegmentReader {
2445                file: Arc::new(File::open(path)?),
2446                position: 0,
2447            }));
2448            self.ever_opened[segment_index] = true;
2449            if was_opened {
2450                self.statistics.record_segment_handle_reopen();
2451            } else {
2452                self.statistics.record_segment_handle_open(1);
2453            }
2454        }
2455        self.mark_used(segment_index);
2456        self.files[segment_index]
2457            .as_ref()
2458            .and_then(SegmentFileHandle::path_file)
2459            .map(Arc::clone)
2460            .map(Some)
2461            .ok_or_else(|| EwfError::Malformed("path segment file was not opened".into()))
2462    }
2463
2464    fn can_close_handles(&self) -> bool {
2465        self.mode == SegmentFilePoolMode::ReopenFromPath
2466    }
2467
2468    fn has_supplied_readers(&self) -> bool {
2469        self.mode == SegmentFilePoolMode::SuppliedReaders
2470    }
2471
2472    fn mark_used(&mut self, segment_index: usize) {
2473        if let Some(position) = self
2474            .open_order
2475            .iter()
2476            .position(|open_segment_index| *open_segment_index == segment_index)
2477        {
2478            self.open_order.remove(position);
2479        }
2480        self.open_order.push_back(segment_index);
2481    }
2482
2483    fn enforce_limit(&mut self) -> Result<()> {
2484        if let Some(maximum_open_handles) = self.maximum_open_handles {
2485            if self.open_count() > maximum_open_handles && !self.can_close_handles() {
2486                return Err(EwfError::Unsupported(
2487                    "maximum open handles cannot evict supplied segment readers".into(),
2488                ));
2489            }
2490            while self.open_count() > maximum_open_handles {
2491                if !self.close_least_recently_used()? {
2492                    return Err(EwfError::Unsupported(
2493                        "maximum open handles are all serving active reads".into(),
2494                    ));
2495                }
2496            }
2497        }
2498        Ok(())
2499    }
2500
2501    fn close_least_recently_used(&mut self) -> Result<bool> {
2502        let candidates = self.open_order.len();
2503        for _ in 0..candidates {
2504            let Some(segment_index) = self.open_order.pop_front() else {
2505                break;
2506            };
2507            let Some(slot) = self.files.get_mut(segment_index) else {
2508                return Err(EwfError::Malformed("segment index out of range".into()));
2509            };
2510            if slot
2511                .as_ref()
2512                .is_some_and(SegmentFileHandle::idle_path_handle)
2513            {
2514                slot.take();
2515                return Ok(true);
2516            }
2517            if slot.is_some() {
2518                self.open_order.push_back(segment_index);
2519            }
2520        }
2521        Ok(false)
2522    }
2523}
2524
2525fn validate_maximum_open_handles(maximum_open_handles: Option<usize>) -> Result<()> {
2526    if maximum_open_handles == Some(0) {
2527        return Err(EwfError::Unsupported(
2528            "maximum open handles must be at least one".into(),
2529        ));
2530    }
2531    Ok(())
2532}
2533
2534fn data_chunk_encoding(encoding: ChunkEncoding) -> DataChunkEncoding {
2535    match encoding {
2536        ChunkEncoding::Raw => DataChunkEncoding::Raw,
2537        ChunkEncoding::Zlib => DataChunkEncoding::Zlib,
2538        ChunkEncoding::Zstd => DataChunkEncoding::Zstd,
2539        ChunkEncoding::Bzip2 => DataChunkEncoding::Bzip2,
2540        ChunkEncoding::PatternFill(pattern) => DataChunkEncoding::PatternFill(pattern),
2541    }
2542}
2543
2544impl ImageCursor {
2545    /// Returns the current logical media byte position.
2546    pub fn position(&self) -> u64 {
2547        self.position
2548    }
2549
2550    /// Alias for [`ImageCursor::position`].
2551    pub fn offset(&self) -> u64 {
2552        self.position()
2553    }
2554
2555    /// Reads logical media bytes at the current cursor position and advances.
2556    ///
2557    /// # Errors
2558    ///
2559    /// Returns an error if the underlying [`Image::read_at`] call fails or the
2560    /// cursor position would overflow.
2561    pub fn read_buffer(&mut self, buf: &mut [u8]) -> Result<usize> {
2562        let read = self.image.read_at(buf, self.position)?;
2563        self.position = self
2564            .position
2565            .checked_add(u64::try_from(read).expect("usize fits u64"))
2566            .ok_or_else(|| EwfError::Malformed("cursor position overflow".into()))?;
2567        Ok(read)
2568    }
2569
2570    /// Seeks to `offset`, reads into `buf`, and advances by the bytes read.
2571    ///
2572    /// # Errors
2573    ///
2574    /// Returns the same errors as [`ImageCursor::read_buffer`].
2575    pub fn read_buffer_at_offset(&mut self, buf: &mut [u8], offset: u64) -> Result<usize> {
2576        self.position = offset;
2577        self.read_buffer(buf)
2578    }
2579
2580    /// Seeks the cursor using [`SeekFrom`] and returns the new position.
2581    ///
2582    /// # Errors
2583    ///
2584    /// Returns an error if the seek would move before the start of the image or
2585    /// beyond `u64` addressable space.
2586    pub fn seek_offset(&mut self, pos: SeekFrom) -> Result<u64> {
2587        self.seek_position(pos).map_err(EwfError::from)
2588    }
2589
2590    /// Returns the segment containing the current cursor position.
2591    ///
2592    /// Positions at or beyond the logical media size return `Ok(None)`.
2593    ///
2594    /// # Errors
2595    ///
2596    /// Returns an error if the image chunk geometry is malformed.
2597    pub fn segment_filename(&self) -> Result<Option<&Path>> {
2598        self.image.segment_filename_for_offset(self.position)
2599    }
2600
2601    /// Reads the data chunk at the current cursor position and advances to the next chunk.
2602    ///
2603    /// Returns `Ok(None)` when the cursor is at or beyond the logical media end.
2604    ///
2605    /// # Errors
2606    ///
2607    /// Returns an error if reading the chunk fails or the cursor position would
2608    /// overflow.
2609    pub fn read_data_chunk(&mut self) -> Result<Option<DataChunk>> {
2610        let Some(chunk_index) = self.current_chunk_index()? else {
2611            return Ok(None);
2612        };
2613        let chunk = self.image.read_data_chunk(chunk_index)?;
2614        self.advance_to_chunk_end(chunk.logical_offset, chunk.logical_size)?;
2615        Ok(Some(chunk))
2616    }
2617
2618    /// Reads the encoded data chunk at the current cursor position and advances.
2619    ///
2620    /// Returns `Ok(None)` when the cursor is at or beyond the logical media end.
2621    ///
2622    /// # Errors
2623    ///
2624    /// Returns an error if reading the chunk fails or the cursor position would
2625    /// overflow.
2626    pub fn read_encoded_data_chunk(&mut self) -> Result<Option<EncodedDataChunk>> {
2627        let Some(chunk_index) = self.current_chunk_index()? else {
2628            return Ok(None);
2629        };
2630        let chunk = self.image.read_encoded_data_chunk(chunk_index)?;
2631        self.advance_to_chunk_end(chunk.logical_offset, chunk.logical_size)?;
2632        Ok(Some(chunk))
2633    }
2634
2635    fn current_chunk_index(&self) -> Result<Option<u64>> {
2636        if self.position >= self.image.info().logical_size {
2637            return Ok(None);
2638        }
2639        let chunk_size = self.image.info().chunk_size;
2640        if chunk_size == 0 {
2641            return Err(EwfError::Malformed("chunk size is zero".into()));
2642        }
2643        Ok(Some(self.position / chunk_size))
2644    }
2645
2646    fn advance_to_chunk_end(&mut self, logical_offset: u64, logical_size: usize) -> Result<()> {
2647        let logical_size = u64::try_from(logical_size)
2648            .map_err(|_| EwfError::Malformed("chunk logical size does not fit u64".into()))?;
2649        self.position = logical_offset
2650            .checked_add(logical_size)
2651            .ok_or_else(|| EwfError::Malformed("cursor chunk end offset overflow".into()))?;
2652        Ok(())
2653    }
2654
2655    fn seek_position(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2656        let next = match pos {
2657            SeekFrom::Start(offset) => i128::from(offset),
2658            SeekFrom::End(offset) => {
2659                i128::from(self.image.info().logical_size) + i128::from(offset)
2660            }
2661            SeekFrom::Current(offset) => i128::from(self.position) + i128::from(offset),
2662        };
2663        if next < 0 {
2664            return Err(std::io::Error::new(
2665                std::io::ErrorKind::InvalidInput,
2666                "seek before start of image",
2667            ));
2668        }
2669        self.position = u64::try_from(next).map_err(|_| {
2670            std::io::Error::new(
2671                std::io::ErrorKind::InvalidInput,
2672                "seek position does not fit u64",
2673            )
2674        })?;
2675        Ok(self.position)
2676    }
2677}
2678
2679impl Read for ImageCursor {
2680    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2681        self.read_buffer(buf).map_err(std::io::Error::other)
2682    }
2683}
2684
2685impl Seek for ImageCursor {
2686    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2687        self.seek_position(pos)
2688    }
2689}
2690
2691impl SingleFileCursor {
2692    /// Returns the current single-file byte position.
2693    pub fn position(&self) -> u64 {
2694        self.position
2695    }
2696
2697    /// Alias for [`SingleFileCursor::position`].
2698    pub fn offset(&self) -> u64 {
2699        self.position()
2700    }
2701
2702    /// Reads bytes at the current single-file position and advances.
2703    ///
2704    /// # Errors
2705    ///
2706    /// Returns an error if the underlying single-file read fails or the cursor
2707    /// position would overflow.
2708    pub fn read_buffer(&mut self, buf: &mut [u8]) -> Result<usize> {
2709        let read = self
2710            .image
2711            .read_single_file_at(&self.entry, buf, self.position)?;
2712        self.position = self
2713            .position
2714            .checked_add(u64::try_from(read).expect("usize fits u64"))
2715            .ok_or_else(|| EwfError::Malformed("single file cursor position overflow".into()))?;
2716        Ok(read)
2717    }
2718
2719    /// Seeks to `offset`, reads into `buf`, and advances by the bytes read.
2720    ///
2721    /// # Errors
2722    ///
2723    /// Returns the same errors as [`SingleFileCursor::read_buffer`].
2724    pub fn read_buffer_at_offset(&mut self, buf: &mut [u8], offset: u64) -> Result<usize> {
2725        self.position = offset;
2726        self.read_buffer(buf)
2727    }
2728
2729    /// Seeks the cursor using [`SeekFrom`] and returns the new position.
2730    ///
2731    /// # Errors
2732    ///
2733    /// Returns an error if the seek would move before the start of the file, the
2734    /// file size is unavailable, or the resulting position does not fit `u64`.
2735    pub fn seek_offset(&mut self, pos: SeekFrom) -> Result<u64> {
2736        self.seek_position(pos).map_err(EwfError::from)
2737    }
2738
2739    fn seek_position(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2740        let file_size = single_file_size(&self.entry).map_err(std::io::Error::other)?;
2741        let next = match pos {
2742            SeekFrom::Start(offset) => i128::from(offset),
2743            SeekFrom::End(offset) => i128::from(file_size) + i128::from(offset),
2744            SeekFrom::Current(offset) => i128::from(self.position) + i128::from(offset),
2745        };
2746        if next < 0 {
2747            return Err(std::io::Error::new(
2748                std::io::ErrorKind::InvalidInput,
2749                "cannot seek before start of single file",
2750            ));
2751        }
2752        self.position = u64::try_from(next).map_err(|_| {
2753            std::io::Error::new(
2754                std::io::ErrorKind::InvalidInput,
2755                "single file cursor position does not fit u64",
2756            )
2757        })?;
2758        Ok(self.position)
2759    }
2760}
2761
2762impl Read for SingleFileCursor {
2763    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2764        self.read_buffer(buf).map_err(std::io::Error::other)
2765    }
2766}
2767
2768impl Seek for SingleFileCursor {
2769    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2770        self.seek_position(pos)
2771    }
2772
2773    fn stream_position(&mut self) -> std::io::Result<u64> {
2774        Ok(self.position)
2775    }
2776}
2777
2778pub(crate) fn single_file_size(entry: &SingleFileEntry) -> Result<u64> {
2779    if let Some(size) = entry.size {
2780        return Ok(size);
2781    }
2782
2783    entry.extents.iter().try_fold(0_u64, |total, extent| {
2784        total
2785            .checked_add(extent.data_size)
2786            .ok_or_else(|| EwfError::Malformed("single file size overflow".into()))
2787    })
2788}
2789
2790struct ParsedSegment {
2791    format: Format,
2792    sections: Vec<SectionInfo>,
2793    format_profile: FormatProfile,
2794    format_profile_hint_only: bool,
2795    segment_number: u64,
2796    set_identifier: Option<[u8; 16]>,
2797    ewf2_header_profile: Option<Ewf2HeaderProfile>,
2798    ewf1_compression_method: Option<CompressionMethod>,
2799    ewf1_media_geometry: Option<Ewf1MediaGeometry>,
2800    chunk_size: u64,
2801    logical_size: u64,
2802    acquisition_complete: bool,
2803    media: MediaInfo,
2804    ranges: Vec<TableRange>,
2805    table_chunk_count: u64,
2806    metadata: EwfMetadata,
2807    stored_hashes: StoredHashes,
2808    acquisition_errors: Vec<AcquisitionError>,
2809    memory_extents: Vec<MemoryExtent>,
2810    single_files: Option<SingleFilesInfo>,
2811    ewf2_single_files_tables: SingleFilesAuxTables,
2812    ewf2_increment_data: Vec<Vec<u8>>,
2813    ewf2_final_information: Option<Vec<u8>>,
2814    ewf2_restart_data: Option<String>,
2815    ewf2_analytical_data: Option<String>,
2816    sessions: Vec<SectorRange>,
2817    tracks: Vec<SectorRange>,
2818    ewf2_device_information: Option<Vec<u8>>,
2819    ewf2_case_data: Option<Vec<u8>>,
2820}
2821
2822#[derive(Clone, Copy)]
2823struct Ewf2HeaderProfile {
2824    major_version: u8,
2825    minor_version: u8,
2826    compression_method: ewf2::CompressionMethod,
2827}
2828
2829#[derive(Clone, Copy, PartialEq, Eq)]
2830struct Ewf1MediaGeometry {
2831    chunk_size: u64,
2832    logical_size: u64,
2833    sectors_per_chunk: u32,
2834    bytes_per_sector: u32,
2835    sector_count: u64,
2836    chunk_count: u32,
2837}
2838
2839#[derive(Clone, Copy)]
2840struct Ewf1SegmentContext {
2841    first_chunk: u64,
2842    inherited_media_geometry: Option<Ewf1MediaGeometry>,
2843}
2844
2845#[derive(Clone, Copy)]
2846struct Ewf1RangeProfile {
2847    allow_large_compressed_chunks: bool,
2848    compression_method: CompressionMethod,
2849}
2850
2851#[derive(Clone)]
2852struct Section {
2853    desc: ewf1::SectionDescriptor,
2854    data_offset: u64,
2855    data_size: u64,
2856}
2857
2858#[derive(Clone, Copy)]
2859struct Ewf2Section {
2860    desc: ewf2::SectionDescriptor,
2861    data_offset: u64,
2862    data_size: u64,
2863    layout: Ewf2SectionLayout,
2864}
2865
2866#[derive(Clone, Copy, PartialEq, Eq)]
2867enum Ewf2SectionLayout {
2868    LeadingDescriptor,
2869    TrailingDescriptor,
2870}
2871
2872fn parse_ewf1_segment(
2873    file: &mut dyn SegmentReader,
2874    segment_index: usize,
2875    first_chunk: u64,
2876    inherited_media_geometry: Option<Ewf1MediaGeometry>,
2877    profile_hint: FormatProfile,
2878    header_codepage: HeaderCodepage,
2879    statistics: &ReaderStatisticsCollector,
2880) -> Result<ParsedSegment> {
2881    let mut header = [0; ewf1::FILE_HEADER_SIZE];
2882    file.seek(SeekFrom::Start(0))?;
2883    file.read_exact(&mut header)?;
2884    let file_header = ewf1::FileHeader::parse(&header)?;
2885
2886    let sections = scan_ewf1_sections(file)?;
2887    let compression_method = detect_ewf1_compression_method(file_header, &sections)?;
2888    let acquisition_complete = ewf1_acquisition_complete(&sections);
2889    let volume = sections
2890        .iter()
2891        .find(|section| {
2892            matches!(
2893                section.desc.section_type.as_str(),
2894                "volume" | "disk" | "data"
2895            )
2896        })
2897        .map(|section| {
2898            let data = read_exact_at(file, section.data_offset, section.data_size)?;
2899            validate_present_ewf1_media_checksum(&data, &section.desc.section_type)?;
2900            ewf1::Volume::parse(&data)
2901        })
2902        .transpose()?;
2903    if segment_index == 0 && volume.is_none() {
2904        return Err(EwfError::Malformed("missing EWF1 media section".into()));
2905    }
2906    let (chunk_size, logical_size, smart_profile, media) = if let Some(volume) = volume.as_ref() {
2907        let chunk_size = volume.chunk_size()?;
2908        validate_chunk_size(chunk_size)?;
2909        let declared_logical_size = volume.logical_size()?;
2910        let logical_size = if declared_logical_size > 0 {
2911            declared_logical_size
2912        } else {
2913            chunk_size
2914                .checked_mul(u64::from(volume.chunk_count))
2915                .ok_or_else(|| EwfError::Malformed("EWF1 logical size overflow".into()))?
2916        };
2917        let smart_profile = !file_header.logical && volume.smart;
2918        let media = MediaInfo {
2919            sectors_per_chunk: Some(u64::from(volume.sectors_per_chunk)),
2920            bytes_per_sector: Some(u64::from(volume.bytes_per_sector)),
2921            sector_count: Some(volume.sector_count),
2922            chunk_count: Some(u64::from(volume.chunk_count)),
2923            error_granularity: volume.error_granularity.map(u64::from),
2924            set_identifier: volume.set_identifier,
2925            ewf2_segment_file_version: None,
2926            compression_method: Some(compression_method),
2927            compression_values: CompressionValues {
2928                level: volume
2929                    .compression_level
2930                    .map(|value| CompressionLevel::from_i8(value as i8))
2931                    .unwrap_or_default(),
2932                ..CompressionValues::default()
2933            },
2934            media_type: volume
2935                .media_type
2936                .map(ewf1_media_type)
2937                .or_else(|| smart_profile.then_some(MediaType::Removable)),
2938            media_flags: ewf1_media_flags(volume.media_flags, file_header.logical || smart_profile),
2939        };
2940        (chunk_size, logical_size, smart_profile, media)
2941    } else {
2942        (0, 0, false, MediaInfo::default())
2943    };
2944    let ewf1_media_geometry = volume.as_ref().map(|volume| Ewf1MediaGeometry {
2945        chunk_size,
2946        logical_size,
2947        sectors_per_chunk: volume.sectors_per_chunk,
2948        bytes_per_sector: volume.bytes_per_sector,
2949        sector_count: volume.sector_count,
2950        chunk_count: volume.chunk_count,
2951    });
2952
2953    let mut metadata = EwfMetadata::default();
2954    let mut stored_hashes = StoredHashes::default();
2955    let mut acquisition_errors = Vec::new();
2956    let memory_extents = Vec::new();
2957    let mut single_files = None;
2958    let mut format_profile = if file_header.logical {
2959        FormatProfile::LogicalEnCase5
2960    } else if smart_profile {
2961        FormatProfile::Smart
2962    } else if volume.is_none() {
2963        FormatProfile::Unknown
2964    } else if profile_hint != FormatProfile::Unknown {
2965        profile_hint
2966    } else {
2967        FormatProfile::EnCase5
2968    };
2969    let mut format_profile_hint_only = !file_header.logical && !smart_profile;
2970    let mut format_profile_detected_from_header2 = false;
2971    let mut sessions = Vec::new();
2972    let mut tracks = Vec::new();
2973    for section in &sections {
2974        match section.desc.section_type.as_str() {
2975            "header" => {
2976                let data = read_exact_at(file, section.data_offset, section.data_size)?;
2977                let payload = ewf1_metadata_payload(&data, compression_method)?;
2978                let text = decode_header_bytes(&payload, header_codepage);
2979                if !format_profile_detected_from_header2
2980                    && apply_detected_ewf1_format_profile(
2981                        &mut format_profile,
2982                        detect_ewf1_header_profile(&text, 1),
2983                    )
2984                {
2985                    format_profile_hint_only = false;
2986                }
2987                parse_header_data(&payload, header_codepage, &mut metadata);
2988            }
2989            "header2" => {
2990                let data = read_exact_at(file, section.data_offset, section.data_size)?;
2991                let payload = ewf1_metadata_payload(&data, compression_method)?;
2992                if apply_detected_ewf1_format_profile(
2993                    &mut format_profile,
2994                    detect_ewf1_header2_profile(&payload),
2995                ) {
2996                    format_profile_hint_only = false;
2997                    format_profile_detected_from_header2 = true;
2998                }
2999                parse_header2_data(&payload, &mut metadata);
3000            }
3001            "xheader" => {
3002                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3003                let payload = ewf1_metadata_payload(&data, compression_method)?;
3004                parse_xheader_data(&payload, &mut metadata);
3005            }
3006            "error2" => {
3007                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3008                acquisition_errors.extend(parse_error2_data(&data)?);
3009            }
3010            "session" => {
3011                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3012                let sector_count = volume
3013                    .as_ref()
3014                    .map(|volume| volume.sector_count)
3015                    .or_else(|| inherited_media_geometry.map(|geometry| geometry.sector_count))
3016                    .ok_or_else(|| {
3017                        EwfError::Malformed(
3018                            "EWF1 session section has no media geometry to inherit".into(),
3019                        )
3020                    })?;
3021                let parsed_sessions = parse_session_data(&data, 1, sector_count)?;
3022                sessions.extend(parsed_sessions.sessions);
3023                tracks.extend(parsed_sessions.tracks);
3024            }
3025            "hash" => {
3026                validate_hash_section_size(section.data_size, EWF1_HASH_SECTION_SIZE, "EWF1 MD5")?;
3027                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3028                validate_adler32_checksum(&data, 32, 32, "EWF1 MD5 hash")?;
3029                if let Some(hash) = parse_nonzero_hash::<16>(&data) {
3030                    insert_hash_value(&mut stored_hashes, "MD5", &hash)?;
3031                }
3032            }
3033            "digest" => {
3034                validate_hash_section_size(
3035                    section.data_size,
3036                    EWF1_DIGEST_SECTION_SIZE,
3037                    "EWF1 digest",
3038                )?;
3039                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3040                validate_adler32_checksum(&data, 76, 76, "EWF1 digest")?;
3041                if let Some(hash) = parse_nonzero_hash::<16>(&data[..16]) {
3042                    insert_hash_value(&mut stored_hashes, "MD5", &hash)?;
3043                }
3044                if let Some(hash) = parse_nonzero_hash::<20>(&data[16..36]) {
3045                    insert_hash_value(&mut stored_hashes, "SHA1", &hash)?;
3046                }
3047            }
3048            "xhash" => {
3049                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3050                let payload = ewf1_metadata_payload(&data, compression_method)?;
3051                parse_xhash_data(&payload, &mut stored_hashes)?;
3052            }
3053            "ltree" => {
3054                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3055                merge_single_files(&mut single_files, Some(parse_ewf1_ltree_data(&data)?))?;
3056            }
3057            _ => {}
3058        }
3059    }
3060
3061    let ranges = parse_ewf1_ranges(
3062        file,
3063        &sections,
3064        segment_index,
3065        first_chunk,
3066        logical_size,
3067        Ewf1RangeProfile {
3068            allow_large_compressed_chunks: volume.as_ref().is_some_and(|volume| volume.smart),
3069            compression_method,
3070        },
3071        statistics,
3072    )?;
3073    let table_chunk_count = ranges.iter().try_fold(0_u64, |count, range| {
3074        count
3075            .checked_add(range.chunk_count)
3076            .ok_or_else(|| EwfError::Malformed("EWF1 table chunk count overflow".into()))
3077    })?;
3078    Ok(ParsedSegment {
3079        format: Format::Ewf1,
3080        sections: sections
3081            .iter()
3082            .map(|section| SectionInfo {
3083                segment_index,
3084                kind: SectionKind::Ewf1(section.desc.section_type.clone()),
3085                descriptor_offset: section.desc.offset,
3086                data_offset: section.data_offset,
3087                data_size: section.data_size,
3088                linked_descriptor_offset: section.desc.next,
3089                data_flags: 0,
3090            })
3091            .collect(),
3092        format_profile,
3093        format_profile_hint_only,
3094        segment_number: u64::from(file_header.segment_number),
3095        set_identifier: volume.as_ref().and_then(|volume| volume.set_identifier),
3096        ewf2_header_profile: None,
3097        ewf1_compression_method: Some(compression_method),
3098        ewf1_media_geometry,
3099        chunk_size,
3100        logical_size,
3101        acquisition_complete,
3102        media,
3103        ranges,
3104        table_chunk_count,
3105        metadata,
3106        stored_hashes,
3107        acquisition_errors,
3108        memory_extents,
3109        single_files,
3110        ewf2_single_files_tables: SingleFilesAuxTables::default(),
3111        ewf2_increment_data: Vec::new(),
3112        ewf2_final_information: None,
3113        ewf2_restart_data: None,
3114        ewf2_analytical_data: None,
3115        sessions,
3116        tracks,
3117        ewf2_device_information: None,
3118        ewf2_case_data: None,
3119    })
3120}
3121
3122fn ewf1_media_type(value: u8) -> MediaType {
3123    match value {
3124        0x00 => MediaType::Removable,
3125        0x01 => MediaType::Fixed,
3126        0x03 => MediaType::Optical,
3127        0x0e => MediaType::SingleFiles,
3128        0x10 => MediaType::Memory,
3129        value => MediaType::Unknown(value),
3130    }
3131}
3132
3133fn ewf1_media_flags(value: Option<u8>, logical_file_header: bool) -> MediaFlags {
3134    value.map_or(
3135        MediaFlags {
3136            physical: !logical_file_header,
3137            fastbloc: false,
3138            tableau: false,
3139        },
3140        |value| MediaFlags {
3141            physical: value & 0x02 != 0,
3142            fastbloc: value & 0x04 != 0,
3143            tableau: value & 0x08 != 0,
3144        },
3145    )
3146}
3147
3148fn detect_ewf1_compression_method(
3149    file_header: ewf1::FileHeader,
3150    sections: &[Section],
3151) -> Result<CompressionMethod> {
3152    let header_method = match file_header.format_marker {
3153        0x01 | 0x81 => CompressionMethod::Zlib,
3154        0x21 | 0xa1 => CompressionMethod::Zstd,
3155        marker => {
3156            return Err(EwfError::Malformed(format!(
3157                "unsupported EWF1 file header marker 0x{marker:02x}"
3158            )));
3159        }
3160    };
3161
3162    let mut descriptor_method = None;
3163    for marker in sections
3164        .iter()
3165        .filter(|section| section.desc.section_type == "header")
3166        .filter_map(|section| section.desc.compression_method.as_deref())
3167    {
3168        let observed = if marker.eq_ignore_ascii_case("zstd") {
3169            CompressionMethod::Zstd
3170        } else if marker.eq_ignore_ascii_case("zlib") || marker.eq_ignore_ascii_case("deflate") {
3171            CompressionMethod::Zlib
3172        } else {
3173            return Err(EwfError::Malformed(format!(
3174                "unsupported EWF1 descriptor compression marker {marker:?}"
3175            )));
3176        };
3177        if descriptor_method.is_some_and(|expected| expected != observed) {
3178            return Err(EwfError::Malformed(
3179                "EWF1 descriptor compression marker conflict".into(),
3180            ));
3181        }
3182        descriptor_method.get_or_insert(observed);
3183    }
3184
3185    if descriptor_method.is_some_and(|observed| observed != header_method) {
3186        return Err(EwfError::Malformed(
3187            "EWF1 file header and descriptor compression marker conflict".into(),
3188        ));
3189    }
3190    Ok(header_method)
3191}
3192
3193fn ewf1_format_profile_hint_from_path(path: &Path) -> FormatProfile {
3194    match path.extension().and_then(|extension| extension.to_str()) {
3195        Some(extension) if extension.starts_with('e') => FormatProfile::Ewf,
3196        Some(extension) if extension.starts_with('E') => FormatProfile::EnCase2,
3197        Some(extension) if extension.starts_with('L') => FormatProfile::LogicalEnCase5,
3198        Some(extension) if extension.starts_with('s') || extension.starts_with('S') => {
3199            FormatProfile::Smart
3200        }
3201        _ => FormatProfile::Unknown,
3202    }
3203}
3204
3205fn parse_segment(
3206    file: &mut dyn SegmentReader,
3207    first_path: &Path,
3208    segment_index: usize,
3209    ewf1_context: Ewf1SegmentContext,
3210    strictness: OpenStrictness,
3211    header_codepage: HeaderCodepage,
3212    statistics: &ReaderStatisticsCollector,
3213) -> Result<ParsedSegment> {
3214    let mut signature = [0; 8];
3215    file.seek(SeekFrom::Start(0))?;
3216    file.read_exact(&mut signature)?;
3217    if signature == ewf1::EVF_SIGNATURE || signature == ewf1::LVF_SIGNATURE {
3218        parse_ewf1_segment(
3219            file,
3220            segment_index,
3221            ewf1_context.first_chunk,
3222            ewf1_context.inherited_media_geometry,
3223            // Extensions advance EZZ -> FAA (and can eventually reach Sxx).
3224            // Family hints belong to the first segment, never the continuation
3225            // name. Explicit metadata in each segment is still checked below.
3226            ewf1_format_profile_hint_from_path(first_path),
3227            header_codepage,
3228            statistics,
3229        )
3230    } else if signature == ewf2::EX01_SIGNATURE || signature == ewf2::LEF2_SIGNATURE {
3231        parse_ewf2_segment(file, segment_index, strictness, statistics)
3232    } else {
3233        Err(EwfError::InvalidSignature)
3234    }
3235}
3236
3237fn parse_ewf2_segment(
3238    file: &mut dyn SegmentReader,
3239    segment_index: usize,
3240    strictness: OpenStrictness,
3241    statistics: &ReaderStatisticsCollector,
3242) -> Result<ParsedSegment> {
3243    let mut header = [0; ewf2::FILE_HEADER_SIZE];
3244    file.seek(SeekFrom::Start(0))?;
3245    file.read_exact(&mut header)?;
3246    let header = ewf2::FileHeader::parse(&header)?;
3247    let sections = scan_ewf2_sections(file, strictness)?;
3248    let acquisition_complete = ewf2_acquisition_complete(&sections);
3249
3250    let mut chunk_size = 0;
3251    let mut logical_size = 0;
3252    let mut metadata = EwfMetadata::default();
3253    let mut stored_hashes = StoredHashes::default();
3254    let mut acquisition_errors = Vec::new();
3255    let mut memory_extents = Vec::new();
3256    let mut single_files = None;
3257    let mut ewf2_single_files_tables = SingleFilesAuxTables::default();
3258    let mut ewf2_increment_data = Vec::new();
3259    let mut ewf2_final_information = None;
3260    let mut ewf2_restart_data = None;
3261    let mut ewf2_analytical_data = None;
3262    let mut session_table_data = Vec::new();
3263    let mut sessions = Vec::new();
3264    let mut tracks = Vec::new();
3265    let mut media = MediaInfo {
3266        sectors_per_chunk: None,
3267        bytes_per_sector: None,
3268        sector_count: None,
3269        chunk_count: None,
3270        error_granularity: None,
3271        set_identifier: Some(header.set_identifier),
3272        ewf2_segment_file_version: Some(SegmentFileVersion {
3273            major: header.major_version,
3274            minor: header.minor_version,
3275        }),
3276        compression_method: Some(public_compression_method(header.compression_method)),
3277        compression_values: CompressionValues::default(),
3278        media_type: None,
3279        media_flags: MediaFlags {
3280            physical: !header.logical,
3281            fastbloc: false,
3282            tableau: false,
3283        },
3284    };
3285    let mut device_information = None;
3286    let mut case_data = None;
3287    for section in &sections {
3288        match section.desc.section_type {
3289            ewf2::SectionType::DeviceInformation => {
3290                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3291                let data = ewf2_metadata_payload(&data, header.compression_method)?;
3292                remember_ewf2_metadata_payload(
3293                    &mut device_information,
3294                    &data,
3295                    "device information",
3296                )?;
3297                parse_ewf2_device_info_values(&data, &mut metadata);
3298                let geometry = parse_ewf2_device_info(&data)?;
3299                if let Some(size) = geometry.chunk_size {
3300                    chunk_size = size;
3301                }
3302                if let Some(size) = geometry.logical_size {
3303                    logical_size = size;
3304                }
3305                apply_ewf2_geometry(&mut media, geometry);
3306            }
3307            ewf2::SectionType::CaseData => {
3308                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3309                let data = ewf2_metadata_payload(&data, header.compression_method)?;
3310                remember_ewf2_metadata_payload(&mut case_data, &data, "case data")?;
3311                parse_ewf2_case_data(&data, &mut metadata);
3312                let geometry = parse_ewf2_device_info(&data)?;
3313                if chunk_size == 0
3314                    && let Some(size) = geometry.chunk_size
3315                {
3316                    chunk_size = size;
3317                }
3318                if logical_size == 0
3319                    && let Some(size) = geometry.logical_size
3320                {
3321                    logical_size = size;
3322                }
3323                apply_ewf2_geometry_if_missing(&mut media, geometry);
3324            }
3325            ewf2::SectionType::Md5Hash => {
3326                validate_hash_section_size(section.data_size, EWF2_HASH_SECTION_SIZE, "EWF2 MD5")?;
3327                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3328                validate_adler32_checksum(&data, 16, 16, "EWF2 MD5 hash")?;
3329                if let Some(hash) = parse_nonzero_hash::<16>(&data) {
3330                    insert_hash_value(&mut stored_hashes, "MD5", &hash)?;
3331                }
3332            }
3333            ewf2::SectionType::Sha1Hash => {
3334                validate_hash_section_size(section.data_size, EWF2_HASH_SECTION_SIZE, "EWF2 SHA1")?;
3335                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3336                validate_adler32_checksum(&data, 20, 20, "EWF2 SHA1 hash")?;
3337                if let Some(hash) = parse_nonzero_hash::<20>(&data) {
3338                    insert_hash_value(&mut stored_hashes, "SHA1", &hash)?;
3339                }
3340            }
3341            ewf2::SectionType::ErrorTable => {
3342                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3343                acquisition_errors.extend(parse_ewf2_error_table_data(&data)?);
3344            }
3345            ewf2::SectionType::MemoryExtentsTable => {
3346                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3347                memory_extents.extend(parse_ewf2_memory_extents_table(&data)?);
3348            }
3349            ewf2::SectionType::SingleFilesData => {
3350                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3351                merge_single_files(
3352                    &mut single_files,
3353                    Some(parse_ewf2_single_files_data(&data)?),
3354                )?;
3355            }
3356            ewf2::SectionType::SingleFilesTable => {
3357                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3358                merge_single_files_aux_u64_table(
3359                    &mut ewf2_single_files_tables.table_0x21_entries,
3360                    parse_ewf2_single_files_aux_u64_table(&data, "EWF2 single files 0x21 table")?,
3361                    "0x21",
3362                )?;
3363            }
3364            ewf2::SectionType::SingleFilesMd5HashTable => {
3365                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3366                merge_single_files_aux_md5_table(
3367                    &mut ewf2_single_files_tables.md5_hashes,
3368                    parse_ewf2_single_files_md5_hash_table(&data)?,
3369                )?;
3370            }
3371            ewf2::SectionType::SingleFilesUnknownTable => {
3372                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3373                merge_single_files_aux_u64_table(
3374                    &mut ewf2_single_files_tables.table_0x23_entries,
3375                    parse_ewf2_single_files_aux_u64_table(&data, "EWF2 single files 0x23 table")?,
3376                    "0x23",
3377                )?;
3378            }
3379            ewf2::SectionType::IncrementData => {
3380                ewf2_increment_data.push(read_exact_at(
3381                    file,
3382                    section.data_offset,
3383                    section.data_size,
3384                )?);
3385            }
3386            ewf2::SectionType::FinalInformation => {
3387                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3388                merge_optional_ewf2_raw_section(
3389                    &mut ewf2_final_information,
3390                    Some(data),
3391                    "final information",
3392                )?;
3393            }
3394            ewf2::SectionType::RestartData => {
3395                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3396                ewf2_restart_data = Some(decode_ewf2_string_section(
3397                    &data,
3398                    header.compression_method,
3399                    "restart data",
3400                )?);
3401            }
3402            ewf2::SectionType::AnalyticalData => {
3403                let data = read_exact_at(file, section.data_offset, section.data_size)?;
3404                ewf2_analytical_data = Some(decode_ewf2_string_section(
3405                    &data,
3406                    header.compression_method,
3407                    "analytical data",
3408                )?);
3409            }
3410            ewf2::SectionType::SessionTable => {
3411                session_table_data.push(read_exact_at(
3412                    file,
3413                    section.data_offset,
3414                    section.data_size,
3415                )?);
3416            }
3417            _ => {}
3418        }
3419    }
3420
3421    let media_sector_count = media.sector_count.unwrap_or(0);
3422    for data in session_table_data {
3423        let parsed_sessions = parse_session_data(&data, 2, media_sector_count)?;
3424        sessions.extend(parsed_sessions.sessions);
3425        tracks.extend(parsed_sessions.tracks);
3426    }
3427
3428    if chunk_size == 0 {
3429        chunk_size = 32_768;
3430    }
3431    validate_chunk_size(chunk_size)?;
3432    let ranges = parse_ewf2_ranges(
3433        file,
3434        &sections,
3435        segment_index,
3436        logical_size,
3437        header.compression_method,
3438        statistics,
3439    )?;
3440    if logical_size == 0 {
3441        let discovered_chunks = ranges.iter().try_fold(0_u64, |max, range| {
3442            let end = range
3443                .first_chunk
3444                .checked_add(range.chunk_count)
3445                .ok_or_else(|| EwfError::Malformed("EWF2 table chunk count overflow".into()))?;
3446            Ok::<u64, EwfError>(max.max(end))
3447        })?;
3448        logical_size = chunk_size
3449            .checked_mul(discovered_chunks)
3450            .ok_or_else(|| EwfError::Malformed("EWF2 logical size overflow".into()))?;
3451    }
3452    let table_chunk_count = ranges.iter().try_fold(0_u64, |count, range| {
3453        count
3454            .checked_add(range.chunk_count)
3455            .ok_or_else(|| EwfError::Malformed("EWF2 table chunk count overflow".into()))
3456    })?;
3457
3458    Ok(ParsedSegment {
3459        format: Format::Ewf2,
3460        sections: sections
3461            .iter()
3462            .map(|section| {
3463                let raw_kind = read_exact_at(file, section.desc.offset, 4)?;
3464                Ok(SectionInfo {
3465                    segment_index,
3466                    kind: SectionKind::Ewf2(u32::from_le_bytes(
3467                        raw_kind[..4].try_into().expect("four bytes read"),
3468                    )),
3469                    descriptor_offset: section.desc.offset,
3470                    data_offset: section.data_offset,
3471                    data_size: section.data_size,
3472                    linked_descriptor_offset: section.desc.previous_offset,
3473                    data_flags: section.desc.data_flags,
3474                })
3475            })
3476            .collect::<Result<Vec<_>>>()?,
3477        format_profile: if header.logical {
3478            FormatProfile::Ewf2LogicalEnCase7
3479        } else {
3480            FormatProfile::Ewf2EnCase7
3481        },
3482        format_profile_hint_only: false,
3483        segment_number: u64::from(header.segment_number),
3484        set_identifier: Some(header.set_identifier),
3485        ewf2_header_profile: Some(Ewf2HeaderProfile {
3486            major_version: header.major_version,
3487            minor_version: header.minor_version,
3488            compression_method: header.compression_method,
3489        }),
3490        ewf1_compression_method: None,
3491        ewf1_media_geometry: None,
3492        chunk_size,
3493        logical_size,
3494        acquisition_complete,
3495        media,
3496        ranges,
3497        table_chunk_count,
3498        metadata,
3499        stored_hashes,
3500        acquisition_errors,
3501        memory_extents,
3502        single_files,
3503        ewf2_single_files_tables,
3504        ewf2_increment_data,
3505        ewf2_final_information,
3506        ewf2_restart_data,
3507        ewf2_analytical_data,
3508        sessions,
3509        tracks,
3510        ewf2_device_information: device_information,
3511        ewf2_case_data: case_data,
3512    })
3513}
3514
3515fn scan_ewf1_sections(file: &mut dyn SegmentReader) -> Result<Vec<Section>> {
3516    let file_len = file.segment_len()?;
3517    let mut sections = Vec::new();
3518    let mut offset = ewf1::FILE_HEADER_SIZE as u64;
3519    loop {
3520        let descriptor_end = offset
3521            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
3522            .ok_or_else(|| EwfError::Malformed("EWF1 section descriptor overflow".into()))?;
3523        if descriptor_end > file_len {
3524            return Err(EwfError::Malformed(
3525                "EWF1 section descriptor exceeds file".into(),
3526            ));
3527        }
3528        let mut buf = [0; ewf1::SECTION_DESCRIPTOR_SIZE];
3529        file.seek(SeekFrom::Start(offset))?;
3530        file.read_exact(&mut buf)?;
3531        validate_present_adler32_checksum(&buf, 72, 72, "EWF1 section descriptor")?;
3532        let desc = ewf1::SectionDescriptor::parse(&buf, offset)?;
3533        let data_size = desc.data_size()?;
3534        let data_offset = descriptor_end;
3535        let data_end = data_offset
3536            .checked_add(data_size)
3537            .ok_or_else(|| EwfError::Malformed("EWF1 section data exceeds file".into()))?;
3538        if data_end > file_len {
3539            return Err(EwfError::Malformed("EWF1 section data exceeds file".into()));
3540        }
3541        let section_type = desc.section_type.clone();
3542        let next = desc.next;
3543        sections.push(Section {
3544            desc,
3545            data_offset,
3546            data_size,
3547        });
3548        if matches!(section_type.as_str(), "done" | "next") || next == 0 {
3549            return Ok(sections);
3550        }
3551        if next <= offset {
3552            return Err(EwfError::Malformed(
3553                "EWF1 section chain does not advance".into(),
3554            ));
3555        }
3556        if next < data_end {
3557            return Err(EwfError::Malformed(
3558                "EWF1 next section offset overlaps current section".into(),
3559            ));
3560        }
3561        offset = next;
3562    }
3563}
3564
3565fn ewf1_acquisition_complete(sections: &[Section]) -> bool {
3566    sections
3567        .last()
3568        .is_none_or(|section| section.desc.section_type != "next")
3569}
3570
3571fn scan_ewf2_sections(
3572    file: &mut dyn SegmentReader,
3573    strictness: OpenStrictness,
3574) -> Result<Vec<Ewf2Section>> {
3575    let file_len = file.segment_len()?;
3576    if file_len < ewf2::FILE_HEADER_SIZE as u64 + ewf2::SECTION_DESCRIPTOR_SIZE as u64 {
3577        return Err(EwfError::Malformed("EWF2 file is too short".into()));
3578    }
3579
3580    if let Some(sections) = scan_ewf2_leading_sections(file, file_len, strictness)? {
3581        return Ok(sections);
3582    }
3583    scan_ewf2_trailing_sections(file, file_len, strictness)
3584}
3585
3586fn scan_ewf2_leading_sections(
3587    file: &mut dyn SegmentReader,
3588    file_len: u64,
3589    strictness: OpenStrictness,
3590) -> Result<Option<Vec<Ewf2Section>>> {
3591    let mut sections = Vec::new();
3592    let mut offset = ewf2::FILE_HEADER_SIZE as u64;
3593
3594    loop {
3595        if offset
3596            .checked_add(ewf2::SECTION_DESCRIPTOR_SIZE as u64)
3597            .is_none_or(|end| end > file_len)
3598        {
3599            if sections.is_empty() {
3600                return Ok(None);
3601            }
3602            return Err(EwfError::Malformed(
3603                "EWF2 leading section descriptor exceeds file".into(),
3604            ));
3605        }
3606
3607        let mut buf = [0; ewf2::SECTION_DESCRIPTOR_SIZE];
3608        file.seek(SeekFrom::Start(offset))?;
3609        file.read_exact(&mut buf)?;
3610        let Ok(desc) = ewf2::SectionDescriptor::parse(&buf, offset) else {
3611            return if sections.is_empty() {
3612                Ok(None)
3613            } else {
3614                Err(EwfError::Malformed(
3615                    "EWF2 leading section descriptor is invalid".into(),
3616                ))
3617            };
3618        };
3619        if !is_valid_ewf2_leading_descriptor(desc, strictness) {
3620            return if sections.is_empty() {
3621                Ok(None)
3622            } else {
3623                Err(EwfError::Malformed(
3624                    "EWF2 leading section descriptor is invalid".into(),
3625                ))
3626            };
3627        }
3628        validate_present_adler32_checksum(&buf, 60, 60, "EWF2 section descriptor")?;
3629
3630        let data_offset = offset
3631            .checked_add(u64::from(desc.descriptor_size))
3632            .ok_or_else(|| EwfError::Malformed("EWF2 section data offset overflow".into()))?;
3633        let data_end = data_offset
3634            .checked_add(desc.data_size)
3635            .ok_or_else(|| EwfError::Malformed("EWF2 section advance overflow".into()))?;
3636        if data_end > file_len {
3637            return if sections.is_empty() {
3638                Ok(None)
3639            } else {
3640                Err(EwfError::Malformed("EWF2 section data exceeds file".into()))
3641            };
3642        }
3643        let padding_size = ewf2_section_padding_size(desc)?;
3644        let next_offset = data_end
3645            .checked_add(padding_size)
3646            .ok_or_else(|| EwfError::Malformed("EWF2 section padding overflow".into()))?;
3647        if next_offset > file_len {
3648            return Err(EwfError::Malformed(
3649                "EWF2 section padding exceeds file".into(),
3650            ));
3651        }
3652        reject_encrypted_ewf2_section(desc)?;
3653        validate_ewf2_section_integrity_hash(file, desc, data_offset)?;
3654        let section_type = desc.section_type;
3655        sections.push(Ewf2Section {
3656            desc,
3657            data_offset,
3658            data_size: desc.data_size,
3659            layout: Ewf2SectionLayout::LeadingDescriptor,
3660        });
3661        if is_terminal_ewf2_section(section_type) {
3662            return Ok(Some(sections));
3663        }
3664        if next_offset <= offset {
3665            return Err(EwfError::Malformed(
3666                "EWF2 leading section chain does not advance".into(),
3667            ));
3668        }
3669        offset = next_offset;
3670    }
3671}
3672
3673fn scan_ewf2_trailing_sections(
3674    file: &mut dyn SegmentReader,
3675    file_len: u64,
3676    strictness: OpenStrictness,
3677) -> Result<Vec<Ewf2Section>> {
3678    let mut sections = Vec::new();
3679    let header_size = ewf2::FILE_HEADER_SIZE as u64;
3680    let descriptor_size = ewf2::SECTION_DESCRIPTOR_SIZE as u64;
3681    let mut offset = file_len
3682        .checked_sub(descriptor_size)
3683        .ok_or_else(|| EwfError::Malformed("EWF2 file is too short".into()))?;
3684    let max_sections = ((file_len - header_size) / descriptor_size).saturating_add(1);
3685
3686    for _ in 0..max_sections {
3687        let mut buf = [0; ewf2::SECTION_DESCRIPTOR_SIZE];
3688        file.seek(SeekFrom::Start(offset))?;
3689        file.read_exact(&mut buf)?;
3690        let desc = ewf2::SectionDescriptor::parse(&buf, offset)?;
3691        if !is_valid_ewf2_descriptor(desc, strictness) {
3692            return Err(EwfError::Malformed(
3693                "EWF2 trailing section descriptor is invalid".into(),
3694            ));
3695        }
3696        validate_present_adler32_checksum(&buf, 60, 60, "EWF2 section descriptor")?;
3697        ewf2_section_padding_size(desc)?;
3698        let data_offset = if desc.previous_offset == 0 {
3699            header_size
3700        } else {
3701            desc.previous_offset
3702                .checked_add(descriptor_size)
3703                .ok_or_else(|| {
3704                    EwfError::Malformed("EWF2 previous section offset overflow".into())
3705                })?
3706        };
3707        if data_offset < header_size {
3708            return Err(EwfError::Malformed(
3709                "EWF2 trailing section data precedes file header".into(),
3710            ));
3711        }
3712        let data_end = data_offset
3713            .checked_add(desc.data_size)
3714            .ok_or_else(|| EwfError::Malformed("EWF2 trailing section data overflow".into()))?;
3715        if data_end > offset {
3716            return Err(EwfError::Malformed(
3717                "EWF2 trailing section data exceeds descriptor".into(),
3718            ));
3719        }
3720        if desc.previous_offset != 0 {
3721            if desc.previous_offset >= desc.offset {
3722                return Err(EwfError::Malformed(
3723                    "EWF2 previous section offset is not before current section".into(),
3724                ));
3725            }
3726            let previous_end = desc
3727                .previous_offset
3728                .checked_add(descriptor_size)
3729                .ok_or_else(|| {
3730                    EwfError::Malformed("EWF2 previous section offset overflow".into())
3731                })?;
3732            if previous_end > offset {
3733                return Err(EwfError::Malformed(
3734                    "EWF2 previous section overlaps current section descriptor".into(),
3735                ));
3736            }
3737        }
3738
3739        reject_encrypted_ewf2_section(desc)?;
3740        validate_ewf2_section_integrity_hash(file, desc, data_offset)?;
3741        let previous_offset = desc.previous_offset;
3742        sections.push(Ewf2Section {
3743            desc,
3744            data_offset,
3745            data_size: desc.data_size,
3746            layout: Ewf2SectionLayout::TrailingDescriptor,
3747        });
3748        if previous_offset == 0 {
3749            sections.reverse();
3750            return Ok(sections);
3751        }
3752        offset = previous_offset;
3753    }
3754
3755    Err(EwfError::Malformed(
3756        "EWF2 trailing section descriptor chain is too long".into(),
3757    ))
3758}
3759
3760fn is_valid_ewf2_descriptor(desc: ewf2::SectionDescriptor, strictness: OpenStrictness) -> bool {
3761    desc.descriptor_size == ewf2::SECTION_DESCRIPTOR_SIZE as u32
3762        && (strictness == OpenStrictness::Lenient
3763            || !matches!(desc.section_type, ewf2::SectionType::Unknown(_)))
3764}
3765
3766fn is_valid_ewf2_leading_descriptor(
3767    desc: ewf2::SectionDescriptor,
3768    strictness: OpenStrictness,
3769) -> bool {
3770    is_valid_ewf2_descriptor(desc, strictness)
3771}
3772
3773fn ewf2_section_padding_size(desc: ewf2::SectionDescriptor) -> Result<u64> {
3774    let padding_size = u64::from(desc.padding_size);
3775    if padding_size > desc.data_size {
3776        return Err(EwfError::Malformed(
3777            "EWF2 section padding size exceeds data size".into(),
3778        ));
3779    }
3780    Ok(padding_size)
3781}
3782
3783fn reject_encrypted_ewf2_section(desc: ewf2::SectionDescriptor) -> Result<()> {
3784    if desc.section_type == ewf2::SectionType::EncryptionKeys {
3785        return Err(EwfError::Unsupported(
3786            "encrypted EWF2 image with encryption keys section".into(),
3787        ));
3788    }
3789    if desc.encrypted {
3790        return Err(EwfError::Unsupported(format!(
3791            "encrypted EWF2 {:?} section",
3792            desc.section_type
3793        )));
3794    }
3795    Ok(())
3796}
3797
3798fn validate_ewf2_section_integrity_hash(
3799    file: &mut dyn SegmentReader,
3800    desc: ewf2::SectionDescriptor,
3801    data_offset: u64,
3802) -> Result<()> {
3803    if !desc.has_integrity_hash {
3804        return Ok(());
3805    }
3806
3807    let mut hasher = md5::Md5::new();
3808    let mut remaining = desc.data_size;
3809    let mut buffer = [0; 8192];
3810    file.seek(SeekFrom::Start(data_offset))?;
3811    while remaining > 0 {
3812        let take = usize::try_from(remaining.min(buffer.len() as u64))
3813            .expect("section hash read is bounded by buffer length");
3814        file.read_exact(&mut buffer[..take])?;
3815        md5::Digest::update(&mut hasher, &buffer[..take]);
3816        remaining -= u64::try_from(take).expect("usize fits u64");
3817    }
3818
3819    let calculated: [u8; 16] = hasher.finalize().into();
3820    if calculated != desc.data_integrity_hash {
3821        return Err(EwfError::Malformed(
3822            "EWF2 section data integrity hash mismatch".into(),
3823        ));
3824    }
3825
3826    Ok(())
3827}
3828
3829fn remember_ewf2_metadata_payload(
3830    target: &mut Option<Vec<u8>>,
3831    data: &[u8],
3832    label: &str,
3833) -> Result<()> {
3834    if let Some(existing) = target {
3835        if existing.as_slice() != data {
3836            return Err(EwfError::Malformed(format!("EWF2 {label} does not match")));
3837        }
3838    } else {
3839        *target = Some(data.to_vec());
3840    }
3841    Ok(())
3842}
3843
3844fn is_terminal_ewf2_section(section_type: ewf2::SectionType) -> bool {
3845    matches!(
3846        section_type,
3847        ewf2::SectionType::Done | ewf2::SectionType::Next
3848    )
3849}
3850
3851fn ewf2_acquisition_complete(sections: &[Ewf2Section]) -> bool {
3852    sections
3853        .last()
3854        .is_none_or(|section| section.desc.section_type != ewf2::SectionType::Next)
3855}
3856
3857fn parse_ewf2_memory_extents_table(data: &[u8]) -> Result<Vec<MemoryExtent>> {
3858    const ENTRY_SIZE: usize = 16;
3859
3860    if !data.len().is_multiple_of(ENTRY_SIZE) {
3861        return Err(EwfError::Malformed(
3862            "EWF2 memory extents table has partial entry".into(),
3863        ));
3864    }
3865
3866    Ok(data
3867        .as_chunks::<ENTRY_SIZE>()
3868        .0
3869        .iter()
3870        .map(|entry| MemoryExtent {
3871            start_page: u64::from_le_bytes(entry[0..8].try_into().expect("slice length checked")),
3872            page_count: u64::from_le_bytes(entry[8..16].try_into().expect("slice length checked")),
3873        })
3874        .collect())
3875}
3876
3877fn parse_ewf1_ltree_data(data: &[u8]) -> Result<SingleFilesInfo> {
3878    if data.len() < EWF1_LTREE_HEADER_SIZE {
3879        return Err(EwfError::Malformed(
3880            "EWF1 ltree section is too short".into(),
3881        ));
3882    }
3883
3884    let single_files_data_size =
3885        u64::from_le_bytes(data[16..24].try_into().expect("ltree header size checked"));
3886    let single_files_data_size = usize::try_from(single_files_data_size)
3887        .map_err(|_| EwfError::Malformed("EWF1 ltree data size does not fit usize".into()))?;
3888    let single_files_data_end = EWF1_LTREE_HEADER_SIZE
3889        .checked_add(single_files_data_size)
3890        .ok_or_else(|| EwfError::Malformed("EWF1 ltree data size overflow".into()))?;
3891    if single_files_data_end > data.len() {
3892        return Err(EwfError::Malformed(
3893            "EWF1 ltree data size exceeds section".into(),
3894        ));
3895    }
3896
3897    let stored = u32::from_le_bytes(data[24..28].try_into().expect("ltree header size checked"));
3898    let mut header = data[..EWF1_LTREE_HEADER_SIZE].to_vec();
3899    header[24..28].fill(0);
3900    validate_adler32_checksum_value(stored, &header, "EWF1 ltree header")?;
3901
3902    let text = &data[EWF1_LTREE_HEADER_SIZE..single_files_data_end];
3903    let computed = md5::Md5::digest(text);
3904    if computed[..] != data[..16] {
3905        return Err(EwfError::Malformed("EWF1 ltree text MD5 mismatch".into()));
3906    }
3907    parse_ewf1_single_files_data(text)
3908}
3909
3910fn parse_ewf2_single_files_aux_u64_table(data: &[u8], label: &str) -> Result<Vec<u64>> {
3911    parse_ewf2_single_files_aux_table(data, 8, label, |entry| {
3912        u64::from_le_bytes(entry.try_into().expect("entry size checked"))
3913    })
3914}
3915
3916fn parse_ewf2_single_files_md5_hash_table(data: &[u8]) -> Result<Vec<[u8; 16]>> {
3917    parse_ewf2_single_files_aux_table(data, 16, "EWF2 single files MD5 hash table", |entry| {
3918        entry.try_into().expect("entry size checked")
3919    })
3920}
3921
3922fn parse_ewf2_single_files_aux_table<T>(
3923    data: &[u8],
3924    entry_size: usize,
3925    label: &str,
3926    parse_entry: impl Fn(&[u8]) -> T,
3927) -> Result<Vec<T>> {
3928    const PADDED_HEADER_SIZE: usize = 32;
3929    const FOOTER_SIZE: usize = 4;
3930
3931    if data.len() < PADDED_HEADER_SIZE + FOOTER_SIZE {
3932        return Err(EwfError::Malformed(format!("{label} is too short")));
3933    }
3934
3935    validate_present_adler32_checksum(data, 16, 16, &format!("{label} header"))?;
3936    let entry_count = u32::from_le_bytes(data[0..4].try_into().expect("slice length checked"));
3937    let entry_bytes = usize::try_from(entry_count)
3938        .map_err(|_| EwfError::Malformed(format!("{label} entry count does not fit usize")))?
3939        .checked_mul(entry_size)
3940        .ok_or_else(|| EwfError::Malformed(format!("{label} entry bytes overflow")))?;
3941    let entries_offset = PADDED_HEADER_SIZE;
3942    let entries_end = entries_offset
3943        .checked_add(entry_bytes)
3944        .ok_or_else(|| EwfError::Malformed(format!("{label} entry range overflow")))?;
3945    let footer_end = entries_end
3946        .checked_add(FOOTER_SIZE)
3947        .ok_or_else(|| EwfError::Malformed(format!("{label} footer range overflow")))?;
3948    if footer_end > data.len() {
3949        return Err(EwfError::Malformed(format!(
3950            "{label} entries exceed section"
3951        )));
3952    }
3953
3954    let stored = u32::from_le_bytes(
3955        data[entries_end..entries_end + FOOTER_SIZE]
3956            .try_into()
3957            .expect("footer range checked"),
3958    );
3959    if stored != 0 {
3960        validate_adler32_checksum_value(stored, &data[entries_offset..entries_end], label)?;
3961    }
3962    Ok(data[entries_offset..entries_end]
3963        .chunks_exact(entry_size)
3964        .map(parse_entry)
3965        .collect())
3966}
3967
3968fn parse_ewf1_ranges(
3969    file: &mut dyn SegmentReader,
3970    sections: &[Section],
3971    segment_index: usize,
3972    first_chunk: u64,
3973    logical_size: u64,
3974    profile: Ewf1RangeProfile,
3975    statistics: &ReaderStatisticsCollector,
3976) -> Result<Vec<TableRange>> {
3977    let mut ranges = Vec::new();
3978    let mut next_chunk = first_chunk;
3979    let mut previous_table: Option<(bool, u32, u64)> = None;
3980    for section in sections
3981        .iter()
3982        .filter(|section| matches!(section.desc.section_type.as_str(), "table" | "table2"))
3983    {
3984        if section.data_size < 24 {
3985            return Err(EwfError::Malformed(
3986                "EWF1 table section is too short".into(),
3987            ));
3988        }
3989        let data = read_exact_at(file, section.data_offset, 24)?;
3990        validate_present_adler32_checksum(&data, 20, 20, "EWF1 table header")?;
3991        let entry_count = u32::from_le_bytes(data[0..4].try_into().expect("slice length checked"));
3992        let base_offset = u64::from_le_bytes(data[8..16].try_into().expect("slice length checked"));
3993        if entry_count == 0 {
3994            continue;
3995        }
3996        if section.desc.section_type == "table2"
3997            && previous_table.is_some_and(|(previous_was_table, previous_count, previous_base)| {
3998                previous_was_table && previous_count == entry_count && previous_base == base_offset
3999            })
4000        {
4001            continue;
4002        }
4003
4004        let entry_bytes = u64::from(entry_count)
4005            .checked_mul(4)
4006            .ok_or_else(|| EwfError::Malformed("EWF1 table entry bytes overflow".into()))?;
4007        let entries_offset = section
4008            .data_offset
4009            .checked_add(24)
4010            .ok_or_else(|| EwfError::Malformed("EWF1 table entry offset overflow".into()))?;
4011        let entries_end = entries_offset
4012            .checked_add(entry_bytes)
4013            .ok_or_else(|| EwfError::Malformed("EWF1 table entry range overflow".into()))?;
4014        let section_end = section
4015            .data_offset
4016            .checked_add(section.data_size)
4017            .ok_or_else(|| EwfError::Malformed("EWF1 table section range overflow".into()))?;
4018        if entries_end > section_end {
4019            return Err(EwfError::Malformed(
4020                "EWF1 table entries exceed section".into(),
4021            ));
4022        }
4023        let first_entry_offset = if base_offset == 0 {
4024            let raw = read_exact_at(file, entries_offset, 4)?;
4025            Some(u64::from(
4026                u32::from_le_bytes(raw.try_into().expect("first table entry read size checked"))
4027                    & 0x7fff_ffff,
4028            ))
4029        } else {
4030            None
4031        };
4032        let table_resident_without_entries_checksum =
4033            first_entry_offset.is_some_and(|offset| offset == entries_end);
4034        if entries_end
4035            .checked_add(4)
4036            .is_some_and(|footer_end| footer_end <= section_end)
4037            && !table_resident_without_entries_checksum
4038        {
4039            validate_present_table_entries_checksum(
4040                file,
4041                entries_offset,
4042                entry_bytes,
4043                entries_end,
4044                "EWF1 table entries",
4045                statistics,
4046            )?;
4047        }
4048
4049        let (data_base, data_end) = if let Some(sectors) =
4050            matching_sectors_section(sections, base_offset).or_else(|| {
4051                first_entry_offset
4052                    .and_then(|offset| sectors_section_containing_offset(sections, offset))
4053            }) {
4054            let data_end = sectors
4055                .data_offset
4056                .checked_add(sectors.data_size)
4057                .ok_or_else(|| EwfError::Malformed("EWF1 sectors range overflow".into()))?;
4058            (base_offset, data_end)
4059        } else {
4060            let resident_data_start = if table_resident_without_entries_checksum {
4061                entries_end
4062            } else {
4063                entries_end.checked_add(4).ok_or_else(|| {
4064                    EwfError::Malformed("EWF1 table-resident data offset overflow".into())
4065                })?
4066            };
4067            if resident_data_start > section_end {
4068                return Err(EwfError::Malformed(
4069                    "EWF1 table-resident data starts beyond table section".into(),
4070                ));
4071            }
4072            (0, section_end)
4073        };
4074
4075        ranges.push(TableRange {
4076            kind: TableRangeKind::Ewf1,
4077            segment_index,
4078            first_chunk: next_chunk,
4079            chunk_count: u64::from(entry_count),
4080            entries_offset,
4081            base_offset: data_base,
4082            data_end: Some(data_end),
4083            ewf1_allow_large_compressed_chunks: profile.allow_large_compressed_chunks,
4084            ewf1_compression_method: Some(profile.compression_method),
4085            ewf2_compression_method: None,
4086        });
4087        next_chunk = next_chunk
4088            .checked_add(u64::from(entry_count))
4089            .ok_or_else(|| EwfError::Malformed("EWF1 table chunk count overflow".into()))?;
4090        previous_table = Some((
4091            section.desc.section_type == "table",
4092            entry_count,
4093            base_offset,
4094        ));
4095    }
4096
4097    if ranges.is_empty() && logical_size > 0 {
4098        return Err(EwfError::Malformed("EWF1 table coverage is missing".into()));
4099    }
4100    Ok(ranges)
4101}
4102
4103fn matching_sectors_section(sections: &[Section], base_offset: u64) -> Option<&Section> {
4104    sections
4105        .iter()
4106        .filter(|section| section.desc.section_type == "sectors")
4107        .find(|section| {
4108            base_offset == section.desc.offset
4109                || sectors_section_contains_offset(section, base_offset)
4110        })
4111}
4112
4113fn sectors_section_containing_offset(sections: &[Section], offset: u64) -> Option<&Section> {
4114    sections
4115        .iter()
4116        .filter(|section| section.desc.section_type == "sectors")
4117        .find(|section| sectors_section_contains_offset(section, offset))
4118}
4119
4120fn sectors_section_contains_offset(section: &Section, offset: u64) -> bool {
4121    section
4122        .data_offset
4123        .checked_add(section.data_size)
4124        .is_some_and(|data_end| offset >= section.data_offset && offset <= data_end)
4125}
4126
4127fn parse_ewf2_ranges(
4128    file: &mut dyn SegmentReader,
4129    sections: &[Ewf2Section],
4130    segment_index: usize,
4131    logical_size: u64,
4132    compression_method: ewf2::CompressionMethod,
4133    statistics: &ReaderStatisticsCollector,
4134) -> Result<Vec<TableRange>> {
4135    let mut ranges = Vec::new();
4136    for section in sections
4137        .iter()
4138        .filter(|section| section.desc.section_type == ewf2::SectionType::SectorTable)
4139    {
4140        let minimum_table_header_size = match section.layout {
4141            Ewf2SectionLayout::LeadingDescriptor => ewf2::TABLE_HEADER_SIZE as u64,
4142            Ewf2SectionLayout::TrailingDescriptor => EWF2_TABLE_HEADER_V2_SIZE,
4143        };
4144        if section.data_size < minimum_table_header_size {
4145            return Err(EwfError::Malformed("EWF2 sector table is too short".into()));
4146        }
4147        let header_read_size = if section.data_size >= EWF2_TABLE_HEADER_V2_SIZE {
4148            EWF2_TABLE_HEADER_V2_SIZE
4149        } else {
4150            minimum_table_header_size
4151        };
4152        let data = read_exact_at(file, section.data_offset, header_read_size)?;
4153        validate_present_adler32_checksum(&data, 16, 16, "EWF2 table header")?;
4154        let header = ewf2::TableHeader::parse(&data[..ewf2::TABLE_HEADER_SIZE])?;
4155        let entry_count = u64::from(header.entry_count);
4156        if entry_count == 0 {
4157            continue;
4158        }
4159        let entries_bytes = entry_count
4160            .checked_mul(ewf2::TABLE_ENTRY_SIZE as u64)
4161            .ok_or_else(|| EwfError::Malformed("EWF2 table entry bytes overflow".into()))?;
4162        let table_header_and_padding_size = match section.layout {
4163            Ewf2SectionLayout::TrailingDescriptor => EWF2_TABLE_HEADER_V2_SIZE,
4164            Ewf2SectionLayout::LeadingDescriptor => {
4165                let full_header_entries_size = EWF2_TABLE_HEADER_V2_SIZE
4166                    .checked_add(entries_bytes)
4167                    .ok_or_else(|| EwfError::Malformed("EWF2 table size overflow".into()))?;
4168                if section.data_size >= full_header_entries_size {
4169                    EWF2_TABLE_HEADER_V2_SIZE
4170                } else {
4171                    ewf2::TABLE_HEADER_SIZE as u64
4172                }
4173            }
4174        };
4175        let entries_offset = section
4176            .data_offset
4177            .checked_add(table_header_and_padding_size)
4178            .ok_or_else(|| EwfError::Malformed("EWF2 table entry offset overflow".into()))?;
4179        let entries_end = entries_offset
4180            .checked_add(entries_bytes)
4181            .ok_or_else(|| EwfError::Malformed("EWF2 table entry range overflow".into()))?;
4182        let section_end = section
4183            .data_offset
4184            .checked_add(section.data_size)
4185            .ok_or_else(|| EwfError::Malformed("EWF2 table section range overflow".into()))?;
4186        if entries_end > section_end {
4187            return Err(EwfError::Malformed(
4188                "EWF2 table entries exceed section".into(),
4189            ));
4190        }
4191        if entries_end
4192            .checked_add(EWF2_TABLE_FOOTER_SIZE)
4193            .is_some_and(|footer_end| footer_end <= section_end)
4194        {
4195            validate_present_table_entries_checksum(
4196                file,
4197                entries_offset,
4198                entries_bytes,
4199                entries_end,
4200                "EWF2 table entries",
4201                statistics,
4202            )?;
4203        }
4204
4205        ranges.push(TableRange {
4206            kind: TableRangeKind::Ewf2,
4207            segment_index,
4208            first_chunk: header.first_chunk,
4209            chunk_count: entry_count,
4210            entries_offset,
4211            base_offset: 0,
4212            data_end: None,
4213            ewf1_allow_large_compressed_chunks: false,
4214            ewf1_compression_method: None,
4215            ewf2_compression_method: Some(compression_method_code(compression_method)),
4216        });
4217    }
4218
4219    if ranges.is_empty() && logical_size > 0 {
4220        return Err(EwfError::Malformed("EWF2 image has no sector table".into()));
4221    }
4222    Ok(ranges)
4223}
4224
4225fn read_exact_at(file: &mut dyn SegmentReader, offset: u64, size: u64) -> Result<Vec<u8>> {
4226    let mut data = vec![
4227        0;
4228        usize::try_from(size).map_err(|_| {
4229            EwfError::Malformed("read size does not fit usize".into())
4230        })?
4231    ];
4232    file.seek(SeekFrom::Start(offset))?;
4233    file.read_exact(&mut data)?;
4234    Ok(data)
4235}
4236
4237#[cfg(unix)]
4238fn read_file_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
4239    std::os::unix::fs::FileExt::read_at(file, buffer, offset)
4240}
4241
4242#[cfg(windows)]
4243fn read_file_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
4244    std::os::windows::fs::FileExt::seek_read(file, buffer, offset)
4245}
4246
4247#[cfg(not(any(unix, windows)))]
4248fn read_file_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
4249    let mut file = file.try_clone()?;
4250    file.seek(SeekFrom::Start(offset))?;
4251    file.read(buffer)
4252}
4253
4254fn read_file_exact_at(file: &File, buffer: &mut [u8], mut offset: u64) -> io::Result<()> {
4255    let mut remaining = buffer;
4256    while !remaining.is_empty() {
4257        match read_file_at(file, remaining, offset) {
4258            Ok(0) => {
4259                return Err(io::Error::new(
4260                    io::ErrorKind::UnexpectedEof,
4261                    "segment file ended before requested range",
4262                ));
4263            }
4264            Ok(count) => {
4265                offset = offset.saturating_add(count as u64);
4266                remaining = &mut remaining[count..];
4267            }
4268            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
4269            Err(error) => return Err(error),
4270        }
4271    }
4272    Ok(())
4273}
4274
4275fn table_entry_offset(range: &TableRange, local_index: u64, entry_size: u64) -> Result<u64> {
4276    range
4277        .entries_offset
4278        .checked_add(
4279            local_index
4280                .checked_mul(entry_size)
4281                .ok_or_else(|| EwfError::Malformed("table entry offset overflow".into()))?,
4282        )
4283        .ok_or_else(|| EwfError::Malformed("table entry offset overflow".into()))
4284}
4285
4286fn validate_ewf1_encoded_size(
4287    encoded_size: u64,
4288    chunk_size: u64,
4289    encoding: ChunkEncoding,
4290    allow_large_compressed_chunks: bool,
4291) -> Result<()> {
4292    if encoding == ChunkEncoding::Zlib {
4293        let standard_maximum = zlib_compressed_chunk_size_cap(chunk_size)?;
4294        if !allow_large_compressed_chunks && encoded_size <= standard_maximum {
4295            return validate_encoded_size(encoded_size, chunk_size, encoding);
4296        }
4297        if encoded_size == 0 {
4298            return Err(EwfError::Malformed("chunk data size is zero".into()));
4299        }
4300        let maximum = chunk_size
4301            .checked_mul(2)
4302            .ok_or_else(|| EwfError::Malformed("EWF1 chunk size cap overflow".into()))?;
4303        if encoded_size > maximum {
4304            return Err(EwfError::Malformed(format!(
4305                "EWF1 compressed chunk size {encoded_size} exceeds maximum {maximum}"
4306            )));
4307        }
4308        Ok(())
4309    } else {
4310        validate_encoded_size(encoded_size, chunk_size, encoding)
4311    }
4312}
4313
4314fn ewf1_chunk_encoding(
4315    entry_compressed: bool,
4316    encoded_size: u64,
4317    chunk_size: u64,
4318    compression_method: CompressionMethod,
4319) -> Result<ChunkEncoding> {
4320    if entry_compressed {
4321        return match compression_method {
4322            CompressionMethod::Zlib => Ok(ChunkEncoding::Zlib),
4323            CompressionMethod::Zstd => Ok(ChunkEncoding::Zstd),
4324            method => Err(EwfError::Unsupported(format!(
4325                "unsupported EWF1 compression method {method:?}"
4326            ))),
4327        };
4328    }
4329    if compression_method == CompressionMethod::Zlib
4330        && encoded_size > raw_chunk_size_cap(chunk_size)?
4331        && encoded_size <= zlib_compressed_chunk_size_cap(chunk_size)?
4332    {
4333        return Ok(ChunkEncoding::Zlib);
4334    }
4335    Ok(ChunkEncoding::Raw)
4336}
4337
4338fn decode_ewf1_entry(
4339    range: &TableRange,
4340    raw: u32,
4341    chunk_size: u64,
4342    next_raw: Option<u32>,
4343    is_final: bool,
4344) -> Result<Ewf1DecodedEntry> {
4345    let entry = ewf1::TableEntry::parse(&raw.to_le_bytes())?;
4346    let masked_offset = range
4347        .base_offset
4348        .checked_add(entry.offset)
4349        .ok_or_else(|| EwfError::Malformed("EWF1 chunk offset overflow".into()))?;
4350
4351    if should_use_full_width_ewf1_offset(range, raw, masked_offset, chunk_size, next_raw, is_final)?
4352    {
4353        return Ok(Ewf1DecodedEntry {
4354            compressed: false,
4355            offset: range
4356                .base_offset
4357                .checked_add(u64::from(raw))
4358                .ok_or_else(|| EwfError::Malformed("EWF1 chunk offset overflow".into()))?,
4359        });
4360    }
4361
4362    Ok(Ewf1DecodedEntry {
4363        compressed: entry.compressed,
4364        offset: masked_offset,
4365    })
4366}
4367
4368fn should_use_full_width_ewf1_offset(
4369    range: &TableRange,
4370    raw: u32,
4371    masked_offset: u64,
4372    chunk_size: u64,
4373    next_raw: Option<u32>,
4374    is_final: bool,
4375) -> Result<bool> {
4376    if raw & 0x8000_0000 == 0 {
4377        return Ok(false);
4378    }
4379    let Some(data_end) = range.data_end else {
4380        return Ok(false);
4381    };
4382    let data_len = data_end
4383        .checked_sub(range.base_offset)
4384        .ok_or_else(|| EwfError::Malformed("EWF1 data region precedes base offset".into()))?;
4385    if data_len <= 0x8000_0000 {
4386        return Ok(false);
4387    }
4388
4389    let full_offset = range
4390        .base_offset
4391        .checked_add(u64::from(raw))
4392        .ok_or_else(|| EwfError::Malformed("EWF1 chunk offset overflow".into()))?;
4393    if full_offset >= data_end {
4394        return Ok(false);
4395    }
4396
4397    if let Some(next_raw) = next_raw {
4398        let next_masked = range
4399            .base_offset
4400            .checked_add(u64::from(next_raw & 0x7fff_ffff))
4401            .ok_or_else(|| EwfError::Malformed("EWF1 next chunk offset overflow".into()))?;
4402        let masked_is_valid = next_masked > masked_offset
4403            && next_masked - masked_offset <= zlib_compressed_chunk_size_cap(chunk_size)?;
4404        if masked_is_valid {
4405            return Ok(false);
4406        }
4407
4408        let next_full = if next_raw & 0x8000_0000 != 0 {
4409            range
4410                .base_offset
4411                .checked_add(u64::from(next_raw))
4412                .ok_or_else(|| EwfError::Malformed("EWF1 next chunk offset overflow".into()))?
4413        } else {
4414            next_masked
4415        };
4416        return Ok(
4417            next_full > full_offset && next_full - full_offset <= raw_chunk_size_cap(chunk_size)?
4418        );
4419    }
4420
4421    if is_final {
4422        let masked_size = data_end.saturating_sub(masked_offset);
4423        let full_size = data_end - full_offset;
4424        return Ok(masked_size > zlib_compressed_chunk_size_cap(chunk_size)?
4425            && full_size <= raw_chunk_size_cap(chunk_size)?);
4426    }
4427
4428    Ok(false)
4429}
4430
4431fn logical_chunk_size(logical_size: u64, chunk_size: u64, chunk_id: u64) -> Result<usize> {
4432    let logical_offset = chunk_id
4433        .checked_mul(chunk_size)
4434        .ok_or_else(|| EwfError::Malformed("logical chunk offset overflow".into()))?;
4435    let size = logical_size.saturating_sub(logical_offset).min(chunk_size);
4436    usize::try_from(size)
4437        .map_err(|_| EwfError::Malformed("logical chunk size does not fit usize".into()))
4438}
4439
4440fn checksum_error_range(
4441    info: &ImageInfo,
4442    chunk_id: u64,
4443    logical_size: usize,
4444) -> Result<SectorRange> {
4445    let logical_offset = chunk_id
4446        .checked_mul(info.chunk_size)
4447        .ok_or_else(|| EwfError::Malformed("checksum error logical offset overflow".into()))?;
4448    let logical_size = u64::try_from(logical_size)
4449        .map_err(|_| EwfError::Malformed("checksum error size does not fit u64".into()))?;
4450    let Some(bytes_per_sector) = info.media.bytes_per_sector.filter(|value| *value != 0) else {
4451        return Ok(SectorRange {
4452            first_sector: logical_offset,
4453            sector_count: logical_size.max(1),
4454        });
4455    };
4456
4457    Ok(SectorRange {
4458        first_sector: logical_offset / bytes_per_sector,
4459        sector_count: logical_size.div_ceil(bytes_per_sector).max(1),
4460    })
4461}
4462
4463fn validate_chunk_size(chunk_size: u64) -> Result<()> {
4464    if chunk_size == 0 {
4465        return Err(EwfError::Malformed("chunk size is zero".into()));
4466    }
4467    if chunk_size > MAX_CHUNK_SIZE {
4468        return Err(EwfError::Malformed(format!(
4469            "chunk size {chunk_size} exceeds maximum {MAX_CHUNK_SIZE}"
4470        )));
4471    }
4472    Ok(())
4473}
4474
4475fn apply_ewf2_geometry(media: &mut MediaInfo, geometry: Ewf2Geometry) {
4476    if let Some(value) = geometry.sectors_per_chunk {
4477        media.sectors_per_chunk = Some(value);
4478    }
4479    if let Some(value) = geometry.bytes_per_sector {
4480        media.bytes_per_sector = Some(value);
4481    }
4482    if let Some(value) = geometry.sector_count {
4483        media.sector_count = Some(value);
4484    }
4485    if let Some(value) = geometry.chunk_count {
4486        media.chunk_count = Some(value);
4487    }
4488    if let Some(value) = geometry.error_granularity {
4489        media.error_granularity = Some(value);
4490    }
4491    if let Some(value) = geometry.media_type {
4492        media.media_type = Some(value);
4493    }
4494    if let Some(value) = geometry.physical {
4495        media.media_flags.physical = value;
4496    }
4497    media.media_flags.fastbloc |= geometry.fastbloc;
4498    media.media_flags.tableau |= geometry.tableau;
4499}
4500
4501fn apply_ewf2_geometry_if_missing(media: &mut MediaInfo, geometry: Ewf2Geometry) {
4502    if media.sectors_per_chunk.is_none() {
4503        media.sectors_per_chunk = geometry.sectors_per_chunk;
4504    }
4505    if media.bytes_per_sector.is_none() {
4506        media.bytes_per_sector = geometry.bytes_per_sector;
4507    }
4508    if media.sector_count.is_none() {
4509        media.sector_count = geometry.sector_count;
4510    }
4511    if media.chunk_count.is_none() {
4512        media.chunk_count = geometry.chunk_count;
4513    }
4514    if media.error_granularity.is_none() {
4515        media.error_granularity = geometry.error_granularity;
4516    }
4517    if media.media_type.is_none() {
4518        media.media_type = geometry.media_type;
4519    }
4520    if let Some(value) = geometry.physical {
4521        media.media_flags.physical = value;
4522    }
4523    media.media_flags.fastbloc |= geometry.fastbloc;
4524    media.media_flags.tableau |= geometry.tableau;
4525}
4526
4527fn public_compression_method(method: ewf2::CompressionMethod) -> CompressionMethod {
4528    match method {
4529        ewf2::CompressionMethod::None => CompressionMethod::None,
4530        ewf2::CompressionMethod::Zlib => CompressionMethod::Zlib,
4531        ewf2::CompressionMethod::Bzip2 => CompressionMethod::Bzip2,
4532        ewf2::CompressionMethod::Unknown(method) => CompressionMethod::Unknown(method),
4533    }
4534}
4535
4536fn compression_method_code(method: ewf2::CompressionMethod) -> u16 {
4537    match method {
4538        ewf2::CompressionMethod::None => 0,
4539        ewf2::CompressionMethod::Zlib => 1,
4540        ewf2::CompressionMethod::Bzip2 => 2,
4541        ewf2::CompressionMethod::Unknown(method) => method,
4542    }
4543}
4544
4545fn merge_hashes(target: &mut StoredHashes, source: &StoredHashes) -> Result<()> {
4546    if let Some(hash) = source.md5 {
4547        insert_hash_value(target, "MD5", &hash)?;
4548    }
4549    if let Some(hash) = source.sha1 {
4550        insert_hash_value(target, "SHA1", &hash)?;
4551    }
4552    for (identifier, value) in &source.hash_values {
4553        crate::hashes::insert_stored_hash(target, identifier, value)?;
4554    }
4555    Ok(())
4556}
4557
4558fn merge_segment_format_profile(
4559    target: &mut Option<FormatProfile>,
4560    target_hint_only: &mut bool,
4561    source: FormatProfile,
4562    source_hint_only: bool,
4563) -> Result<()> {
4564    if source == FormatProfile::Unknown {
4565        return Ok(());
4566    }
4567    match target {
4568        Some(existing) if *existing == FormatProfile::Unknown => {
4569            *existing = source;
4570            *target_hint_only = source_hint_only;
4571        }
4572        Some(existing) if *existing == source => {
4573            if !source_hint_only {
4574                *target_hint_only = false;
4575            }
4576        }
4577        Some(existing)
4578            if *target_hint_only
4579                && !source_hint_only
4580                && ewf1_profile_hint_matches_detected_profile(*existing, source) =>
4581        {
4582            *existing = source;
4583            *target_hint_only = false;
4584        }
4585        Some(existing)
4586            if !*target_hint_only
4587                && source_hint_only
4588                && ewf1_profile_hint_matches_detected_profile(source, *existing) => {}
4589        Some(existing) if *existing != source => {
4590            return Err(EwfError::Malformed(format!(
4591                "segment format profiles do not match ({existing:?} != {source:?})",
4592            )));
4593        }
4594        Some(_) => {}
4595        None => {
4596            *target = Some(source);
4597            *target_hint_only = source_hint_only;
4598        }
4599    }
4600    Ok(())
4601}
4602
4603fn ewf1_profile_hint_matches_detected_profile(
4604    hint: FormatProfile,
4605    detected: FormatProfile,
4606) -> bool {
4607    matches!(
4608        hint,
4609        FormatProfile::EnCase2 | FormatProfile::EnCase5 | FormatProfile::Ewf
4610    ) && matches!(
4611        detected,
4612        FormatProfile::EnCase1
4613            | FormatProfile::EnCase2
4614            | FormatProfile::EnCase3
4615            | FormatProfile::EnCase4
4616            | FormatProfile::EnCase5
4617            | FormatProfile::EnCase6
4618            | FormatProfile::EnCase7
4619            | FormatProfile::FtkImager
4620            | FormatProfile::Linen5
4621            | FormatProfile::Linen6
4622            | FormatProfile::Linen7
4623    )
4624}
4625
4626fn apply_detected_ewf1_format_profile(
4627    target: &mut FormatProfile,
4628    detected: Option<FormatProfile>,
4629) -> bool {
4630    let Some(detected) = detected.filter(|profile| *profile != FormatProfile::Unknown) else {
4631        return false;
4632    };
4633    if *target == FormatProfile::Smart && detected == FormatProfile::FtkImager {
4634        *target = detected;
4635        return true;
4636    }
4637    if matches!(
4638        target,
4639        FormatProfile::Smart
4640            | FormatProfile::LogicalEnCase5
4641            | FormatProfile::LogicalEnCase6
4642            | FormatProfile::LogicalEnCase7
4643    ) {
4644        return false;
4645    }
4646    *target = detected;
4647    true
4648}
4649
4650fn merge_acquisition_errors(
4651    target: &mut Vec<AcquisitionError>,
4652    source: Vec<AcquisitionError>,
4653) -> Result<()> {
4654    if source.is_empty() {
4655        return Ok(());
4656    }
4657    target.extend(source);
4658    target.sort_unstable_by_key(|range| range.first_sector);
4659    let mut merged: Vec<AcquisitionError> = Vec::with_capacity(target.len());
4660    for range in target.drain(..) {
4661        let end = range
4662            .first_sector
4663            .checked_add(range.sector_count)
4664            .ok_or_else(|| EwfError::Malformed("acquisition error range overflow".into()))?;
4665        if let Some(previous) = merged.last_mut()
4666            && range.first_sector <= previous.first_sector + previous.sector_count
4667        {
4668            previous.sector_count =
4669                end.max(previous.first_sector + previous.sector_count) - previous.first_sector;
4670        } else {
4671            merged.push(range);
4672        }
4673    }
4674    *target = merged;
4675    Ok(())
4676}
4677
4678fn merge_single_files(
4679    target: &mut Option<SingleFilesInfo>,
4680    source: Option<SingleFilesInfo>,
4681) -> Result<()> {
4682    if let Some(source) = source {
4683        if target.is_some() {
4684            return Err(EwfError::Malformed(
4685                "EWF2 image has duplicate single files data sections".into(),
4686            ));
4687        }
4688        *target = Some(source);
4689    }
4690    Ok(())
4691}
4692
4693fn merge_single_files_aux_tables(
4694    target: &mut SingleFilesAuxTables,
4695    source: SingleFilesAuxTables,
4696) -> Result<()> {
4697    merge_single_files_aux_u64_table(
4698        &mut target.table_0x21_entries,
4699        source.table_0x21_entries,
4700        "0x21",
4701    )?;
4702    merge_single_files_aux_md5_table(&mut target.md5_hashes, source.md5_hashes)?;
4703    merge_single_files_aux_u64_table(
4704        &mut target.table_0x23_entries,
4705        source.table_0x23_entries,
4706        "0x23",
4707    )
4708}
4709
4710fn merge_single_files_aux_u64_table(
4711    target: &mut Vec<u64>,
4712    source: Vec<u64>,
4713    table_name: &str,
4714) -> Result<()> {
4715    if source.is_empty() {
4716        return Ok(());
4717    }
4718    if !target.is_empty() {
4719        return Err(EwfError::Malformed(format!(
4720            "EWF2 image has duplicate single files {table_name} table sections"
4721        )));
4722    }
4723    *target = source;
4724    Ok(())
4725}
4726
4727fn merge_single_files_aux_md5_table(
4728    target: &mut Vec<[u8; 16]>,
4729    source: Vec<[u8; 16]>,
4730) -> Result<()> {
4731    if source.is_empty() {
4732        return Ok(());
4733    }
4734    if !target.is_empty() {
4735        return Err(EwfError::Malformed(
4736            "EWF2 image has duplicate single files MD5 hash table sections".into(),
4737        ));
4738    }
4739    *target = source;
4740    Ok(())
4741}
4742
4743fn merge_optional_ewf2_string_section(
4744    target: &mut Option<String>,
4745    source: Option<String>,
4746    label: &str,
4747) -> Result<()> {
4748    let Some(source) = source else {
4749        return Ok(());
4750    };
4751    if target.is_some() {
4752        return Err(EwfError::Malformed(format!(
4753            "EWF2 image has duplicate {label} sections"
4754        )));
4755    }
4756    *target = Some(source);
4757    Ok(())
4758}
4759
4760fn merge_optional_ewf2_raw_section(
4761    target: &mut Option<Vec<u8>>,
4762    source: Option<Vec<u8>>,
4763    label: &str,
4764) -> Result<()> {
4765    let Some(source) = source else {
4766        return Ok(());
4767    };
4768    if target.is_some() {
4769        return Err(EwfError::Malformed(format!(
4770            "EWF2 image has duplicate {label} sections"
4771        )));
4772    }
4773    *target = Some(source);
4774    Ok(())
4775}
4776
4777fn parse_nonzero_hash<const N: usize>(data: &[u8]) -> Option<[u8; N]> {
4778    let value = data.get(..N)?;
4779    let mut hash = [0; N];
4780    hash.copy_from_slice(value);
4781    hash.iter().any(|byte| *byte != 0).then_some(hash)
4782}
4783
4784fn validate_adler32_checksum(
4785    data: &[u8],
4786    checksum_offset: usize,
4787    checksum_data_size: usize,
4788    label: &str,
4789) -> Result<()> {
4790    let stored = u32::from_le_bytes(
4791        data[checksum_offset..checksum_offset + 4]
4792            .try_into()
4793            .expect("hash section size checked"),
4794    );
4795    validate_adler32_checksum_value(stored, &data[..checksum_data_size], label)
4796}
4797
4798fn validate_present_adler32_checksum(
4799    data: &[u8],
4800    checksum_offset: usize,
4801    checksum_data_size: usize,
4802    label: &str,
4803) -> Result<()> {
4804    let stored = u32::from_le_bytes(
4805        data[checksum_offset..checksum_offset + 4]
4806            .try_into()
4807            .expect("checksum offset checked"),
4808    );
4809    if stored == 0 {
4810        return Ok(());
4811    }
4812    validate_adler32_checksum_value(stored, &data[..checksum_data_size], label)
4813}
4814
4815fn validate_present_table_entries_checksum(
4816    file: &mut dyn SegmentReader,
4817    entries_offset: u64,
4818    entry_bytes: u64,
4819    checksum_offset: u64,
4820    label: &str,
4821    statistics: &ReaderStatisticsCollector,
4822) -> Result<()> {
4823    let checksum = read_exact_at(file, checksum_offset, 4)?;
4824    let stored = u32::from_le_bytes(
4825        checksum
4826            .as_slice()
4827            .try_into()
4828            .expect("checksum size checked"),
4829    );
4830    if stored == 0 {
4831        return Ok(());
4832    }
4833    let started = statistics.enabled().then(Instant::now);
4834    let calculated = stream_adler32(file, entries_offset, entry_bytes)?;
4835    if let Some(started) = started {
4836        statistics.record_table_checksum(entry_bytes, started.elapsed());
4837    }
4838    if stored != calculated {
4839        return Err(EwfError::Malformed(format!("{label} checksum mismatch")));
4840    }
4841    Ok(())
4842}
4843
4844fn stream_adler32(file: &mut dyn SegmentReader, offset: u64, size: u64) -> Result<u32> {
4845    file.seek(SeekFrom::Start(offset))?;
4846    let mut remaining = size;
4847    let mut checksum = 1_u32;
4848    let mut buffer = vec![0_u8; TABLE_CHECKSUM_BUFFER_SIZE];
4849    while remaining > 0 {
4850        let take = usize::try_from(remaining.min(TABLE_CHECKSUM_BUFFER_SIZE as u64))
4851            .expect("table checksum read is bounded by the buffer");
4852        file.read_exact(&mut buffer[..take])?;
4853        checksum = adler32_update(checksum, &buffer[..take]);
4854        remaining -= u64::try_from(take).expect("usize fits u64");
4855    }
4856    Ok(checksum)
4857}
4858
4859fn validate_present_ewf1_media_checksum(data: &[u8], section_type: &str) -> Result<()> {
4860    let Some(checksum_offset) = data.len().checked_sub(4) else {
4861        return Ok(());
4862    };
4863    validate_present_adler32_checksum(
4864        data,
4865        checksum_offset,
4866        checksum_offset,
4867        &format!("EWF1 {section_type}"),
4868    )
4869}
4870
4871fn validate_adler32_checksum_value(stored: u32, checksum_data: &[u8], label: &str) -> Result<()> {
4872    let calculated = adler32(checksum_data);
4873    if stored != calculated {
4874        return Err(EwfError::Malformed(format!("{label} checksum mismatch")));
4875    }
4876    Ok(())
4877}
4878
4879fn validate_raw_chunk_checksum(encoded: &[u8], logical_size: usize) -> Result<()> {
4880    let checksum_offset = logical_size;
4881    if encoded.len() < checksum_offset + 4 {
4882        return Err(EwfError::Malformed(
4883            "raw chunk checksum trailer is missing".into(),
4884        ));
4885    }
4886    validate_adler32_checksum(encoded, checksum_offset, logical_size, "raw chunk")
4887}
4888
4889fn adler32(data: &[u8]) -> u32 {
4890    adler32_update(1, data)
4891}
4892
4893fn adler32_update(checksum: u32, data: &[u8]) -> u32 {
4894    const MOD_ADLER: u32 = 65_521;
4895    let mut a = checksum & 0xffff;
4896    let mut b = checksum >> 16;
4897    for byte in data {
4898        a = (a + u32::from(*byte)) % MOD_ADLER;
4899        b = (b + a) % MOD_ADLER;
4900    }
4901    (b << 16) | a
4902}
4903
4904fn insert_hash_value(
4905    stored_hashes: &mut StoredHashes,
4906    identifier: &str,
4907    hash: &[u8],
4908) -> Result<()> {
4909    crate::hashes::insert_stored_hash(stored_hashes, identifier, &hex_string(hash))
4910}
4911
4912fn hex_string(bytes: &[u8]) -> String {
4913    const HEX: &[u8; 16] = b"0123456789abcdef";
4914    let mut out = String::with_capacity(bytes.len() * 2);
4915    for byte in bytes {
4916        out.push(char::from(HEX[(byte >> 4) as usize]));
4917        out.push(char::from(HEX[(byte & 0x0f) as usize]));
4918    }
4919    out
4920}
4921
4922fn validate_hash_section_size(observed: u64, expected: u64, label: &str) -> Result<()> {
4923    if observed != expected {
4924        return Err(EwfError::Malformed(format!(
4925            "{label} hash section has size {observed}, expected {expected}"
4926        )));
4927    }
4928    Ok(())
4929}
4930
4931fn validate_set_identifier(
4932    expected: &mut Option<[u8; 16]>,
4933    observed: Option<[u8; 16]>,
4934) -> Result<()> {
4935    let Some(observed) = observed else {
4936        return Ok(());
4937    };
4938    if let Some(expected) = expected {
4939        if *expected != observed {
4940            return Err(EwfError::Malformed(
4941                "segment set identifier mismatch".into(),
4942            ));
4943        }
4944    } else {
4945        *expected = Some(observed);
4946    }
4947    Ok(())
4948}
4949
4950fn validate_ewf2_header_profile(
4951    expected: &mut Option<Ewf2HeaderProfile>,
4952    observed: Option<Ewf2HeaderProfile>,
4953) -> Result<()> {
4954    let Some(observed) = observed else {
4955        return Ok(());
4956    };
4957    if let Some(expected) = expected {
4958        if expected.major_version != observed.major_version
4959            || expected.minor_version != observed.minor_version
4960        {
4961            return Err(EwfError::Malformed(
4962                "EWF2 segment file format version mismatch".into(),
4963            ));
4964        }
4965        if expected.compression_method != observed.compression_method {
4966            return Err(EwfError::Malformed(
4967                "EWF2 segment file compression method mismatch".into(),
4968            ));
4969        }
4970    } else {
4971        *expected = Some(observed);
4972    }
4973    Ok(())
4974}
4975
4976fn ewf1_metadata_payload(data: &[u8], compression_method: CompressionMethod) -> Result<Vec<u8>> {
4977    if compression_method == CompressionMethod::Zstd && data.starts_with(&[0x28, 0xb5, 0x2f, 0xfd])
4978    {
4979        let decoder = ruzstd::decoding::StreamingDecoder::new_with_max_window_size(
4980            data,
4981            MAX_DECOMPRESSED_METADATA,
4982        )
4983        .map_err(|err| {
4984            EwfError::Malformed(format!(
4985                "EWF1 metadata Zstandard decoder initialization failed: {err}"
4986            ))
4987        })?;
4988        return decompress_metadata(decoder, "EWF1");
4989    }
4990
4991    let mut decompressed = Vec::new();
4992    let result = flate2::read::ZlibDecoder::new(data)
4993        .take(MAX_DECOMPRESSED_METADATA + 1)
4994        .read_to_end(&mut decompressed);
4995    if result.is_ok() && decompressed.len() <= MAX_DECOMPRESSED_METADATA as usize {
4996        Ok(decompressed)
4997    } else {
4998        Ok(data.to_vec())
4999    }
5000}
5001
5002fn ewf2_metadata_payload(
5003    data: &[u8],
5004    _compression_method: ewf2::CompressionMethod,
5005) -> Result<Vec<u8>> {
5006    if data.first() == Some(&0x78) {
5007        return decompress_metadata(flate2::read::ZlibDecoder::new(data), "EWF2");
5008    }
5009    if data.starts_with(b"BZh") {
5010        return decompress_metadata(bzip2::read::BzDecoder::new(data), "EWF2");
5011    }
5012    Ok(data.to_vec())
5013}
5014
5015fn decode_ewf2_string_section(
5016    data: &[u8],
5017    compression_method: ewf2::CompressionMethod,
5018    label: &str,
5019) -> Result<String> {
5020    let payload = ewf2_metadata_payload(data, compression_method)?;
5021    if payload.len() % 2 != 0 {
5022        return Err(EwfError::Malformed(format!(
5023            "EWF2 {label} section has odd UTF-16 size"
5024        )));
5025    }
5026
5027    let units: Vec<u16> = payload
5028        .as_chunks::<2>()
5029        .0
5030        .iter()
5031        .map(|chunk| u16::from_le_bytes(*chunk))
5032        .collect();
5033    let text = String::from_utf16(&units)
5034        .map_err(|_| EwfError::Malformed(format!("EWF2 {label} section is not valid UTF-16LE")))?;
5035    Ok(text.strip_prefix('\u{feff}').unwrap_or(&text).to_owned())
5036}
5037
5038fn decompress_metadata(reader: impl Read, format: &str) -> Result<Vec<u8>> {
5039    let mut decompressed = Vec::new();
5040    reader
5041        .take(MAX_DECOMPRESSED_METADATA + 1)
5042        .read_to_end(&mut decompressed)
5043        .map_err(|err| {
5044            EwfError::Malformed(format!("{format} metadata decompression failed: {err}"))
5045        })?;
5046    if decompressed.len() > MAX_DECOMPRESSED_METADATA as usize {
5047        return Err(EwfError::Malformed(format!(
5048            "{format} metadata exceeds decompressed size limit"
5049        )));
5050    }
5051    Ok(decompressed)
5052}
5053
5054#[cfg(test)]
5055mod tests {
5056    use std::io::{Cursor, Read, Seek, SeekFrom, Write};
5057    use std::sync::{
5058        Arc,
5059        atomic::{AtomicUsize, Ordering as AtomicOrdering},
5060    };
5061
5062    use flate2::write::ZlibEncoder;
5063
5064    use super::*;
5065
5066    struct ObservedReader {
5067        cursor: Cursor<Vec<u8>>,
5068        maximum_read: usize,
5069    }
5070
5071    impl Read for ObservedReader {
5072        fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5073            self.maximum_read = self.maximum_read.max(buffer.len());
5074            self.cursor.read(buffer)
5075        }
5076    }
5077
5078    impl Seek for ObservedReader {
5079        fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
5080            self.cursor.seek(position)
5081        }
5082    }
5083
5084    struct LengthObservedReader {
5085        cursor: Cursor<Vec<u8>>,
5086        seek_from_end_calls: Arc<AtomicUsize>,
5087    }
5088
5089    impl Read for LengthObservedReader {
5090        fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5091            self.cursor.read(buffer)
5092        }
5093    }
5094
5095    impl Seek for LengthObservedReader {
5096        fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
5097            if matches!(position, SeekFrom::End(_)) {
5098                self.seek_from_end_calls
5099                    .fetch_add(1, AtomicOrdering::Relaxed);
5100            }
5101            self.cursor.seek(position)
5102        }
5103    }
5104
5105    #[test]
5106    fn segment_file_pool_caches_segment_lengths() {
5107        let seek_from_end_calls = Arc::new(AtomicUsize::new(0));
5108        let reader = LengthObservedReader {
5109            cursor: Cursor::new(vec![0; 128]),
5110            seek_from_end_calls: Arc::clone(&seek_from_end_calls),
5111        };
5112        let mut pool = SegmentFilePool::new_readers(
5113            vec![Box::new(reader)],
5114            None,
5115            Arc::new(ReaderStatisticsCollector::new(false)),
5116        )
5117        .unwrap();
5118        let path = Path::new("cached-length.E01");
5119
5120        assert_eq!(pool.segment_len(0, path).unwrap(), 128);
5121        assert_eq!(pool.segment_len(0, path).unwrap(), 128);
5122        assert_eq!(seek_from_end_calls.load(AtomicOrdering::Relaxed), 1);
5123    }
5124
5125    #[test]
5126    fn active_positioned_reads_preserve_the_handle_limit() {
5127        let first = tempfile::NamedTempFile::new().unwrap();
5128        let second = tempfile::NamedTempFile::new().unwrap();
5129        let mut pool =
5130            SegmentFilePool::new_path(2, Some(1), Arc::new(ReaderStatisticsCollector::new(false)))
5131                .unwrap();
5132
5133        let active = pool.acquire_path_file(0, first.path()).unwrap().unwrap();
5134        assert_eq!(pool.open_count(), 1);
5135        assert!(pool.acquire_path_file(1, second.path()).unwrap().is_none());
5136        assert_eq!(pool.open_count(), 1);
5137
5138        drop(active);
5139        assert!(pool.acquire_path_file(1, second.path()).unwrap().is_some());
5140        assert_eq!(pool.open_count(), 1);
5141    }
5142
5143    #[test]
5144    fn table_checksum_validation_uses_bounded_reads() {
5145        const MAXIMUM_CHECKSUM_READ: usize = 64 * 1024;
5146        let entries = vec![0x5a; MAXIMUM_CHECKSUM_READ * 3 + 17];
5147        let mut bytes = entries.clone();
5148        bytes.extend_from_slice(&adler32(&entries).to_le_bytes());
5149        let mut reader = ObservedReader {
5150            cursor: Cursor::new(bytes),
5151            maximum_read: 0,
5152        };
5153
5154        validate_present_table_entries_checksum(
5155            &mut reader,
5156            0,
5157            entries.len() as u64,
5158            entries.len() as u64,
5159            "test table",
5160            &ReaderStatisticsCollector::new(false),
5161        )
5162        .unwrap();
5163
5164        assert!(reader.maximum_read <= MAXIMUM_CHECKSUM_READ);
5165    }
5166
5167    #[test]
5168    fn table_checksum_validation_rejects_mismatch_across_multiple_reads() {
5169        const CHECKSUM_READ_SIZE: usize = 64 * 1024;
5170        let entries = vec![0x3c; CHECKSUM_READ_SIZE * 2 + 7];
5171        let mut bytes = entries.clone();
5172        bytes.extend_from_slice(&adler32(b"different").to_le_bytes());
5173        let mut reader = Cursor::new(bytes);
5174
5175        let error = validate_present_table_entries_checksum(
5176            &mut reader,
5177            0,
5178            entries.len() as u64,
5179            entries.len() as u64,
5180            "test table",
5181            &ReaderStatisticsCollector::new(false),
5182        )
5183        .unwrap_err();
5184
5185        match error {
5186            EwfError::Malformed(message) => {
5187                assert_eq!(message, "test table checksum mismatch");
5188            }
5189            other => panic!("unexpected error: {other}"),
5190        }
5191    }
5192
5193    #[test]
5194    fn ewf1_metadata_payload_decompresses_zlib_data() {
5195        let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
5196        encoder.write_all(b"metadata").unwrap();
5197        let compressed = encoder.finish().unwrap();
5198
5199        assert_eq!(
5200            ewf1_metadata_payload(&compressed, CompressionMethod::Zlib).unwrap(),
5201            b"metadata"
5202        );
5203    }
5204
5205    #[test]
5206    fn ewf1_metadata_payload_keeps_plain_data() {
5207        assert_eq!(
5208            ewf1_metadata_payload(b"plain metadata", CompressionMethod::Zlib).unwrap(),
5209            b"plain metadata"
5210        );
5211    }
5212
5213    #[test]
5214    fn ewf2_metadata_payload_decompresses_zlib_data() {
5215        let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
5216        encoder.write_all(b"metadata").unwrap();
5217        let compressed = encoder.finish().unwrap();
5218
5219        assert_eq!(
5220            ewf2_metadata_payload(&compressed, ewf2::CompressionMethod::Zlib).unwrap(),
5221            b"metadata"
5222        );
5223    }
5224}