Skip to main content

ewf_image/
image.rs

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