Skip to main content

ewf_image/
single_files.rs

1use crate::types::{
2    SingleFileAttribute, SingleFileEntry, SingleFileEntryType, SingleFileExtent,
3    SingleFilePermission, SingleFilePermissionGroup, SingleFileSource, SingleFileSubject,
4    SingleFilesInfo,
5};
6use crate::{EwfError, Result};
7use std::collections::BTreeMap;
8
9/// Path separator used by EWF2 logical single-file catalogs.
10///
11/// Paths are represented with tab-separated entry names. A leading separator
12/// refers to the root entry.
13pub const SINGLE_FILE_PATH_SEPARATOR: char = '\t';
14const EXTENDED_ATTRIBUTES_HEADER: &[u8; 37] = &[
15    0x00, 0x00, 0x00, 0x00, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x00, 0x74,
16    0x00, 0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x62, 0x00, 0x75, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73,
17    0x00, 0x00, 0x00, 0x00, 0x00,
18];
19
20impl SingleFilesInfo {
21    /// Returns the catalog entry at a single-file path.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if the path contains an empty entry name.
26    pub fn entry_by_path(&self, path: &str) -> Result<Option<&SingleFileEntry>> {
27        self.root.child_by_path(path)
28    }
29
30    /// Returns a source record by its source identifier.
31    pub fn source_by_identifier(&self, identifier: i32) -> Option<&SingleFileSource> {
32        self.sources
33            .iter()
34            .find(|source| source.identifier == Some(identifier))
35    }
36
37    /// Returns a source record by its source table index.
38    pub fn source_by_index(&self, index: i32) -> Option<&SingleFileSource> {
39        usize::try_from(index)
40            .ok()
41            .and_then(|index| self.sources.get(index))
42    }
43
44    /// Returns the source record associated with a catalog entry.
45    pub fn source_for_entry(&self, entry: &SingleFileEntry) -> Option<&SingleFileSource> {
46        let identifier = entry.source_identifier?;
47        if identifier < 1 {
48            return None;
49        }
50        self.source_by_index(identifier)
51    }
52
53    /// Returns a subject record by its subject identifier.
54    pub fn subject_by_identifier(&self, identifier: u32) -> Option<&SingleFileSubject> {
55        self.subjects
56            .iter()
57            .find(|subject| subject.identifier == Some(identifier))
58    }
59
60    /// Returns the subject record associated with a catalog entry.
61    pub fn subject_for_entry(&self, entry: &SingleFileEntry) -> Option<&SingleFileSubject> {
62        self.subject_by_identifier(entry.subject_identifier?)
63    }
64
65    /// Returns the number of permission groups.
66    pub fn number_of_permission_groups(&self) -> usize {
67        self.permission_groups.len()
68    }
69
70    /// Returns a permission group by its table index.
71    pub fn permission_group_by_index(&self, index: i32) -> Option<&SingleFilePermissionGroup> {
72        usize::try_from(index)
73            .ok()
74            .and_then(|index| self.permission_groups.get(index))
75    }
76
77    /// Returns the permission group associated with a catalog entry.
78    pub fn permission_group_for_entry(
79        &self,
80        entry: &SingleFileEntry,
81    ) -> Option<&SingleFilePermissionGroup> {
82        self.permission_group_by_index(entry.permission_group_index?)
83    }
84
85    /// Returns access-control entries associated with a catalog entry.
86    pub fn access_control_entries_for_entry(
87        &self,
88        entry: &SingleFileEntry,
89    ) -> &[SingleFilePermission] {
90        self.permission_group_for_entry(entry)
91            .map_or(&[], |group| group.permissions.as_slice())
92    }
93
94    /// Returns the number of access-control entries for a catalog entry.
95    pub fn number_of_access_control_entries_for_entry(&self, entry: &SingleFileEntry) -> usize {
96        self.access_control_entries_for_entry(entry).len()
97    }
98
99    /// Returns one access-control entry for a catalog entry by index.
100    pub fn access_control_entry_for_entry(
101        &self,
102        entry: &SingleFileEntry,
103        index: usize,
104    ) -> Option<&SingleFilePermission> {
105        self.access_control_entries_for_entry(entry).get(index)
106    }
107}
108
109impl SingleFileSource {
110    /// Returns the source identifier.
111    pub fn identifier(&self) -> Option<i32> {
112        self.identifier
113    }
114
115    /// Returns the source name.
116    pub fn name(&self) -> Option<&str> {
117        self.name.as_deref()
118    }
119
120    /// Returns the evidence number associated with the source.
121    pub fn evidence_number(&self) -> Option<&str> {
122        self.evidence_number.as_deref()
123    }
124
125    /// Returns the source location.
126    pub fn location(&self) -> Option<&str> {
127        self.location.as_deref()
128    }
129
130    /// Returns the source device GUID.
131    pub fn device_guid(&self) -> Option<&str> {
132        self.device_guid.as_deref()
133    }
134
135    /// Returns the primary source device GUID.
136    pub fn primary_device_guid(&self) -> Option<&str> {
137        self.primary_device_guid.as_deref()
138    }
139
140    /// Returns the source manufacturer.
141    pub fn manufacturer(&self) -> Option<&str> {
142        self.manufacturer.as_deref()
143    }
144
145    /// Returns the source model.
146    pub fn model(&self) -> Option<&str> {
147        self.model.as_deref()
148    }
149
150    /// Returns the source serial number.
151    pub fn serial_number(&self) -> Option<&str> {
152        self.serial_number.as_deref()
153    }
154
155    /// Returns the source domain.
156    pub fn domain(&self) -> Option<&str> {
157        self.domain.as_deref()
158    }
159
160    /// Returns the source IP address.
161    pub fn ip_address(&self) -> Option<&str> {
162        self.ip_address.as_deref()
163    }
164
165    /// Returns the source MAC address.
166    pub fn mac_address(&self) -> Option<&str> {
167        self.mac_address.as_deref()
168    }
169
170    /// Returns the source size in bytes.
171    pub fn size(&self) -> Option<u64> {
172        self.size
173    }
174
175    /// Returns the source drive type code.
176    pub fn drive_type(&self) -> Option<char> {
177        self.drive_type
178    }
179
180    /// Returns the source logical offset.
181    pub fn logical_offset(&self) -> Option<i64> {
182        self.logical_offset
183    }
184
185    /// Returns the source physical offset.
186    pub fn physical_offset(&self) -> Option<i64> {
187        self.physical_offset
188    }
189
190    /// Returns the source acquisition timestamp.
191    pub fn acquisition_time(&self) -> Option<i64> {
192        self.acquisition_time
193    }
194
195    /// Returns the source MD5 hash string.
196    pub fn md5_hash(&self) -> Option<&str> {
197        self.md5.as_deref()
198    }
199
200    /// Returns the source SHA1 hash string.
201    pub fn sha1_hash(&self) -> Option<&str> {
202        self.sha1.as_deref()
203    }
204}
205
206impl SingleFileEntry {
207    /// Returns the entry identifier.
208    pub fn identifier(&self) -> Option<u64> {
209        self.identifier
210    }
211
212    /// Returns the entry type.
213    pub fn entry_type(&self) -> Option<SingleFileEntryType> {
214        self.file_entry_type
215    }
216
217    /// Returns the raw entry flags.
218    pub fn flags(&self) -> Option<u32> {
219        self.flags
220    }
221
222    /// Returns the entry GUID.
223    pub fn guid(&self) -> Option<&str> {
224        self.guid.as_deref()
225    }
226
227    /// Returns the logical media offset for entry data.
228    pub fn media_data_offset(&self) -> Option<i64> {
229        self.logical_offset
230    }
231
232    /// Returns the entry data size in bytes.
233    pub fn media_data_size(&self) -> Option<u64> {
234        self.size
235    }
236
237    /// Returns the physical source offset for entry data.
238    pub fn physical_offset(&self) -> Option<i64> {
239        self.physical_offset
240    }
241
242    /// Returns the duplicate-data logical media offset.
243    pub fn duplicate_media_data_offset(&self) -> Option<i64> {
244        self.duplicate_data_offset
245    }
246
247    /// Returns the associated source identifier.
248    pub fn source_identifier(&self) -> Option<i32> {
249        self.source_identifier
250    }
251
252    /// Returns the associated subject identifier.
253    pub fn subject_identifier(&self) -> Option<u32> {
254        self.subject_identifier
255    }
256
257    /// Returns the associated permission group index.
258    pub fn permission_group_index(&self) -> Option<i32> {
259        self.permission_group_index
260    }
261
262    /// Returns the raw record type.
263    pub fn record_type(&self) -> Option<u32> {
264        self.record_type
265    }
266
267    /// Returns the entry name.
268    pub fn name(&self) -> Option<&str> {
269        self.name.as_deref()
270    }
271
272    /// Returns the short entry name.
273    pub fn short_name(&self) -> Option<&str> {
274        self.short_name.as_deref()
275    }
276
277    /// Returns the entry size in bytes.
278    pub fn size(&self) -> Option<u64> {
279        self.size
280    }
281
282    /// Returns the creation timestamp.
283    pub fn creation_time(&self) -> Option<i64> {
284        self.creation_time
285    }
286
287    /// Returns the modification timestamp.
288    pub fn modification_time(&self) -> Option<i64> {
289        self.modification_time
290    }
291
292    /// Returns the access timestamp.
293    pub fn access_time(&self) -> Option<i64> {
294        self.access_time
295    }
296
297    /// Returns the entry metadata modification timestamp.
298    pub fn entry_modification_time(&self) -> Option<i64> {
299        self.entry_modification_time
300    }
301
302    /// Returns the deletion timestamp.
303    pub fn deletion_time(&self) -> Option<i64> {
304        self.deletion_time
305    }
306
307    /// Returns the entry MD5 hash string.
308    pub fn md5_hash(&self) -> Option<&str> {
309        self.md5.as_deref()
310    }
311
312    /// Returns the entry SHA1 hash string.
313    pub fn sha1_hash(&self) -> Option<&str> {
314        self.sha1.as_deref()
315    }
316
317    /// Returns the number of child entries.
318    pub fn number_of_sub_file_entries(&self) -> usize {
319        self.children.len()
320    }
321
322    /// Returns a child entry by index.
323    pub fn sub_file_entry(&self, index: usize) -> Option<&SingleFileEntry> {
324        self.children.get(index)
325    }
326
327    /// Returns a child entry by name.
328    pub fn sub_file_entry_by_name(&self, name: &str) -> Option<&SingleFileEntry> {
329        self.child_by_name(name)
330    }
331
332    /// Returns a descendant entry by single-file path.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if the path contains an empty entry name.
337    pub fn sub_file_entry_by_path(&self, path: &str) -> Result<Option<&SingleFileEntry>> {
338        self.child_by_path(path)
339    }
340
341    /// Returns the number of data extents.
342    pub fn number_of_extents(&self) -> usize {
343        self.extents.len()
344    }
345
346    /// Returns a data extent by index.
347    pub fn extent(&self, index: usize) -> Option<&SingleFileExtent> {
348        self.extents.get(index)
349    }
350
351    /// Returns the number of extended attributes.
352    pub fn number_of_attributes(&self) -> usize {
353        self.attributes.len()
354    }
355
356    /// Returns an extended attribute by index.
357    pub fn attribute(&self, index: usize) -> Option<&SingleFileAttribute> {
358        self.attributes.get(index)
359    }
360
361    /// Returns a direct child entry by name.
362    pub fn child_by_name(&self, name: &str) -> Option<&SingleFileEntry> {
363        self.children
364            .iter()
365            .find(|child| child.name.as_deref() == Some(name))
366    }
367
368    /// Returns a descendant entry by single-file path.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error if the path contains an empty entry name.
373    pub fn child_by_path(&self, path: &str) -> Result<Option<&SingleFileEntry>> {
374        entry_by_path(self, path)
375    }
376}
377
378impl SingleFilePermission {
379    /// Returns the permission name.
380    pub fn name(&self) -> Option<&str> {
381        self.name.as_deref()
382    }
383
384    /// Returns the permission identifier.
385    pub fn identifier(&self) -> Option<&str> {
386        self.identifier.as_deref()
387    }
388
389    /// Returns the permission property type.
390    pub fn property_type(&self) -> Option<u32> {
391        self.property_type
392    }
393
394    /// Returns the access mask.
395    pub fn access_mask(&self) -> Option<u32> {
396        self.access_mask
397    }
398
399    /// Returns the ACE flags.
400    pub fn flags(&self) -> Option<u32> {
401        self.ace_flags
402    }
403}
404
405impl SingleFilePermissionGroup {
406    /// Returns the group name.
407    pub fn name(&self) -> Option<&str> {
408        self.name.as_deref()
409    }
410
411    /// Returns the group identifier.
412    pub fn identifier(&self) -> Option<&str> {
413        self.identifier.as_deref()
414    }
415
416    /// Returns the group property type.
417    pub fn property_type(&self) -> Option<u32> {
418        self.property_type
419    }
420
421    /// Returns the group access mask.
422    pub fn access_mask(&self) -> Option<u32> {
423        self.access_mask
424    }
425
426    /// Returns the group ACE flags.
427    pub fn flags(&self) -> Option<u32> {
428        self.ace_flags
429    }
430
431    /// Returns the number of permissions in the group.
432    pub fn number_of_entries(&self) -> usize {
433        self.permissions.len()
434    }
435
436    /// Returns a permission in the group by table index.
437    pub fn entry_by_index(&self, index: i32) -> Option<&SingleFilePermission> {
438        usize::try_from(index)
439            .ok()
440            .and_then(|index| self.permissions.get(index))
441    }
442}
443
444impl SingleFileAttribute {
445    /// Returns the attribute name.
446    pub fn name(&self) -> Option<&str> {
447        self.name.as_deref()
448    }
449
450    /// Returns the attribute value.
451    pub fn value(&self) -> Option<&str> {
452        self.value.as_deref()
453    }
454}
455
456pub(crate) fn parse_ewf2_single_files_data(data: &[u8]) -> Result<SingleFilesInfo> {
457    parse_single_files_data(data, false)
458}
459
460pub(crate) fn parse_ewf1_single_files_data(data: &[u8]) -> Result<SingleFilesInfo> {
461    parse_single_files_data(data, true)
462}
463
464fn parse_single_files_data(data: &[u8], require_record_size: bool) -> Result<SingleFilesInfo> {
465    let DecodedUtf16Lines {
466        lines,
467        mut invalid_utf16_lines,
468    } = decode_utf16le_lines(data)?;
469    if lines.first().map(String::as_str) != Some("5") {
470        return Err(EwfError::Malformed(
471            "EWF2 single files data has unsupported category count".into(),
472        ));
473    }
474
475    let mut category_index = 1;
476
477    let (data_size, next_category_index) = parse_record_category(&lines, category_index)?;
478    category_index = next_category_index;
479    let (permission_groups, next_category_index) =
480        parse_permission_groups_category(&lines, category_index)?;
481    category_index = next_category_index;
482    let (sources, next_category_index) = parse_sources_category(&lines, category_index)?;
483    category_index = next_category_index;
484    let (subjects, entry_index) = parse_subjects_category(&lines, category_index)?;
485
486    require_category_at(&lines, entry_index, "entry", "entry category")?;
487    let types_line_index = entry_index
488        .checked_add(2)
489        .ok_or_else(|| EwfError::Malformed("EWF2 single files entry index overflow".into()))?;
490    let mut cursor = entry_index
491        .checked_add(3)
492        .ok_or_else(|| EwfError::Malformed("EWF2 single files entry index overflow".into()))?;
493    if types_line_index >= lines.len() {
494        return Err(EwfError::Malformed(
495            "EWF2 single files entry category is truncated".into(),
496        ));
497    }
498
499    parse_category_entry_count(&lines[entry_index + 1], "EWF2 single files entry count")?;
500    let types = parse_type_line(
501        &lines[types_line_index],
502        "EWF2 single files entry category types",
503    )?;
504
505    let data_size = match data_size {
506        Some(size) => size,
507        None if require_record_size => {
508            return Err(EwfError::Malformed(
509                "EWF1 ltree record has no total data size".into(),
510            ));
511        }
512        None => u64::try_from(data.len())
513            .map_err(|_| EwfError::Malformed("single files data size overflow".into()))?,
514    };
515    let root = parse_entry(&lines, &types, &mut cursor, &mut invalid_utf16_lines)?;
516    require_empty_category_terminator(
517        &lines,
518        cursor,
519        "EWF2 single files entry category terminator",
520    )?;
521    if !invalid_utf16_lines.is_empty() {
522        return Err(EwfError::Malformed(
523            "single files data has invalid UTF-16 outside an entry name".into(),
524        ));
525    }
526    Ok(SingleFilesInfo {
527        data_size,
528        root,
529        sources,
530        subjects,
531        permission_groups,
532    })
533}
534
535fn entry_by_path<'a>(
536    mut entry: &'a SingleFileEntry,
537    path: &str,
538) -> Result<Option<&'a SingleFileEntry>> {
539    let mut rest = path
540        .strip_prefix(SINGLE_FILE_PATH_SEPARATOR)
541        .unwrap_or(path);
542    if rest.is_empty() {
543        return Ok(Some(entry));
544    }
545
546    while !rest.is_empty() {
547        let (segment, next_rest) = match rest.find([SINGLE_FILE_PATH_SEPARATOR, '\0']) {
548            Some(separator_index) => {
549                let separator_size = rest[separator_index..]
550                    .chars()
551                    .next()
552                    .expect("separator index points to a character")
553                    .len_utf8();
554                (
555                    &rest[..separator_index],
556                    &rest[separator_index + separator_size..],
557                )
558            }
559            None => (rest, ""),
560        };
561        if segment.is_empty() {
562            return Err(EwfError::Malformed(
563                "single file path is missing entry name".into(),
564            ));
565        }
566
567        entry = match entry.child_by_name(segment) {
568            Some(child) => child,
569            None => return Ok(None),
570        };
571        rest = next_rest;
572    }
573
574    Ok(Some(entry))
575}
576
577fn parse_sources_category(
578    lines: &[String],
579    category_index: usize,
580) -> Result<(Vec<SingleFileSource>, usize)> {
581    require_category_at(lines, category_index, "srce", "source category")?;
582    let (types, entry_count, mut cursor) = parse_category_header(lines, category_index, "srce")?;
583
584    let root = parse_source_row(&types, get_line(lines, cursor, "EWF2 source root")?)?;
585    cursor += 1;
586    let mut sources = Vec::new();
587    sources.push(root);
588
589    for source_index in 0..entry_count {
590        let child_count = parse_child_entry_count(
591            get_line(lines, cursor, "EWF2 source child count")?,
592            "EWF2 source child count",
593        )?;
594        if child_count != 0 {
595            return Err(EwfError::Malformed(
596                "EWF2 source entries cannot have children".into(),
597            ));
598        }
599        cursor += 1;
600
601        let source = parse_source_row(&types, get_line(lines, cursor, "EWF2 source row")?)?;
602        let expected_identifier = source_index
603            .checked_add(1)
604            .and_then(|identifier| i32::try_from(identifier).ok())
605            .ok_or_else(|| EwfError::Malformed("EWF2 source identifier index overflow".into()))?;
606        if source.identifier != Some(expected_identifier) {
607            return Err(EwfError::Malformed(
608                "EWF2 source identifier does not match source index".into(),
609            ));
610        }
611        sources.push(source);
612        cursor += 1;
613    }
614    require_empty_category_terminator(
615        lines,
616        cursor,
617        "EWF2 single files source category terminator",
618    )?;
619    Ok((
620        sources,
621        next_category_index(cursor, "EWF2 source category terminator")?,
622    ))
623}
624
625fn parse_subjects_category(
626    lines: &[String],
627    category_index: usize,
628) -> Result<(Vec<SingleFileSubject>, usize)> {
629    require_category_at(lines, category_index, "sub", "subject category")?;
630    let (types, entry_count, mut cursor) = parse_category_header(lines, category_index, "sub")?;
631
632    let root = parse_subject_row(&types, get_line(lines, cursor, "EWF2 subject root")?)?;
633    cursor += 1;
634    let mut subjects = Vec::new();
635    subjects.push(root);
636
637    for _ in 0..entry_count {
638        let child_count = parse_child_entry_count(
639            get_line(lines, cursor, "EWF2 subject child count")?,
640            "EWF2 subject child count",
641        )?;
642        if child_count != 0 {
643            return Err(EwfError::Malformed(
644                "EWF2 subject entries cannot have children".into(),
645            ));
646        }
647        cursor += 1;
648
649        let subject = parse_subject_row(&types, get_line(lines, cursor, "EWF2 subject row")?)?;
650        subjects.push(subject);
651        cursor += 1;
652    }
653    require_empty_category_terminator(
654        lines,
655        cursor,
656        "EWF2 single files subject category terminator",
657    )?;
658    Ok((
659        subjects,
660        next_category_index(cursor, "EWF2 subject category terminator")?,
661    ))
662}
663
664fn parse_permission_groups_category(
665    lines: &[String],
666    category_index: usize,
667) -> Result<(Vec<SingleFilePermissionGroup>, usize)> {
668    require_category_at(lines, category_index, "perm", "permission category")?;
669    let (types, entry_count, mut cursor) = parse_category_header(lines, category_index, "perm")?;
670
671    let root_permission = parse_permission_row(
672        &types,
673        get_line(lines, cursor, "EWF2 permission category root")?,
674    )?;
675    require_permission_group_type(&root_permission, "EWF2 permission category root")?;
676    cursor += 1;
677    let mut groups = Vec::new();
678
679    for _ in 0..entry_count {
680        let permission_count = parse_child_entry_count(
681            get_line(lines, cursor, "EWF2 permission group child count")?,
682            "EWF2 permission group child count",
683        )?;
684        cursor += 1;
685
686        let group_permission = parse_permission_row(
687            &types,
688            get_line(lines, cursor, "EWF2 permission group row")?,
689        )?;
690        require_permission_group_type(&group_permission, "EWF2 permission group row")?;
691        cursor += 1;
692        let mut group = permission_group_from_permission(group_permission);
693
694        for _ in 0..permission_count {
695            let child_count = parse_child_entry_count(
696                get_line(lines, cursor, "EWF2 permission child count")?,
697                "EWF2 permission child count",
698            )?;
699            if child_count != 0 {
700                return Err(EwfError::Malformed(
701                    "EWF2 permission entries cannot have children".into(),
702                ));
703            }
704            cursor += 1;
705
706            let permission =
707                parse_permission_row(&types, get_line(lines, cursor, "EWF2 permission row")?)?;
708            group.permissions.push(permission);
709            cursor += 1;
710        }
711        groups.push(group);
712    }
713    require_empty_category_terminator(
714        lines,
715        cursor,
716        "EWF2 single files permission category terminator",
717    )?;
718    Ok((
719        groups,
720        next_category_index(cursor, "EWF2 permission category terminator")?,
721    ))
722}
723
724fn require_category_at(lines: &[String], index: usize, name: &str, label: &str) -> Result<()> {
725    let line = get_line(lines, index, &format!("EWF2 single files {label}"))?;
726    if line != name {
727        return Err(EwfError::Malformed(format!(
728            "EWF2 single files {label} is missing or out of order"
729        )));
730    }
731    Ok(())
732}
733
734fn next_category_index(index: usize, label: &str) -> Result<usize> {
735    index
736        .checked_add(1)
737        .ok_or_else(|| EwfError::Malformed(format!("{label} index overflow")))
738}
739
740fn parse_record_category(lines: &[String], category_index: usize) -> Result<(Option<u64>, usize)> {
741    require_category_at(lines, category_index, "rec", "record category")?;
742    let types_line_index = category_index
743        .checked_add(1)
744        .ok_or_else(|| EwfError::Malformed("EWF2 record category index overflow".into()))?;
745    let values_line_index = category_index
746        .checked_add(2)
747        .ok_or_else(|| EwfError::Malformed("EWF2 record category index overflow".into()))?;
748    let types = parse_type_line(
749        get_line(lines, types_line_index, "EWF2 record category types")?,
750        "EWF2 record category types",
751    )?;
752    let values: Vec<&str> = get_line(lines, values_line_index, "EWF2 record category values")?
753        .split('\t')
754        .collect();
755    let terminator_index = category_index
756        .checked_add(3)
757        .ok_or_else(|| EwfError::Malformed("EWF2 record category index overflow".into()))?;
758
759    require_empty_category_terminator(
760        lines,
761        terminator_index,
762        "EWF2 single files record category terminator",
763    )?;
764    let next_category_index =
765        next_category_index(terminator_index, "EWF2 record category terminator")?;
766
767    for (index, value_type) in types.iter().enumerate() {
768        if *value_type != "tb" {
769            continue;
770        }
771        let Some(value) = values.get(index).copied().filter(|value| !value.is_empty()) else {
772            return Ok((None, next_category_index));
773        };
774        return Ok((
775            parse_u64(value, "record data size").map(Some)?,
776            next_category_index,
777        ));
778    }
779    Ok((None, next_category_index))
780}
781
782fn parse_category_header<'a>(
783    lines: &'a [String],
784    category_index: usize,
785    label: &str,
786) -> Result<(Vec<&'a str>, usize, usize)> {
787    let count_line_index = category_index
788        .checked_add(1)
789        .ok_or_else(|| EwfError::Malformed(format!("EWF2 {label} category index overflow")))?;
790    let types_line_index = category_index
791        .checked_add(2)
792        .ok_or_else(|| EwfError::Malformed(format!("EWF2 {label} category index overflow")))?;
793    let copy_count_line_index = category_index
794        .checked_add(3)
795        .ok_or_else(|| EwfError::Malformed(format!("EWF2 {label} category index overflow")))?;
796    let root_line_index = category_index
797        .checked_add(4)
798        .ok_or_else(|| EwfError::Malformed(format!("EWF2 {label} category index overflow")))?;
799
800    let entry_count = parse_category_entry_count(
801        get_line(lines, count_line_index, "EWF2 category count")?,
802        "EWF2 category count",
803    )?;
804    let types = parse_type_line(
805        get_line(lines, types_line_index, "EWF2 category types")?,
806        &format!("EWF2 {label} category types"),
807    )?;
808    let copy_entry_count = parse_child_entry_count(
809        get_line(lines, copy_count_line_index, "EWF2 category copy count")?,
810        "EWF2 category copy count",
811    )?;
812    if copy_entry_count != entry_count {
813        return Err(EwfError::Malformed(format!(
814            "EWF2 {label} category entry count mismatch"
815        )));
816    }
817    Ok((types, entry_count, root_line_index))
818}
819
820fn parse_source_row(types: &[&str], row: &str) -> Result<SingleFileSource> {
821    let values = split_exact_typed_values(types, row, "EWF2 source row")?;
822    let mut source = SingleFileSource::default();
823    for (value_type, value) in types.iter().zip(values.iter()) {
824        if value.is_empty() {
825            continue;
826        }
827        match *value_type {
828            "ah" => source.md5 = parse_serialized_base16_string(value, "source MD5 hash")?,
829            "aq" => source.acquisition_time = Some(parse_i64(value, "source acquisition time")?),
830            "do" => source.domain = Some((*value).to_owned()),
831            "dt" => source.drive_type = Some(parse_single_char(value, "source drive type")?),
832            "ev" => source.evidence_number = Some((*value).to_owned()),
833            "gu" => {
834                source.device_guid = parse_serialized_base16_string(value, "source device GUID")?;
835            }
836            "id" => source.identifier = Some(parse_non_negative_i32(value, "source identifier")?),
837            "ip" => source.ip_address = Some((*value).to_owned()),
838            "lo" => source.logical_offset = Some(parse_i64(value, "source logical offset")?),
839            "loc" => source.location = Some((*value).to_owned()),
840            "ma" => {
841                source.mac_address = parse_serialized_base16_string(value, "source MAC address")?;
842            }
843            "mfr" => source.manufacturer = Some((*value).to_owned()),
844            "mo" => source.model = Some((*value).to_owned()),
845            "n" => source.name = Some((*value).to_owned()),
846            "pgu" => {
847                source.primary_device_guid =
848                    parse_serialized_base16_string(value, "source primary device GUID")?;
849            }
850            "po" => source.physical_offset = Some(parse_i64(value, "source physical offset")?),
851            "se" => source.serial_number = Some((*value).to_owned()),
852            "sh" => source.sha1 = parse_serialized_base16_string(value, "source SHA1 hash")?,
853            "tb" => source.size = Some(parse_u64(value, "source size")?),
854            _ => {}
855        }
856    }
857    Ok(source)
858}
859
860fn parse_subject_row(types: &[&str], row: &str) -> Result<SingleFileSubject> {
861    let values = split_exact_typed_values(types, row, "EWF2 subject row")?;
862    let mut subject = SingleFileSubject::default();
863    for (value_type, value) in types.iter().zip(values.iter()) {
864        if value.is_empty() {
865            continue;
866        }
867        match *value_type {
868            "id" => subject.identifier = Some(parse_u32(value, "subject identifier")?),
869            "n" => subject.name = Some((*value).to_owned()),
870            _ => {}
871        }
872    }
873    Ok(subject)
874}
875
876fn parse_permission_row(types: &[&str], row: &str) -> Result<SingleFilePermission> {
877    let values = split_exact_typed_values(types, row, "EWF2 permission row")?;
878    let mut permission = SingleFilePermission::default();
879    for (value_type, value) in types.iter().zip(values.iter()) {
880        if value.is_empty() {
881            continue;
882        }
883        match *value_type {
884            "n" => permission.name = Some((*value).to_owned()),
885            "nta" => permission.access_mask = Some(parse_u32(value, "permission access mask")?),
886            "nti" => permission.ace_flags = Some(parse_u32(value, "permission ACE flags")?),
887            "pr" | "pt" => {
888                permission.property_type = Some(parse_u32(value, "permission property type")?);
889            }
890            "s" => permission.identifier = Some((*value).to_owned()),
891            _ => {}
892        }
893    }
894    Ok(permission)
895}
896
897fn require_permission_group_type(permission: &SingleFilePermission, label: &str) -> Result<()> {
898    if permission.property_type != Some(10) {
899        return Err(EwfError::Malformed(format!(
900            "{label} has unsupported permission group type"
901        )));
902    }
903    Ok(())
904}
905
906fn permission_group_from_permission(permission: SingleFilePermission) -> SingleFilePermissionGroup {
907    SingleFilePermissionGroup {
908        name: permission.name,
909        identifier: permission.identifier,
910        property_type: permission.property_type,
911        access_mask: permission.access_mask,
912        ace_flags: permission.ace_flags,
913        permissions: Vec::new(),
914    }
915}
916
917fn split_typed_values<'a>(types: &[&str], row: &'a str) -> Vec<&'a str> {
918    let mut values: Vec<&str> = row.split('\t').collect();
919    values.truncate(types.len());
920    values.resize(types.len(), "");
921    values
922}
923
924fn split_exact_typed_values<'a>(types: &[&str], row: &'a str, label: &str) -> Result<Vec<&'a str>> {
925    let values: Vec<&str> = row.split('\t').collect();
926    if values.len() != types.len() {
927        return Err(EwfError::Malformed(format!(
928            "{label} value count does not match type count"
929        )));
930    }
931    Ok(values)
932}
933
934fn parse_type_line<'a>(line: &'a str, label: &str) -> Result<Vec<&'a str>> {
935    let types: Vec<&str> = line.split('\t').collect();
936    if types.is_empty() || types.iter().any(|value_type| value_type.is_empty()) {
937        return Err(EwfError::Malformed(format!("{label} has empty type")));
938    }
939    Ok(types)
940}
941
942fn parse_category_entry_count(line: &str, label: &str) -> Result<usize> {
943    let (first, second) = parse_number_pair(line, label)?;
944    if second != 1 {
945        return Err(EwfError::Malformed(format!(
946            "{label} has unsupported category count shape"
947        )));
948    }
949    usize::try_from(first).map_err(|_| EwfError::Malformed(format!("{label} does not fit usize")))
950}
951
952fn parse_child_entry_count(line: &str, label: &str) -> Result<usize> {
953    let (first, second) = parse_number_pair(line, label)?;
954    if first != 0 {
955        return Err(EwfError::Malformed(format!(
956            "{label} has unsupported child count shape"
957        )));
958    }
959    Ok(second)
960}
961
962fn parse_file_entry_child_count(line: &str, label: &str) -> Result<usize> {
963    let (parent_value, child_count) = parse_number_pair(line, label)?;
964    if parent_value != 0 && parent_value != 26 {
965        return Err(EwfError::Malformed(format!(
966            "{label} has unsupported parent value"
967        )));
968    }
969    Ok(child_count)
970}
971
972fn get_line<'a>(lines: &'a [String], index: usize, label: &str) -> Result<&'a str> {
973    lines
974        .get(index)
975        .map(String::as_str)
976        .ok_or_else(|| EwfError::Malformed(format!("{label} is truncated")))
977}
978
979fn require_empty_category_terminator(lines: &[String], cursor: usize, label: &str) -> Result<()> {
980    if !get_line(lines, cursor, label)?.is_empty() {
981        return Err(EwfError::Malformed(format!("{label} is not empty")));
982    }
983    Ok(())
984}
985
986fn parse_single_char(value: &str, label: &str) -> Result<char> {
987    let mut characters = value.chars();
988    let character = characters
989        .next()
990        .ok_or_else(|| EwfError::Malformed(format!("{label} is empty")))?;
991    if characters.next().is_some() {
992        return Err(EwfError::Malformed(format!(
993            "{label} has more than one character"
994        )));
995    }
996    Ok(character)
997}
998
999struct DecodedUtf16Lines {
1000    lines: Vec<String>,
1001    invalid_utf16_lines: BTreeMap<usize, Vec<u16>>,
1002}
1003
1004fn decode_utf16le_lines(data: &[u8]) -> Result<DecodedUtf16Lines> {
1005    if !data.len().is_multiple_of(2) {
1006        return Err(EwfError::Malformed(
1007            "EWF2 single files data has odd UTF-16 size".into(),
1008        ));
1009    }
1010
1011    let units: Vec<u16> = data
1012        .as_chunks::<2>()
1013        .0
1014        .iter()
1015        .map(|chunk| u16::from_le_bytes(*chunk))
1016        .collect();
1017    let units = units.strip_prefix(&[0xfeff]).unwrap_or(&units);
1018    let mut lines = Vec::new();
1019    let mut invalid = BTreeMap::new();
1020    for raw in units.split(|unit| *unit == u16::from(b'\n')) {
1021        let raw = raw.strip_suffix(&[u16::from(b'\r')]).unwrap_or(raw);
1022        if let Ok(line) = String::from_utf16(raw) {
1023            lines.push(line);
1024        } else {
1025            invalid.insert(lines.len(), raw.to_vec());
1026            lines.push(String::from_utf16_lossy(raw));
1027        }
1028    }
1029    Ok(DecodedUtf16Lines {
1030        lines,
1031        invalid_utf16_lines: invalid,
1032    })
1033}
1034
1035fn parse_entry(
1036    lines: &[String],
1037    types: &[&str],
1038    cursor: &mut usize,
1039    invalid_utf16_lines: &mut BTreeMap<usize, Vec<u16>>,
1040) -> Result<SingleFileEntry> {
1041    let count_line = lines
1042        .get(*cursor)
1043        .ok_or_else(|| EwfError::Malformed("EWF2 single file entry missing count".into()))?;
1044    let child_count =
1045        parse_file_entry_child_count(count_line, "EWF2 single file entry child count")?;
1046    *cursor = cursor
1047        .checked_add(1)
1048        .ok_or_else(|| EwfError::Malformed("EWF2 single file entry index overflow".into()))?;
1049
1050    let value_line_index = *cursor;
1051    let value_line = lines
1052        .get(*cursor)
1053        .ok_or_else(|| EwfError::Malformed("EWF2 single file entry missing values".into()))?;
1054    let values = split_typed_values(types, value_line);
1055    *cursor = cursor
1056        .checked_add(1)
1057        .ok_or_else(|| EwfError::Malformed("EWF2 single file entry index overflow".into()))?;
1058
1059    let mut entry = SingleFileEntry::default();
1060    for (value_type, value) in types.iter().zip(values.iter()) {
1061        apply_entry_value(&mut entry, value_type, value)?;
1062    }
1063    if let Some(raw) = invalid_utf16_lines.remove(&value_line_index) {
1064        for (index, field) in raw.split(|unit| *unit == u16::from(b'\t')).enumerate() {
1065            if String::from_utf16(field).is_err() {
1066                if types.get(index) != Some(&"n") || entry.name_utf16.is_some() {
1067                    return Err(EwfError::Malformed(
1068                        "single files entry has invalid UTF-16 outside its name".into(),
1069                    ));
1070                }
1071                entry.name_utf16 = Some(field.to_vec());
1072            }
1073        }
1074        if entry.name_utf16.is_none() {
1075            return Err(EwfError::Malformed(
1076                "single files entry has invalid UTF-16 outside its name".into(),
1077            ));
1078        }
1079    }
1080
1081    for _ in 0..child_count {
1082        let child = parse_entry(lines, types, cursor, invalid_utf16_lines)?;
1083        entry.children.push(child);
1084    }
1085    Ok(entry)
1086}
1087
1088fn apply_entry_value(entry: &mut SingleFileEntry, value_type: &str, value: &str) -> Result<()> {
1089    if value.is_empty() {
1090        if value_type == "p" {
1091            entry.file_entry_type = Some(SingleFileEntryType::File);
1092        }
1093        return Ok(());
1094    }
1095
1096    match value_type {
1097        "ac" => entry.access_time = Some(parse_i64(value, "access time")?),
1098        "be" => entry.extents = parse_binary_extents(value)?,
1099        "cid" => entry.record_type = Some(parse_u32(value, "record type")?),
1100        "cr" => entry.creation_time = Some(parse_i64(value, "creation time")?),
1101        "dl" => entry.deletion_time = Some(parse_i64(value, "deletion time")?),
1102        "du" => entry.duplicate_data_offset = Some(parse_i64(value, "duplicate data offset")?),
1103        "ea" => entry.attributes = parse_extended_attributes(value)?,
1104        "ha" => entry.md5 = parse_serialized_base16_string(value, "MD5 hash")?,
1105        "id" => entry.identifier = Some(parse_u64(value, "identifier")?),
1106        "lo" => entry.logical_offset = Some(parse_i64(value, "logical offset")?),
1107        "ls" => entry.size = Some(parse_u64(value, "file size")?),
1108        "mid" => entry.guid = parse_serialized_base16_string(value, "GUID")?,
1109        "mo" => entry.entry_modification_time = Some(parse_i64(value, "entry modification time")?),
1110        "n" => entry.name = Some(value.to_owned()),
1111        "opr" => entry.flags = Some(parse_u32(value, "entry flags")?),
1112        "p" => {
1113            entry.file_entry_type = Some(match value {
1114                "f" | "0" => SingleFileEntryType::File,
1115                "d" | "1" => SingleFileEntryType::Directory,
1116                _ => SingleFileEntryType::Unknown,
1117            });
1118        }
1119        "pm" => entry.permission_group_index = Some(parse_i32(value, "permission group index")?),
1120        "po" => entry.physical_offset = Some(parse_i64(value, "physical offset")?),
1121        "sha" => entry.sha1 = parse_serialized_base16_string(value, "SHA1 hash")?,
1122        "snh" => entry.short_name = Some(parse_short_name(value)?),
1123        "src" => {
1124            entry.source_identifier = Some(parse_non_negative_i32(value, "source identifier")?);
1125        }
1126        "sub" => entry.subject_identifier = Some(parse_u32(value, "subject identifier")?),
1127        "wr" => entry.modification_time = Some(parse_i64(value, "modification time")?),
1128        _ => {}
1129    }
1130    Ok(())
1131}
1132
1133fn parse_binary_extents(value: &str) -> Result<Vec<SingleFileExtent>> {
1134    let mut parts = value.split(' ').filter(|part| !part.is_empty()).peekable();
1135    let extent_count = parts
1136        .next()
1137        .ok_or_else(|| EwfError::Malformed("EWF2 single files extents missing count".into()))
1138        .and_then(|part| parse_hex_usize(part, "extent count"))?;
1139    // The declared count is untrusted. Grow only as actual extent pairs parse.
1140    let mut extents = Vec::new();
1141
1142    while parts.peek().is_some() {
1143        let sparse = if parts.peek() == Some(&"S") {
1144            parts.next();
1145            true
1146        } else {
1147            false
1148        };
1149        let data_offset = parts
1150            .next()
1151            .ok_or_else(|| EwfError::Malformed("EWF2 single files extent missing offset".into()))
1152            .and_then(|part| parse_hex_u64(part, "extent offset"))?;
1153        let data_size = parts
1154            .next()
1155            .ok_or_else(|| EwfError::Malformed("EWF2 single files extent missing size".into()))
1156            .and_then(|part| parse_hex_u64(part, "extent size"))?;
1157        extents.push(SingleFileExtent {
1158            data_offset,
1159            data_size,
1160            sparse,
1161        });
1162    }
1163
1164    if extents.len() != extent_count {
1165        return Err(EwfError::Malformed(
1166            "EWF2 single files extent count mismatch".into(),
1167        ));
1168    }
1169    Ok(extents)
1170}
1171
1172fn parse_short_name(value: &str) -> Result<String> {
1173    let mut parts = value.split(' ');
1174    let declared_size = parts
1175        .next()
1176        .ok_or_else(|| EwfError::Malformed("short name is missing size".into()))?;
1177    let short_name = parts
1178        .next()
1179        .ok_or_else(|| EwfError::Malformed("short name is missing value".into()))?;
1180    if parts.next().is_some() {
1181        return Err(EwfError::Malformed(
1182            "short name has unsupported value count".into(),
1183        ));
1184    }
1185    let declared_size = parse_u64(declared_size, "short name size")?;
1186    let actual_size = short_name
1187        .len()
1188        .checked_add(1)
1189        .ok_or_else(|| EwfError::Malformed("short name size overflow".into()))?;
1190    let actual_size = u64::try_from(actual_size)
1191        .map_err(|_| EwfError::Malformed("short name size does not fit u64".into()))?;
1192    if declared_size != actual_size {
1193        return Err(EwfError::Malformed(
1194            "short name size does not match value size".into(),
1195        ));
1196    }
1197    Ok(short_name.to_owned())
1198}
1199
1200fn parse_extended_attributes(value: &str) -> Result<Vec<SingleFileAttribute>> {
1201    let data = parse_hex_byte_stream(value, "extended attributes")?;
1202    if data.is_empty() {
1203        return Ok(Vec::new());
1204    }
1205    if data.len() < EXTENDED_ATTRIBUTES_HEADER.len()
1206        || &data[..EXTENDED_ATTRIBUTES_HEADER.len()] != EXTENDED_ATTRIBUTES_HEADER
1207    {
1208        return Err(EwfError::Malformed(
1209            "EWF2 single files extended attributes header is unsupported".into(),
1210        ));
1211    }
1212
1213    let mut attributes = Vec::new();
1214    let mut offset = 0_usize;
1215    while offset < data.len() {
1216        let (attribute, read_size) = parse_extended_attribute_record(&data[offset..])?;
1217        offset = offset
1218            .checked_add(read_size)
1219            .ok_or_else(|| EwfError::Malformed("extended attribute offset overflow".into()))?;
1220        if !attribute.is_branch {
1221            attributes.push(SingleFileAttribute {
1222                name: attribute.name,
1223                value: attribute.value,
1224            });
1225        }
1226    }
1227    Ok(attributes)
1228}
1229
1230struct ParsedExtendedAttribute {
1231    name: Option<String>,
1232    value: Option<String>,
1233    is_branch: bool,
1234}
1235
1236fn parse_extended_attribute_record(data: &[u8]) -> Result<(ParsedExtendedAttribute, usize)> {
1237    if data.len() < 13 {
1238        return Err(EwfError::Malformed(
1239            "EWF2 single files extended attribute is truncated".into(),
1240        ));
1241    }
1242
1243    let is_branch = data[4] != 0;
1244    let name_units = u32::from_le_bytes(data[5..9].try_into().expect("slice length checked"));
1245    let value_units = u32::from_le_bytes(data[9..13].try_into().expect("slice length checked"));
1246    let name_size = utf16_byte_size(name_units, "extended attribute name")?;
1247    let value_size = utf16_byte_size(value_units, "extended attribute value")?;
1248    let value_offset = 13_usize
1249        .checked_add(name_size)
1250        .ok_or_else(|| EwfError::Malformed("extended attribute value offset overflow".into()))?;
1251    let end_offset = value_offset
1252        .checked_add(value_size)
1253        .ok_or_else(|| EwfError::Malformed("extended attribute end offset overflow".into()))?;
1254    if data.len() < end_offset {
1255        return Err(EwfError::Malformed(
1256            "EWF2 single files extended attribute is truncated".into(),
1257        ));
1258    }
1259
1260    let name = decode_optional_utf16le_string(&data[13..value_offset], "extended attribute name")?;
1261    let value = decode_optional_utf16le_string(
1262        &data[value_offset..end_offset],
1263        "extended attribute value",
1264    )?;
1265
1266    Ok((
1267        ParsedExtendedAttribute {
1268            name,
1269            value,
1270            is_branch,
1271        },
1272        end_offset,
1273    ))
1274}
1275
1276fn utf16_byte_size(units: u32, label: &str) -> Result<usize> {
1277    let units = usize::try_from(units)
1278        .map_err(|_| EwfError::Malformed(format!("{label} size does not fit usize")))?;
1279    units
1280        .checked_mul(2)
1281        .ok_or_else(|| EwfError::Malformed(format!("{label} size overflow")))
1282}
1283
1284fn decode_optional_utf16le_string(data: &[u8], label: &str) -> Result<Option<String>> {
1285    if data.is_empty() {
1286        return Ok(None);
1287    }
1288    if !data.len().is_multiple_of(2) {
1289        return Err(EwfError::Malformed(format!("{label} has odd UTF-16 size")));
1290    }
1291
1292    let units: Vec<u16> = data
1293        .as_chunks::<2>()
1294        .0
1295        .iter()
1296        .map(|chunk| u16::from_le_bytes(*chunk))
1297        .collect();
1298    let mut text = String::from_utf16(&units)
1299        .map_err(|_| EwfError::Malformed(format!("{label} is not valid UTF-16LE")))?;
1300    if text.ends_with('\0') {
1301        text.pop();
1302    }
1303    if text.is_empty() {
1304        Ok(None)
1305    } else {
1306        Ok(Some(text))
1307    }
1308}
1309
1310fn parse_hex_byte_stream(value: &str, label: &str) -> Result<Vec<u8>> {
1311    let hex: String = value
1312        .chars()
1313        .filter(|character| !character.is_whitespace())
1314        .collect();
1315    if !hex.len().is_multiple_of(2) {
1316        return Err(EwfError::Malformed(format!(
1317            "EWF2 single files {label} has odd hexadecimal size"
1318        )));
1319    }
1320    if !hex.is_ascii() {
1321        return Err(EwfError::Malformed(format!(
1322            "invalid EWF2 single files {label} hexadecimal value"
1323        )));
1324    }
1325
1326    (0..hex.len())
1327        .step_by(2)
1328        .map(|index| {
1329            u8::from_str_radix(&hex[index..index + 2], 16).map_err(|_| {
1330                EwfError::Malformed(format!(
1331                    "invalid EWF2 single files {label} hexadecimal value"
1332                ))
1333            })
1334        })
1335        .collect()
1336}
1337
1338fn parse_serialized_base16_string(value: &str, label: &str) -> Result<Option<String>> {
1339    if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1340        return Err(EwfError::Malformed(format!(
1341            "invalid EWF2 single files {label} hexadecimal value"
1342        )));
1343    }
1344    if value.bytes().all(|byte| byte == b'0') {
1345        Ok(None)
1346    } else {
1347        Ok(Some(value.to_ascii_lowercase()))
1348    }
1349}
1350
1351fn parse_number_pair(line: &str, label: &str) -> Result<(u64, usize)> {
1352    let mut parts = line.split('\t');
1353    let first = parts
1354        .next()
1355        .ok_or_else(|| EwfError::Malformed(format!("{label} is missing first value")))?;
1356    let second = parts
1357        .next()
1358        .ok_or_else(|| EwfError::Malformed(format!("{label} is missing second value")))?;
1359    if parts.next().is_some() {
1360        return Err(EwfError::Malformed(format!("{label} has too many values")));
1361    }
1362    let first = parse_u64(first, label)?;
1363    let second = parse_u64(second, label)?;
1364    let second = usize::try_from(second)
1365        .map_err(|_| EwfError::Malformed(format!("{label} does not fit usize")))?;
1366    Ok((first, second))
1367}
1368
1369fn parse_hex_usize(value: &str, label: &str) -> Result<usize> {
1370    let value = parse_hex_u64(value, label)?;
1371    usize::try_from(value)
1372        .map_err(|_| EwfError::Malformed(format!("EWF2 single files {label} out of bounds")))
1373}
1374
1375fn parse_hex_u64(value: &str, label: &str) -> Result<u64> {
1376    u64::from_str_radix(value, 16)
1377        .map_err(|_| EwfError::Malformed(format!("invalid EWF2 single files {label} value")))
1378}
1379
1380fn parse_u64(value: &str, label: &str) -> Result<u64> {
1381    value
1382        .parse()
1383        .map_err(|_| EwfError::Malformed(format!("invalid EWF2 single files {label} value")))
1384}
1385
1386fn parse_u32(value: &str, label: &str) -> Result<u32> {
1387    parse_u64(value, label).and_then(|value| {
1388        u32::try_from(value)
1389            .map_err(|_| EwfError::Malformed(format!("EWF2 single files {label} out of bounds")))
1390    })
1391}
1392
1393fn parse_i64(value: &str, label: &str) -> Result<i64> {
1394    value
1395        .parse()
1396        .map_err(|_| EwfError::Malformed(format!("invalid EWF2 single files {label} value")))
1397}
1398
1399fn parse_i32(value: &str, label: &str) -> Result<i32> {
1400    parse_i64(value, label).and_then(|value| {
1401        i32::try_from(value)
1402            .map_err(|_| EwfError::Malformed(format!("EWF2 single files {label} out of bounds")))
1403    })
1404}
1405
1406fn parse_non_negative_i32(value: &str, label: &str) -> Result<i32> {
1407    let value = parse_i32(value, label)?;
1408    if value < 0 {
1409        return Err(EwfError::Malformed(format!(
1410            "EWF2 single files {label} out of bounds"
1411        )));
1412    }
1413    Ok(value)
1414}
1415
1416#[cfg(test)]
1417mod entry_type_tests {
1418    use super::*;
1419
1420    #[test]
1421    fn standard_and_legacy_entry_types() {
1422        for (value, expected) in [
1423            ("", SingleFileEntryType::File),
1424            ("0", SingleFileEntryType::File),
1425            ("1", SingleFileEntryType::Directory),
1426            ("f", SingleFileEntryType::File),
1427            ("d", SingleFileEntryType::Directory),
1428            ("x", SingleFileEntryType::Unknown),
1429        ] {
1430            let mut entry = SingleFileEntry::default();
1431            apply_entry_value(&mut entry, "p", value).unwrap();
1432            assert_eq!(entry.file_entry_type, Some(expected));
1433        }
1434    }
1435
1436    #[test]
1437    fn unbacked_catalog_counts_do_not_reserve_memory() {
1438        assert!(parse_binary_extents("ffffffffffffffff").is_err());
1439        assert!(parse_binary_extents("4000000 0 1").is_err());
1440    }
1441
1442    #[test]
1443    fn extended_attribute_hex_rejects_multibyte_characters() {
1444        assert!(parse_hex_byte_stream("䀊a", "extended attributes").is_err());
1445        assert_eq!(
1446            parse_hex_byte_stream("ab cd", "extended attributes").unwrap(),
1447            vec![0xab, 0xcd]
1448        );
1449    }
1450}