Skip to main content

ewf_image/
types.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4
5use crate::date_time::format_header_date_value;
6use crate::decode::{ChunkEncoding, decode_chunk};
7use crate::{EwfError, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10/// Top-level EWF container generation.
11pub enum Format {
12    /// Original EWF/EVF/LVF segment format.
13    Ewf1,
14    /// EWF2/Ex01/Lx01 segment format.
15    Ewf2,
16}
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19/// Producer/profile inferred from EWF metadata and section layout.
20pub enum FormatProfile {
21    /// The image profile could not be identified.
22    #[default]
23    Unknown,
24    /// `EnCase` version 1 style EWF1 image.
25    EnCase1,
26    /// `EnCase` version 2 style EWF1 image.
27    EnCase2,
28    /// `EnCase` version 3 style EWF1 image.
29    EnCase3,
30    /// `EnCase` version 4 style EWF1 image.
31    EnCase4,
32    /// `EnCase` version 5 style EWF1 image.
33    EnCase5,
34    /// `EnCase` version 6 style EWF1 image.
35    EnCase6,
36    /// `EnCase` version 7 style EWF1 image.
37    EnCase7,
38    /// SMART `.S01` style image.
39    Smart,
40    /// FTK Imager style EWF1 image.
41    FtkImager,
42    /// Linen version 5 style EWF1 image.
43    Linen5,
44    /// Linen version 6 style EWF1 image.
45    Linen6,
46    /// Linen version 7 style EWF1 image.
47    Linen7,
48    /// `EnCase` 5 logical `.L01` style image.
49    LogicalEnCase5,
50    /// `EnCase` 6 logical `.L01` style image.
51    LogicalEnCase6,
52    /// `EnCase` 7 logical `.L01` style image.
53    LogicalEnCase7,
54    /// `EnCase` 7 EWF2 physical `.Ex01` style image.
55    Ewf2EnCase7,
56    /// `EnCase` 7 EWF2 logical `.Lx01` style image.
57    Ewf2LogicalEnCase7,
58    /// Generic EWF-compatible image.
59    Ewf,
60    /// EWF extended profile.
61    Ewfx,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[non_exhaustive]
66/// Compression method recorded for stored chunks.
67pub enum CompressionMethod {
68    /// Chunks are stored without compression.
69    None,
70    /// Chunks use zlib compression.
71    Zlib,
72    /// Chunks in X-Ways EWF1 images use Zstandard compression.
73    Zstd,
74    /// Chunks use `BZip2` compression.
75    Bzip2,
76    /// An unrecognized on-disk compression method value.
77    Unknown(u16),
78}
79
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81/// Compression level recorded in EWF metadata.
82pub enum CompressionLevel {
83    /// Producer default compression level.
84    Default,
85    /// No compression.
86    #[default]
87    None,
88    /// Fast compression.
89    Fast,
90    /// Best compression.
91    Best,
92    /// An unrecognized on-disk compression level value.
93    Unknown(i8),
94}
95
96impl CompressionLevel {
97    /// Returns the EWF metadata numeric representation for this level.
98    pub fn as_i8(self) -> i8 {
99        match self {
100            Self::Default => -1,
101            Self::None => 0,
102            Self::Fast => 1,
103            Self::Best => 2,
104            Self::Unknown(value) => value,
105        }
106    }
107
108    /// Converts an EWF metadata numeric compression level.
109    pub fn from_i8(value: i8) -> Self {
110        match value {
111            -1 => Self::Default,
112            0 => Self::None,
113            1 => Self::Fast,
114            2 => Self::Best,
115            value => Self::Unknown(value),
116        }
117    }
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121/// Compression flags recorded in EWF metadata.
122pub struct CompressionFlags {
123    /// Empty-block compression flag.
124    pub empty_block: bool,
125    /// Pattern-fill compression flag.
126    pub pattern_fill: bool,
127    /// Bits not recognized by this crate.
128    pub unknown_bits: u8,
129}
130
131impl CompressionFlags {
132    const EMPTY_BLOCK: u8 = 0x01;
133    const PATTERN_FILL: u8 = 0x10;
134    const KNOWN_BITS: u8 = Self::EMPTY_BLOCK | Self::PATTERN_FILL;
135
136    /// Decodes a raw EWF compression flags byte.
137    pub fn from_bits(bits: u8) -> Self {
138        Self {
139            empty_block: bits & Self::EMPTY_BLOCK != 0,
140            pattern_fill: bits & Self::PATTERN_FILL != 0,
141            unknown_bits: bits & !Self::KNOWN_BITS,
142        }
143    }
144
145    /// Encodes these flags as a raw EWF compression flags byte.
146    pub fn bits(self) -> u8 {
147        let mut bits = self.unknown_bits;
148        if self.empty_block {
149            bits |= Self::EMPTY_BLOCK;
150        }
151        if self.pattern_fill {
152            bits |= Self::PATTERN_FILL;
153        }
154        bits
155    }
156}
157
158#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
159/// Compression level and flags recorded together.
160pub struct CompressionValues {
161    /// Compression level metadata.
162    pub level: CompressionLevel,
163    /// Compression flag metadata.
164    pub flags: CompressionFlags,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168/// Media type recorded for an image.
169pub enum MediaType {
170    /// Removable physical media.
171    Removable,
172    /// Fixed physical media.
173    Fixed,
174    /// Optical media.
175    Optical,
176    /// Logical single-file collection.
177    SingleFiles,
178    /// Memory acquisition.
179    Memory,
180    /// An unrecognized on-disk media type value.
181    Unknown(u8),
182}
183
184#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
185/// Media flags recorded for an image.
186pub struct MediaFlags {
187    /// The image represents physical media.
188    pub physical: bool,
189    /// `FastBloc` acquisition flag.
190    pub fastbloc: bool,
191    /// Tableau acquisition flag.
192    pub tableau: bool,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196/// EWF2 segment file version.
197pub struct SegmentFileVersion {
198    /// Major version number.
199    pub major: u8,
200    /// Minor version number.
201    pub minor: u8,
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Eq)]
205/// Parsed media geometry and storage metadata.
206pub struct MediaInfo {
207    /// Number of sectors represented by each chunk, if present.
208    pub sectors_per_chunk: Option<u64>,
209    /// Bytes in each logical sector, if present.
210    pub bytes_per_sector: Option<u64>,
211    /// Total logical sector count, if present.
212    pub sector_count: Option<u64>,
213    /// Total logical chunk count, if present.
214    pub chunk_count: Option<u64>,
215    /// Error granularity in sectors, if present.
216    pub error_granularity: Option<u64>,
217    /// Segment set identifier, if present.
218    pub set_identifier: Option<[u8; 16]>,
219    /// EWF2 segment file version, if present.
220    pub ewf2_segment_file_version: Option<SegmentFileVersion>,
221    /// Compression method metadata, if present.
222    pub compression_method: Option<CompressionMethod>,
223    /// Compression level and flags metadata.
224    pub compression_values: CompressionValues,
225    /// Media type metadata, if present.
226    pub media_type: Option<MediaType>,
227    /// Media flags metadata.
228    pub media_flags: MediaFlags,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232/// Strictness used while opening an image.
233pub enum OpenStrictness {
234    /// Treat structural inconsistencies as errors.
235    Strict,
236    /// Accept selected recoverable inconsistencies.
237    Lenient,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241/// Options that control how an image is opened and read.
242///
243/// Configure values with the `with_*` builders and inspect them with getters.
244pub struct OpenOptions {
245    /// Structural validation policy.
246    strictness: OpenStrictness,
247    /// Capacity policy for the decoded-chunk cache.
248    chunk_cache_capacity: ChunkCacheCapacity,
249    /// Maximum bytes retained for cached table-entry pages.
250    table_entry_cache_size_bytes: usize,
251    /// Whether cumulative reader statistics are collected.
252    reader_statistics: bool,
253    /// Return zero-filled chunk data when a chunk checksum fails.
254    read_zero_chunk_on_error: bool,
255    /// Codepage used to decode EWF1 header values.
256    header_codepage: HeaderCodepage,
257    /// Date formatting applied when returning header date values.
258    header_values_date_format: HeaderDateFormat,
259    /// Maximum number of simultaneously open segment handles.
260    maximum_open_handles: Option<usize>,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264/// Capacity policy for the decoded-chunk cache.
265pub enum ChunkCacheCapacity {
266    /// Retain at most this many decoded chunks.
267    Chunks(usize),
268    /// Derive the retained chunk count from this byte target.
269    Bytes(usize),
270}
271
272impl Default for OpenOptions {
273    fn default() -> Self {
274        Self {
275            strictness: OpenStrictness::Strict,
276            chunk_cache_capacity: ChunkCacheCapacity::Chunks(64),
277            table_entry_cache_size_bytes: 4 * 1024 * 1024,
278            reader_statistics: false,
279            read_zero_chunk_on_error: false,
280            header_codepage: HeaderCodepage::Ascii,
281            header_values_date_format: HeaderDateFormat::Ctime,
282            maximum_open_handles: None,
283        }
284    }
285}
286
287impl OpenOptions {
288    /// Returns the structural validation policy.
289    pub fn strictness(&self) -> OpenStrictness {
290        self.strictness
291    }
292
293    /// Sets the structural validation policy.
294    #[must_use]
295    pub fn with_strictness(mut self, strictness: OpenStrictness) -> Self {
296        self.strictness = strictness;
297        self
298    }
299
300    /// Returns the decoded-chunk cache capacity policy.
301    pub fn chunk_cache_capacity(&self) -> ChunkCacheCapacity {
302        self.chunk_cache_capacity
303    }
304
305    /// Configures the decoded-chunk cache by entry count.
306    #[must_use]
307    pub fn with_chunk_cache_size(mut self, chunk_count: usize) -> Self {
308        self.chunk_cache_capacity = ChunkCacheCapacity::Chunks(chunk_count);
309        self
310    }
311
312    /// Configures the decoded-chunk cache by byte target.
313    ///
314    /// At least one decoded chunk is retained even when one chunk exceeds the
315    /// requested byte target.
316    #[must_use]
317    pub fn with_chunk_cache_size_bytes(mut self, bytes: usize) -> Self {
318        self.chunk_cache_capacity = ChunkCacheCapacity::Bytes(bytes);
319        self
320    }
321
322    /// Returns the table-entry page-cache byte limit.
323    pub fn table_entry_cache_size_bytes(&self) -> usize {
324        self.table_entry_cache_size_bytes
325    }
326
327    /// Sets the table-entry page-cache byte limit.
328    ///
329    /// A value of zero disables retention of table-entry pages.
330    #[must_use]
331    pub fn with_table_entry_cache_size_bytes(mut self, bytes: usize) -> Self {
332        self.table_entry_cache_size_bytes = bytes;
333        self
334    }
335
336    /// Returns whether cumulative reader statistics are collected.
337    pub fn reader_statistics_enabled(&self) -> bool {
338        self.reader_statistics
339    }
340
341    /// Enables or disables cumulative reader statistics collection.
342    #[must_use]
343    pub fn with_reader_statistics(mut self, enabled: bool) -> Self {
344        self.reader_statistics = enabled;
345        self
346    }
347
348    /// Returns whether malformed chunks are recovered as zero-filled data.
349    pub fn read_zero_chunk_on_error(&self) -> bool {
350        self.read_zero_chunk_on_error
351    }
352
353    /// Enables or disables malformed-chunk recovery as zero-filled data.
354    #[must_use]
355    pub fn with_read_zero_chunk_on_error(mut self, enabled: bool) -> Self {
356        self.read_zero_chunk_on_error = enabled;
357        self
358    }
359
360    /// Returns the EWF1 header codepage.
361    pub fn header_codepage(&self) -> HeaderCodepage {
362        self.header_codepage
363    }
364
365    /// Sets the EWF1 header codepage.
366    #[must_use]
367    pub fn with_header_codepage(mut self, codepage: HeaderCodepage) -> Self {
368        self.header_codepage = codepage;
369        self
370    }
371
372    /// Returns the header-value date format.
373    pub fn header_values_date_format(&self) -> HeaderDateFormat {
374        self.header_values_date_format
375    }
376
377    /// Sets the header-value date format.
378    #[must_use]
379    pub fn with_header_values_date_format(mut self, format: HeaderDateFormat) -> Self {
380        self.header_values_date_format = format;
381        self
382    }
383
384    /// Returns the maximum number of simultaneously open segment handles.
385    pub fn maximum_open_handles(&self) -> Option<usize> {
386        self.maximum_open_handles
387    }
388
389    /// Sets the maximum number of simultaneously open segment handles.
390    #[must_use]
391    pub fn with_maximum_open_handles(mut self, maximum: Option<usize>) -> Self {
392        self.maximum_open_handles = maximum;
393        self
394    }
395}
396
397#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
398/// Codepage used for EWF1 textual header values.
399pub enum HeaderCodepage {
400    /// ASCII header encoding.
401    #[default]
402    Ascii,
403    /// Windows-874 header encoding.
404    Windows874,
405    /// Windows-932 header encoding.
406    Windows932,
407    /// Windows-936 header encoding.
408    Windows936,
409    /// Windows-1250 header encoding.
410    Windows1250,
411    /// Windows-1251 header encoding.
412    Windows1251,
413    /// Windows-1252 header encoding.
414    Windows1252,
415    /// Windows-1253 header encoding.
416    Windows1253,
417    /// Windows-1254 header encoding.
418    Windows1254,
419    /// Windows-1255 header encoding.
420    Windows1255,
421    /// Windows-1256 header encoding.
422    Windows1256,
423    /// Windows-1257 header encoding.
424    Windows1257,
425    /// Windows-1258 header encoding.
426    Windows1258,
427}
428
429impl HeaderCodepage {
430    /// Returns the EWF numeric codepage identifier.
431    pub fn as_i32(self) -> i32 {
432        match self {
433            Self::Ascii => 20_127,
434            Self::Windows874 => 874,
435            Self::Windows932 => 932,
436            Self::Windows936 => 936,
437            Self::Windows1250 => 1250,
438            Self::Windows1251 => 1251,
439            Self::Windows1252 => 1252,
440            Self::Windows1253 => 1253,
441            Self::Windows1254 => 1254,
442            Self::Windows1255 => 1255,
443            Self::Windows1256 => 1256,
444            Self::Windows1257 => 1257,
445            Self::Windows1258 => 1258,
446        }
447    }
448
449    /// Converts an EWF numeric codepage identifier.
450    pub fn from_i32(value: i32) -> Option<Self> {
451        Some(match value {
452            20_127 => Self::Ascii,
453            874 => Self::Windows874,
454            932 => Self::Windows932,
455            936 => Self::Windows936,
456            1250 => Self::Windows1250,
457            1251 => Self::Windows1251,
458            1252 => Self::Windows1252,
459            1253 => Self::Windows1253,
460            1254 => Self::Windows1254,
461            1255 => Self::Windows1255,
462            1256 => Self::Windows1256,
463            1257 => Self::Windows1257,
464            1258 => Self::Windows1258,
465            _ => return None,
466        })
467    }
468}
469
470#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
471/// Formatting applied when returning parsed EWF header date values.
472pub enum HeaderDateFormat {
473    /// Day-month date order.
474    DayMonth,
475    /// Month-day date order.
476    MonthDay,
477    /// ISO 8601-style date formatting.
478    Iso8601,
479    /// C `ctime`-style date formatting.
480    #[default]
481    Ctime,
482}
483
484impl HeaderDateFormat {
485    /// Returns the numeric date-format identifier used by this crate.
486    pub fn as_i32(self) -> i32 {
487        match self {
488            Self::DayMonth => 1,
489            Self::MonthDay => 2,
490            Self::Iso8601 => 3,
491            Self::Ctime => 4,
492        }
493    }
494
495    /// Converts a numeric date-format identifier used by this crate.
496    pub fn from_i32(value: i32) -> Option<Self> {
497        Some(match value {
498            1 => Self::DayMonth,
499            2 => Self::MonthDay,
500            3 => Self::Iso8601,
501            4 => Self::Ctime,
502            _ => return None,
503        })
504    }
505}
506
507#[derive(Debug, Clone, Default, PartialEq, Eq)]
508/// Parsed case and acquisition metadata.
509pub struct EwfMetadata {
510    /// Case number header value.
511    pub case_number: Option<String>,
512    /// Evidence number header value.
513    pub evidence_number: Option<String>,
514    /// Examiner name header value.
515    pub examiner: Option<String>,
516    /// Case description header value.
517    pub description: Option<String>,
518    /// Notes header value.
519    pub notes: Option<String>,
520    /// Acquisition software name.
521    pub acquisition_software: Option<String>,
522    /// Acquisition software version.
523    pub acquisition_software_version: Option<String>,
524    /// Acquisition operating system version.
525    pub os_version: Option<String>,
526    /// Acquisition date header value.
527    pub acquisition_date: Option<String>,
528    /// System date header value.
529    pub system_date: Option<String>,
530    /// Password header value, when present in metadata.
531    pub password: Option<String>,
532    /// Non-standard or otherwise unmapped header values.
533    pub header_values: BTreeMap<String, String>,
534}
535
536impl EwfMetadata {
537    /// Replaces this metadata with another metadata value.
538    pub fn copy_header_values_from(&mut self, source: &EwfMetadata) {
539        *self = source.clone();
540    }
541
542    /// Returns a header value by its EWF identifier.
543    pub fn header_value(&self, identifier: &str) -> Option<&str> {
544        self.standard_header_value(identifier)
545            .or_else(|| self.header_values.get(identifier).map(String::as_str))
546    }
547
548    /// Returns a header value, applying date formatting for known date fields.
549    pub fn header_value_with_date_format(
550        &self,
551        identifier: &str,
552        date_format: HeaderDateFormat,
553    ) -> Option<Cow<'_, str>> {
554        let value = self.header_value(identifier)?;
555        if matches!(identifier, "acquiry_date" | "system_date") {
556            Some(format_header_date_value(value, date_format))
557        } else {
558            Some(Cow::Borrowed(value))
559        }
560    }
561
562    /// Sets a header value by its EWF identifier and returns the previous value.
563    pub fn set_header_value(
564        &mut self,
565        identifier: &str,
566        value: impl Into<String>,
567    ) -> Option<String> {
568        let previous = self.header_value(identifier).map(str::to_owned);
569        let value = value.into();
570
571        self.header_values.remove(identifier);
572        match identifier {
573            "case_number" => self.case_number = Some(value),
574            "description" => self.description = Some(value),
575            "examiner_name" => self.examiner = Some(value),
576            "evidence_number" => self.evidence_number = Some(value),
577            "notes" => self.notes = Some(value),
578            "acquiry_date" => self.acquisition_date = Some(value),
579            "system_date" => self.system_date = Some(value),
580            "acquiry_operating_system" => self.os_version = Some(value),
581            "acquiry_software" => self.acquisition_software = Some(value),
582            "acquiry_software_version" => self.acquisition_software_version = Some(value),
583            "password" => self.password = Some(value),
584            _ => {
585                self.header_values.insert(identifier.to_string(), value);
586            }
587        }
588
589        previous
590    }
591
592    /// Returns the number of available header values.
593    pub fn number_of_header_values(&self) -> usize {
594        let standard_count = STANDARD_HEADER_VALUE_IDENTIFIERS
595            .iter()
596            .filter(|identifier| self.header_value(identifier).is_some())
597            .count();
598        let generic_count = self
599            .header_values
600            .keys()
601            .filter(|identifier| !is_standard_header_value_identifier(identifier))
602            .count();
603        standard_count + generic_count
604    }
605
606    /// Returns the header value identifier at a stable enumeration index.
607    pub fn header_value_identifier(&self, index: usize) -> Option<&str> {
608        let mut remaining = index;
609        for identifier in STANDARD_HEADER_VALUE_IDENTIFIERS {
610            if self.header_value(identifier).is_some() {
611                if remaining == 0 {
612                    return Some(identifier);
613                }
614                remaining -= 1;
615            }
616        }
617
618        for identifier in self.header_values.keys() {
619            if is_standard_header_value_identifier(identifier) {
620                continue;
621            }
622            if remaining == 0 {
623                return Some(identifier);
624            }
625            remaining -= 1;
626        }
627
628        None
629    }
630
631    fn standard_header_value(&self, identifier: &str) -> Option<&str> {
632        match identifier {
633            "case_number" => self.case_number.as_deref(),
634            "description" => self.description.as_deref(),
635            "examiner_name" => self.examiner.as_deref(),
636            "evidence_number" => self.evidence_number.as_deref(),
637            "notes" => self.notes.as_deref(),
638            "acquiry_date" => self.acquisition_date.as_deref(),
639            "system_date" => self.system_date.as_deref(),
640            "acquiry_operating_system" => self.os_version.as_deref(),
641            "acquiry_software" => self.acquisition_software.as_deref(),
642            "acquiry_software_version" => self.acquisition_software_version.as_deref(),
643            "password" => self.password.as_deref(),
644            _ => None,
645        }
646    }
647}
648
649const STANDARD_HEADER_VALUE_IDENTIFIERS: &[&str] = &[
650    "case_number",
651    "description",
652    "examiner_name",
653    "evidence_number",
654    "notes",
655    "acquiry_date",
656    "system_date",
657    "acquiry_operating_system",
658    "acquiry_software",
659    "acquiry_software_version",
660    "password",
661    "compression_level",
662    "model",
663    "serial_number",
664];
665
666fn is_standard_header_value_identifier(identifier: &str) -> bool {
667    STANDARD_HEADER_VALUE_IDENTIFIERS.contains(&identifier)
668}
669
670#[derive(Debug, Clone, Default, PartialEq, Eq)]
671/// Stored hash values parsed from an image.
672pub struct StoredHashes {
673    /// Parsed MD5 hash bytes, if a valid MD5 value was stored.
674    pub md5: Option<[u8; 16]>,
675    /// Parsed SHA1 hash bytes, if a valid SHA1 value was stored.
676    pub sha1: Option<[u8; 20]>,
677    /// Stored hash strings keyed by hash identifier.
678    pub hash_values: BTreeMap<String, String>,
679}
680
681impl StoredHashes {
682    /// Returns a stored hash value by identifier.
683    pub fn hash_value(&self, identifier: &str) -> Option<&str> {
684        self.hash_values.get(identifier).map(String::as_str)
685    }
686
687    /// Sets a stored hash value and returns the previous string value.
688    pub fn set_hash_value(
689        &mut self,
690        identifier: impl Into<String>,
691        value: impl Into<String>,
692    ) -> Option<String> {
693        let identifier = identifier.into();
694        let value = value.into();
695        set_typed_hash_value(&identifier, &value, &mut self.md5, &mut self.sha1);
696        self.hash_values.insert(identifier, value)
697    }
698
699    /// Returns the number of stored hash values.
700    pub fn number_of_hash_values(&self) -> usize {
701        self.hash_values.len()
702    }
703
704    /// Returns the hash identifier at a stable enumeration index.
705    pub fn hash_value_identifier(&self, index: usize) -> Option<&str> {
706        self.hash_values.keys().nth(index).map(String::as_str)
707    }
708}
709
710pub(crate) fn set_typed_hash_value(
711    identifier: &str,
712    value: &str,
713    md5: &mut Option<[u8; 16]>,
714    sha1: &mut Option<[u8; 20]>,
715) {
716    if identifier.eq_ignore_ascii_case("MD5") {
717        if let Some(parsed) = parse_hex_array(value) {
718            *md5 = Some(parsed);
719        }
720    } else if identifier.eq_ignore_ascii_case("SHA1")
721        && let Some(parsed) = parse_hex_array(value)
722    {
723        *sha1 = Some(parsed);
724    }
725}
726
727pub(crate) fn parse_hex_array<const N: usize>(text: &str) -> Option<[u8; N]> {
728    if text.len() != N * 2 {
729        return None;
730    }
731
732    let mut bytes = [0; N];
733    for (index, pair) in text.as_bytes().as_chunks::<2>().0.iter().enumerate() {
734        let high = hex_nibble(pair[0])?;
735        let low = hex_nibble(pair[1])?;
736        bytes[index] = (high << 4) | low;
737    }
738    Some(bytes)
739}
740
741fn hex_nibble(value: u8) -> Option<u8> {
742    match value {
743        b'0'..=b'9' => Some(value - b'0'),
744        b'a'..=b'f' => Some(value - b'a' + 10),
745        b'A'..=b'F' => Some(value - b'A' + 10),
746        _ => None,
747    }
748}
749
750#[derive(Debug, Clone, PartialEq, Eq)]
751/// Acquisition error range recorded in image metadata.
752pub struct AcquisitionError {
753    /// First sector affected by the acquisition error.
754    pub first_sector: u64,
755    /// Number of sectors affected by the acquisition error.
756    pub sector_count: u64,
757}
758
759#[derive(Debug, Clone, PartialEq, Eq)]
760/// Inclusive start plus count sector range.
761pub struct SectorRange {
762    /// First sector in the range.
763    pub first_sector: u64,
764    /// Number of sectors in the range.
765    pub sector_count: u64,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq)]
769/// Memory acquisition extent recorded in pages.
770pub struct MemoryExtent {
771    /// First memory page in the extent.
772    pub start_page: u64,
773    /// Number of pages in the extent.
774    pub page_count: u64,
775}
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq)]
778#[non_exhaustive]
779/// Encoding used for a data chunk payload.
780pub enum DataChunkEncoding {
781    /// Uncompressed chunk data.
782    Raw,
783    /// zlib-compressed chunk data.
784    Zlib,
785    /// X-Ways EWF1 magicless Zstandard data or its one-byte zero marker.
786    Zstd,
787    /// BZip2-compressed chunk data.
788    Bzip2,
789    /// Pattern-fill chunk data with the repeated pattern value.
790    PatternFill(u64),
791}
792
793#[derive(Debug, Clone, PartialEq, Eq)]
794/// Decoded logical data chunk.
795pub struct DataChunk {
796    /// Zero-based logical chunk index.
797    pub chunk_index: u64,
798    /// Logical byte offset of the chunk.
799    pub logical_offset: u64,
800    /// Logical byte size of the decoded chunk.
801    pub logical_size: usize,
802    /// Encoded byte size as stored in the segment.
803    pub encoded_size: u64,
804    /// Encoding used by the stored chunk.
805    pub encoding: DataChunkEncoding,
806    /// Whether the chunk was returned under a corruption-tolerant read policy.
807    pub corrupted: bool,
808    /// Decoded chunk bytes.
809    pub data: Vec<u8>,
810}
811
812impl DataChunk {
813    /// Returns whether this chunk was marked corrupted while reading.
814    pub fn is_corrupted(&self) -> bool {
815        self.corrupted
816    }
817
818    /// Copies decoded chunk bytes into `buffer`.
819    pub fn read_buffer(&self, buffer: &mut [u8]) -> Result<usize> {
820        Ok(copy_to_buffer(&self.data, buffer))
821    }
822
823    /// Replaces this chunk with raw bytes from `buffer`.
824    ///
825    /// # Errors
826    ///
827    /// Returns an error if the buffer length does not fit in EWF chunk metadata.
828    pub fn write_buffer(&mut self, buffer: &[u8]) -> Result<usize> {
829        self.data.clear();
830        self.data.extend_from_slice(buffer);
831        self.logical_size = buffer.len();
832        self.encoded_size = u64::try_from(buffer.len())
833            .map_err(|_| EwfError::Malformed("data chunk buffer size overflow".into()))?;
834        self.encoding = DataChunkEncoding::Raw;
835        self.corrupted = false;
836        Ok(buffer.len())
837    }
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
841/// Encoded data chunk as stored in an EWF segment.
842pub struct EncodedDataChunk {
843    /// Zero-based logical chunk index.
844    pub chunk_index: u64,
845    /// Logical byte offset of the chunk.
846    pub logical_offset: u64,
847    /// Logical byte size after decoding.
848    pub logical_size: usize,
849    /// Encoded byte size stored in the segment.
850    pub encoded_size: u64,
851    /// Encoding used by the chunk payload.
852    pub encoding: DataChunkEncoding,
853    /// Whether raw chunk data includes a checksum trailer.
854    pub has_checksum: bool,
855    /// Encoded chunk bytes.
856    pub data: Vec<u8>,
857}
858
859impl EncodedDataChunk {
860    /// Decodes this chunk and copies logical bytes into `buffer`.
861    ///
862    /// # Errors
863    ///
864    /// Returns an error if checksum validation or decompression fails.
865    pub fn read_buffer(&self, buffer: &mut [u8]) -> Result<usize> {
866        if self.has_checksum && self.encoding == DataChunkEncoding::Raw {
867            validate_raw_data_chunk_checksum(&self.data, self.logical_size)?;
868        }
869
870        let decoded = decode_chunk(
871            &self.data,
872            data_chunk_encoding(self.encoding),
873            self.logical_size,
874        )?;
875        Ok(copy_to_buffer(&decoded, buffer))
876    }
877}
878
879fn copy_to_buffer(data: &[u8], buffer: &mut [u8]) -> usize {
880    let read_size = data.len().min(buffer.len());
881    buffer[..read_size].copy_from_slice(&data[..read_size]);
882    read_size
883}
884
885fn data_chunk_encoding(encoding: DataChunkEncoding) -> ChunkEncoding {
886    match encoding {
887        DataChunkEncoding::Raw => ChunkEncoding::Raw,
888        DataChunkEncoding::Zlib => ChunkEncoding::Zlib,
889        DataChunkEncoding::Zstd => ChunkEncoding::Zstd,
890        DataChunkEncoding::Bzip2 => ChunkEncoding::Bzip2,
891        DataChunkEncoding::PatternFill(pattern) => ChunkEncoding::PatternFill(pattern),
892    }
893}
894
895fn validate_raw_data_chunk_checksum(encoded: &[u8], logical_size: usize) -> Result<()> {
896    let checksum_end = logical_size
897        .checked_add(4)
898        .ok_or_else(|| EwfError::Malformed("raw chunk checksum offset overflow".into()))?;
899    if encoded.len() < checksum_end {
900        return Err(EwfError::Malformed(
901            "raw chunk checksum trailer is missing".into(),
902        ));
903    }
904
905    let stored = u32::from_le_bytes(
906        encoded[logical_size..checksum_end]
907            .try_into()
908            .expect("raw chunk checksum slice has fixed size"),
909    );
910    let calculated = adler32(&encoded[..logical_size]);
911    if stored != calculated {
912        return Err(EwfError::Malformed("raw chunk checksum mismatch".into()));
913    }
914
915    Ok(())
916}
917
918fn adler32(data: &[u8]) -> u32 {
919    const MOD_ADLER: u32 = 65_521;
920    let mut a = 1_u32;
921    let mut b = 0_u32;
922    for byte in data {
923        a = (a + u32::from(*byte)) % MOD_ADLER;
924        b = (b + a) % MOD_ADLER;
925    }
926    (b << 16) | a
927}
928
929#[derive(Debug, Clone, Copy, PartialEq, Eq)]
930/// Type of a logical single-file catalog entry.
931pub enum SingleFileEntryType {
932    /// Regular file entry.
933    File,
934    /// Directory entry.
935    Directory,
936    /// Entry type not recognized by this crate.
937    Unknown,
938}
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
941/// Data extent for a logical single-file entry.
942pub struct SingleFileExtent {
943    /// Logical media offset where this extent's data starts.
944    pub data_offset: u64,
945    /// Number of bytes represented by this extent.
946    pub data_size: u64,
947    /// Whether the extent is sparse and should read as zeroes.
948    pub sparse: bool,
949}
950
951#[derive(Debug, Clone, Default, PartialEq, Eq)]
952/// Source record for logical single-file metadata.
953pub struct SingleFileSource {
954    /// Source identifier.
955    pub identifier: Option<i32>,
956    /// Source name.
957    pub name: Option<String>,
958    /// Evidence number associated with the source.
959    pub evidence_number: Option<String>,
960    /// Source location.
961    pub location: Option<String>,
962    /// Source device GUID.
963    pub device_guid: Option<String>,
964    /// Primary source device GUID.
965    pub primary_device_guid: Option<String>,
966    /// Source drive type code.
967    pub drive_type: Option<char>,
968    /// Source manufacturer.
969    pub manufacturer: Option<String>,
970    /// Source model.
971    pub model: Option<String>,
972    /// Source serial number.
973    pub serial_number: Option<String>,
974    /// Source domain.
975    pub domain: Option<String>,
976    /// Source IP address.
977    pub ip_address: Option<String>,
978    /// Source MAC address.
979    pub mac_address: Option<String>,
980    /// Source size in bytes.
981    pub size: Option<u64>,
982    /// Source logical offset.
983    pub logical_offset: Option<i64>,
984    /// Source physical offset.
985    pub physical_offset: Option<i64>,
986    /// Source acquisition timestamp.
987    pub acquisition_time: Option<i64>,
988    /// Source MD5 hash string.
989    pub md5: Option<String>,
990    /// Source SHA1 hash string.
991    pub sha1: Option<String>,
992}
993
994#[derive(Debug, Clone, Default, PartialEq, Eq)]
995/// Subject record for logical single-file metadata.
996pub struct SingleFileSubject {
997    /// Subject identifier.
998    pub identifier: Option<u32>,
999    /// Subject name.
1000    pub name: Option<String>,
1001}
1002
1003#[derive(Debug, Clone, Default, PartialEq, Eq)]
1004/// Access-control entry for a logical single-file entry.
1005pub struct SingleFilePermission {
1006    /// Permission name.
1007    pub name: Option<String>,
1008    /// Security identifier or permission identifier.
1009    pub identifier: Option<String>,
1010    /// Permission property type.
1011    pub property_type: Option<u32>,
1012    /// Access mask value.
1013    pub access_mask: Option<u32>,
1014    /// ACE flags value.
1015    pub ace_flags: Option<u32>,
1016}
1017
1018#[derive(Debug, Clone, Default, PartialEq, Eq)]
1019/// Access-control group for logical single-file entries.
1020pub struct SingleFilePermissionGroup {
1021    /// Group name.
1022    pub name: Option<String>,
1023    /// Security identifier or group identifier.
1024    pub identifier: Option<String>,
1025    /// Permission group property type.
1026    pub property_type: Option<u32>,
1027    /// Group access mask value.
1028    pub access_mask: Option<u32>,
1029    /// Group ACE flags value.
1030    pub ace_flags: Option<u32>,
1031    /// Permissions in the group.
1032    pub permissions: Vec<SingleFilePermission>,
1033}
1034
1035#[derive(Debug, Clone, Default, PartialEq, Eq)]
1036/// Extended attribute for a logical single-file entry.
1037pub struct SingleFileAttribute {
1038    /// Attribute name.
1039    pub name: Option<String>,
1040    /// Attribute value.
1041    pub value: Option<String>,
1042}
1043
1044#[derive(Debug, Clone, Default, PartialEq, Eq)]
1045/// Entry in a logical single-file catalog.
1046pub struct SingleFileEntry {
1047    /// Entry identifier.
1048    pub identifier: Option<u64>,
1049    /// Entry type.
1050    pub file_entry_type: Option<SingleFileEntryType>,
1051    /// Raw entry flags.
1052    pub flags: Option<u32>,
1053    /// Entry GUID.
1054    pub guid: Option<String>,
1055    /// Entry name.
1056    pub name: Option<String>,
1057    /// Original UTF-16 units when the entry name contains an unpaired surrogate.
1058    /// `name` is then a display string with replacement characters; use this
1059    /// field or the catalog index to distinguish names that display alike.
1060    pub name_utf16: Option<Vec<u16>>,
1061    /// Short entry name.
1062    pub short_name: Option<String>,
1063    /// Logical file size in bytes.
1064    pub size: Option<u64>,
1065    /// Logical media offset for the entry data.
1066    pub logical_offset: Option<i64>,
1067    /// Physical source offset for the entry data.
1068    pub physical_offset: Option<i64>,
1069    /// Duplicate-data logical media offset.
1070    pub duplicate_data_offset: Option<i64>,
1071    /// Identifier of the associated source record.
1072    pub source_identifier: Option<i32>,
1073    /// Identifier of the associated subject record.
1074    pub subject_identifier: Option<u32>,
1075    /// Index of the associated permission group.
1076    pub permission_group_index: Option<i32>,
1077    /// Raw record type.
1078    pub record_type: Option<u32>,
1079    /// Creation timestamp.
1080    pub creation_time: Option<i64>,
1081    /// Modification timestamp.
1082    pub modification_time: Option<i64>,
1083    /// Access timestamp.
1084    pub access_time: Option<i64>,
1085    /// Entry metadata modification timestamp.
1086    pub entry_modification_time: Option<i64>,
1087    /// Deletion timestamp.
1088    pub deletion_time: Option<i64>,
1089    /// Entry MD5 hash string.
1090    pub md5: Option<String>,
1091    /// Entry SHA1 hash string.
1092    pub sha1: Option<String>,
1093    /// Data extents for the entry.
1094    pub extents: Vec<SingleFileExtent>,
1095    /// Extended attributes for the entry.
1096    pub attributes: Vec<SingleFileAttribute>,
1097    /// Child entries.
1098    pub children: Vec<SingleFileEntry>,
1099}
1100
1101#[derive(Debug, Clone, Default, PartialEq, Eq)]
1102/// Logical single-file catalog metadata.
1103pub struct SingleFilesInfo {
1104    /// Byte size of the single-file data section.
1105    pub data_size: u64,
1106    /// Root catalog entry.
1107    pub root: SingleFileEntry,
1108    /// Source records.
1109    pub sources: Vec<SingleFileSource>,
1110    /// Subject records.
1111    pub subjects: Vec<SingleFileSubject>,
1112    /// Permission groups.
1113    pub permission_groups: Vec<SingleFilePermissionGroup>,
1114}
1115
1116#[derive(Debug, Clone, Default, PartialEq, Eq)]
1117/// Preserved auxiliary EWF2 single-file tables.
1118pub struct SingleFilesAuxTables {
1119    /// Raw entries from EWF2 single-files table `0x21`.
1120    pub table_0x21_entries: Vec<u64>,
1121    /// MD5 hash table entries.
1122    pub md5_hashes: Vec<[u8; 16]>,
1123    /// Raw entries from EWF2 single-files table `0x23`.
1124    pub table_0x23_entries: Vec<u64>,
1125}
1126
1127impl SingleFilesAuxTables {
1128    /// Returns whether no auxiliary single-file table data is present.
1129    pub fn is_empty(&self) -> bool {
1130        self.table_0x21_entries.is_empty()
1131            && self.md5_hashes.is_empty()
1132            && self.table_0x23_entries.is_empty()
1133    }
1134}
1135
1136#[derive(Debug, Clone, PartialEq, Eq)]
1137/// Parsed summary of an opened EWF image.
1138pub struct ImageInfo {
1139    /// Top-level EWF format generation.
1140    pub format: Format,
1141    /// Inferred producer/profile.
1142    pub format_profile: FormatProfile,
1143    /// Number of opened segment files.
1144    pub segment_count: usize,
1145    /// Segment paths or supplied-reader labels.
1146    pub segment_paths: Vec<PathBuf>,
1147    /// Logical chunk size in bytes.
1148    pub chunk_size: u64,
1149    /// Logical media size in bytes.
1150    pub logical_size: u64,
1151    /// Whether the image has a terminal complete marker.
1152    pub acquisition_complete: bool,
1153    /// Header codepage used for decoded EWF1 values.
1154    pub header_codepage: HeaderCodepage,
1155    /// Date format used when returning header date values.
1156    pub header_values_date_format: HeaderDateFormat,
1157    /// Media geometry and flags.
1158    pub media: MediaInfo,
1159    /// Parsed case and acquisition metadata.
1160    pub metadata: EwfMetadata,
1161    /// Stored hash values.
1162    pub stored_hashes: StoredHashes,
1163    /// Acquisition error ranges.
1164    pub acquisition_errors: Vec<AcquisitionError>,
1165    /// Memory acquisition extents.
1166    pub memory_extents: Vec<MemoryExtent>,
1167    /// Logical single-file catalog, if present.
1168    pub single_files: Option<SingleFilesInfo>,
1169    /// Preserved EWF2 single-file auxiliary table data.
1170    pub ewf2_single_files_tables: SingleFilesAuxTables,
1171    /// Raw EWF2 increment data sections.
1172    pub ewf2_increment_data: Vec<Vec<u8>>,
1173    /// Raw EWF2 final information section.
1174    pub ewf2_final_information: Option<Vec<u8>>,
1175    /// EWF2 restart data text.
1176    pub ewf2_restart_data: Option<String>,
1177    /// EWF2 analytical data text.
1178    pub ewf2_analytical_data: Option<String>,
1179    /// Session sector ranges.
1180    pub sessions: Vec<SectorRange>,
1181    /// Track sector ranges.
1182    pub tracks: Vec<SectorRange>,
1183}
1184
1185#[cfg(feature = "verify")]
1186#[derive(Debug, Clone, PartialEq, Eq)]
1187/// Result of streamed logical media hash verification.
1188pub struct VerifyResult {
1189    /// MD5 hash computed from the logical media stream.
1190    pub computed_md5: Option<[u8; 16]>,
1191    /// SHA1 hash computed from the logical media stream.
1192    pub computed_sha1: Option<[u8; 20]>,
1193    /// SHA256 hash computed from the logical media stream.
1194    pub computed_sha256: Option<[u8; 32]>,
1195    /// Whether computed MD5 matched the stored MD5, or `None` if no MD5 was stored.
1196    pub md5_match: Option<bool>,
1197    /// Whether computed SHA1 matched the stored SHA1, or `None` if no SHA1 was stored.
1198    pub sha1_match: Option<bool>,
1199    /// Whether computed SHA256 matched the stored SHA256, or `None` if absent.
1200    pub sha256_match: Option<bool>,
1201}