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