use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::date_time::format_header_date_value;
use crate::decode::{ChunkEncoding, decode_chunk};
use crate::{EwfError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Ewf1,
Ewf2,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FormatProfile {
#[default]
Unknown,
EnCase1,
EnCase2,
EnCase3,
EnCase4,
EnCase5,
EnCase6,
EnCase7,
Smart,
FtkImager,
Linen5,
Linen6,
Linen7,
LogicalEnCase5,
LogicalEnCase6,
LogicalEnCase7,
Ewf2EnCase7,
Ewf2LogicalEnCase7,
Ewf,
Ewfx,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionMethod {
None,
Zlib,
Bzip2,
Unknown(u16),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CompressionLevel {
Default,
#[default]
None,
Fast,
Best,
Unknown(i8),
}
impl CompressionLevel {
pub fn as_i8(self) -> i8 {
match self {
Self::Default => -1,
Self::None => 0,
Self::Fast => 1,
Self::Best => 2,
Self::Unknown(value) => value,
}
}
pub fn from_i8(value: i8) -> Self {
match value {
-1 => Self::Default,
0 => Self::None,
1 => Self::Fast,
2 => Self::Best,
value => Self::Unknown(value),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CompressionFlags {
pub empty_block: bool,
pub pattern_fill: bool,
pub unknown_bits: u8,
}
impl CompressionFlags {
const EMPTY_BLOCK: u8 = 0x01;
const PATTERN_FILL: u8 = 0x10;
const KNOWN_BITS: u8 = Self::EMPTY_BLOCK | Self::PATTERN_FILL;
pub fn from_bits(bits: u8) -> Self {
Self {
empty_block: bits & Self::EMPTY_BLOCK != 0,
pattern_fill: bits & Self::PATTERN_FILL != 0,
unknown_bits: bits & !Self::KNOWN_BITS,
}
}
pub fn bits(self) -> u8 {
let mut bits = self.unknown_bits;
if self.empty_block {
bits |= Self::EMPTY_BLOCK;
}
if self.pattern_fill {
bits |= Self::PATTERN_FILL;
}
bits
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CompressionValues {
pub level: CompressionLevel,
pub flags: CompressionFlags,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaType {
Removable,
Fixed,
Optical,
SingleFiles,
Memory,
Unknown(u8),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MediaFlags {
pub physical: bool,
pub fastbloc: bool,
pub tableau: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SegmentFileVersion {
pub major: u8,
pub minor: u8,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MediaInfo {
pub sectors_per_chunk: Option<u64>,
pub bytes_per_sector: Option<u64>,
pub sector_count: Option<u64>,
pub chunk_count: Option<u64>,
pub error_granularity: Option<u64>,
pub set_identifier: Option<[u8; 16]>,
pub ewf2_segment_file_version: Option<SegmentFileVersion>,
pub compression_method: Option<CompressionMethod>,
pub compression_values: CompressionValues,
pub media_type: Option<MediaType>,
pub media_flags: MediaFlags,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenStrictness {
Strict,
Lenient,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpenOptions {
pub strictness: OpenStrictness,
pub chunk_cache_size: usize,
pub read_zero_chunk_on_error: bool,
pub header_codepage: HeaderCodepage,
pub header_values_date_format: HeaderDateFormat,
pub maximum_open_handles: Option<usize>,
}
impl Default for OpenOptions {
fn default() -> Self {
Self {
strictness: OpenStrictness::Strict,
chunk_cache_size: 64,
read_zero_chunk_on_error: false,
header_codepage: HeaderCodepage::Ascii,
header_values_date_format: HeaderDateFormat::Ctime,
maximum_open_handles: None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum HeaderCodepage {
#[default]
Ascii,
Windows874,
Windows932,
Windows936,
Windows1250,
Windows1251,
Windows1252,
Windows1253,
Windows1254,
Windows1255,
Windows1256,
Windows1257,
Windows1258,
}
impl HeaderCodepage {
pub fn as_i32(self) -> i32 {
match self {
Self::Ascii => 20_127,
Self::Windows874 => 874,
Self::Windows932 => 932,
Self::Windows936 => 936,
Self::Windows1250 => 1250,
Self::Windows1251 => 1251,
Self::Windows1252 => 1252,
Self::Windows1253 => 1253,
Self::Windows1254 => 1254,
Self::Windows1255 => 1255,
Self::Windows1256 => 1256,
Self::Windows1257 => 1257,
Self::Windows1258 => 1258,
}
}
pub fn from_i32(value: i32) -> Option<Self> {
Some(match value {
20_127 => Self::Ascii,
874 => Self::Windows874,
932 => Self::Windows932,
936 => Self::Windows936,
1250 => Self::Windows1250,
1251 => Self::Windows1251,
1252 => Self::Windows1252,
1253 => Self::Windows1253,
1254 => Self::Windows1254,
1255 => Self::Windows1255,
1256 => Self::Windows1256,
1257 => Self::Windows1257,
1258 => Self::Windows1258,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum HeaderDateFormat {
DayMonth,
MonthDay,
Iso8601,
#[default]
Ctime,
}
impl HeaderDateFormat {
pub fn as_i32(self) -> i32 {
match self {
Self::DayMonth => 1,
Self::MonthDay => 2,
Self::Iso8601 => 3,
Self::Ctime => 4,
}
}
pub fn from_i32(value: i32) -> Option<Self> {
Some(match value {
1 => Self::DayMonth,
2 => Self::MonthDay,
3 => Self::Iso8601,
4 => Self::Ctime,
_ => return None,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EwfMetadata {
pub case_number: Option<String>,
pub evidence_number: Option<String>,
pub examiner: Option<String>,
pub description: Option<String>,
pub notes: Option<String>,
pub acquisition_software: Option<String>,
pub acquisition_software_version: Option<String>,
pub os_version: Option<String>,
pub acquisition_date: Option<String>,
pub system_date: Option<String>,
pub password: Option<String>,
pub header_values: BTreeMap<String, String>,
}
impl EwfMetadata {
pub fn copy_header_values_from(&mut self, source: &EwfMetadata) {
*self = source.clone();
}
pub fn header_value(&self, identifier: &str) -> Option<&str> {
self.standard_header_value(identifier)
.or_else(|| self.header_values.get(identifier).map(String::as_str))
}
pub fn header_value_with_date_format(
&self,
identifier: &str,
date_format: HeaderDateFormat,
) -> Option<Cow<'_, str>> {
let value = self.header_value(identifier)?;
if matches!(identifier, "acquiry_date" | "system_date") {
Some(format_header_date_value(value, date_format))
} else {
Some(Cow::Borrowed(value))
}
}
pub fn set_header_value(
&mut self,
identifier: &str,
value: impl Into<String>,
) -> Option<String> {
let previous = self.header_value(identifier).map(str::to_owned);
let value = value.into();
self.header_values.remove(identifier);
match identifier {
"case_number" => self.case_number = Some(value),
"description" => self.description = Some(value),
"examiner_name" => self.examiner = Some(value),
"evidence_number" => self.evidence_number = Some(value),
"notes" => self.notes = Some(value),
"acquiry_date" => self.acquisition_date = Some(value),
"system_date" => self.system_date = Some(value),
"acquiry_operating_system" => self.os_version = Some(value),
"acquiry_software" => self.acquisition_software = Some(value),
"acquiry_software_version" => self.acquisition_software_version = Some(value),
"password" => self.password = Some(value),
_ => {
self.header_values.insert(identifier.to_string(), value);
}
}
previous
}
pub fn number_of_header_values(&self) -> usize {
let standard_count = STANDARD_HEADER_VALUE_IDENTIFIERS
.iter()
.filter(|identifier| self.header_value(identifier).is_some())
.count();
let generic_count = self
.header_values
.keys()
.filter(|identifier| !is_standard_header_value_identifier(identifier))
.count();
standard_count + generic_count
}
pub fn header_value_identifier(&self, index: usize) -> Option<&str> {
let mut remaining = index;
for identifier in STANDARD_HEADER_VALUE_IDENTIFIERS {
if self.header_value(identifier).is_some() {
if remaining == 0 {
return Some(identifier);
}
remaining -= 1;
}
}
for identifier in self.header_values.keys() {
if is_standard_header_value_identifier(identifier) {
continue;
}
if remaining == 0 {
return Some(identifier);
}
remaining -= 1;
}
None
}
fn standard_header_value(&self, identifier: &str) -> Option<&str> {
match identifier {
"case_number" => self.case_number.as_deref(),
"description" => self.description.as_deref(),
"examiner_name" => self.examiner.as_deref(),
"evidence_number" => self.evidence_number.as_deref(),
"notes" => self.notes.as_deref(),
"acquiry_date" => self.acquisition_date.as_deref(),
"system_date" => self.system_date.as_deref(),
"acquiry_operating_system" => self.os_version.as_deref(),
"acquiry_software" => self.acquisition_software.as_deref(),
"acquiry_software_version" => self.acquisition_software_version.as_deref(),
"password" => self.password.as_deref(),
_ => None,
}
}
}
const STANDARD_HEADER_VALUE_IDENTIFIERS: &[&str] = &[
"case_number",
"description",
"examiner_name",
"evidence_number",
"notes",
"acquiry_date",
"system_date",
"acquiry_operating_system",
"acquiry_software",
"acquiry_software_version",
"password",
"compression_level",
"model",
"serial_number",
];
fn is_standard_header_value_identifier(identifier: &str) -> bool {
STANDARD_HEADER_VALUE_IDENTIFIERS.contains(&identifier)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StoredHashes {
pub md5: Option<[u8; 16]>,
pub sha1: Option<[u8; 20]>,
pub hash_values: BTreeMap<String, String>,
}
impl StoredHashes {
pub fn hash_value(&self, identifier: &str) -> Option<&str> {
self.hash_values.get(identifier).map(String::as_str)
}
pub fn set_hash_value(
&mut self,
identifier: impl Into<String>,
value: impl Into<String>,
) -> Option<String> {
let identifier = identifier.into();
let value = value.into();
set_typed_hash_value(&identifier, &value, &mut self.md5, &mut self.sha1);
self.hash_values.insert(identifier, value)
}
pub fn number_of_hash_values(&self) -> usize {
self.hash_values.len()
}
pub fn hash_value_identifier(&self, index: usize) -> Option<&str> {
self.hash_values.keys().nth(index).map(String::as_str)
}
}
pub(crate) fn set_typed_hash_value(
identifier: &str,
value: &str,
md5: &mut Option<[u8; 16]>,
sha1: &mut Option<[u8; 20]>,
) {
if identifier.eq_ignore_ascii_case("MD5") {
if let Some(parsed) = parse_hex_array(value) {
*md5 = Some(parsed);
}
} else if identifier.eq_ignore_ascii_case("SHA1")
&& let Some(parsed) = parse_hex_array(value)
{
*sha1 = Some(parsed);
}
}
fn parse_hex_array<const N: usize>(text: &str) -> Option<[u8; N]> {
if text.len() != N * 2 {
return None;
}
let mut bytes = [0; N];
for (index, pair) in text.as_bytes().chunks_exact(2).enumerate() {
let high = hex_nibble(pair[0])?;
let low = hex_nibble(pair[1])?;
bytes[index] = (high << 4) | low;
}
Some(bytes)
}
fn hex_nibble(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
b'A'..=b'F' => Some(value - b'A' + 10),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcquisitionError {
pub first_sector: u64,
pub sector_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectorRange {
pub first_sector: u64,
pub sector_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryExtent {
pub start_page: u64,
pub page_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataChunkEncoding {
Raw,
Zlib,
Bzip2,
PatternFill(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataChunk {
pub chunk_index: u64,
pub logical_offset: u64,
pub logical_size: usize,
pub encoded_size: u64,
pub encoding: DataChunkEncoding,
pub corrupted: bool,
pub data: Vec<u8>,
}
impl DataChunk {
pub fn is_corrupted(&self) -> bool {
self.corrupted
}
pub fn read_buffer(&self, buffer: &mut [u8]) -> Result<usize> {
Ok(copy_to_buffer(&self.data, buffer))
}
pub fn write_buffer(&mut self, buffer: &[u8]) -> Result<usize> {
self.data.clear();
self.data.extend_from_slice(buffer);
self.logical_size = buffer.len();
self.encoded_size = u64::try_from(buffer.len())
.map_err(|_| EwfError::Malformed("data chunk buffer size overflow".into()))?;
self.encoding = DataChunkEncoding::Raw;
self.corrupted = false;
Ok(buffer.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedDataChunk {
pub chunk_index: u64,
pub logical_offset: u64,
pub logical_size: usize,
pub encoded_size: u64,
pub encoding: DataChunkEncoding,
pub has_checksum: bool,
pub data: Vec<u8>,
}
impl EncodedDataChunk {
pub fn read_buffer(&self, buffer: &mut [u8]) -> Result<usize> {
if self.has_checksum && self.encoding == DataChunkEncoding::Raw {
validate_raw_data_chunk_checksum(&self.data, self.logical_size)?;
}
let decoded = decode_chunk(
&self.data,
data_chunk_encoding(self.encoding),
self.logical_size,
)?;
Ok(copy_to_buffer(&decoded, buffer))
}
}
fn copy_to_buffer(data: &[u8], buffer: &mut [u8]) -> usize {
let read_size = data.len().min(buffer.len());
buffer[..read_size].copy_from_slice(&data[..read_size]);
read_size
}
fn data_chunk_encoding(encoding: DataChunkEncoding) -> ChunkEncoding {
match encoding {
DataChunkEncoding::Raw => ChunkEncoding::Raw,
DataChunkEncoding::Zlib => ChunkEncoding::Zlib,
DataChunkEncoding::Bzip2 => ChunkEncoding::Bzip2,
DataChunkEncoding::PatternFill(pattern) => ChunkEncoding::PatternFill(pattern),
}
}
fn validate_raw_data_chunk_checksum(encoded: &[u8], logical_size: usize) -> Result<()> {
let checksum_end = logical_size
.checked_add(4)
.ok_or_else(|| EwfError::Malformed("raw chunk checksum offset overflow".into()))?;
if encoded.len() < checksum_end {
return Err(EwfError::Malformed(
"raw chunk checksum trailer is missing".into(),
));
}
let stored = u32::from_le_bytes(
encoded[logical_size..checksum_end]
.try_into()
.expect("raw chunk checksum slice has fixed size"),
);
let calculated = adler32(&encoded[..logical_size]);
if stored != calculated {
return Err(EwfError::Malformed("raw chunk checksum mismatch".into()));
}
Ok(())
}
fn adler32(data: &[u8]) -> u32 {
const MOD_ADLER: u32 = 65_521;
let mut a = 1_u32;
let mut b = 0_u32;
for byte in data {
a = (a + u32::from(*byte)) % MOD_ADLER;
b = (b + a) % MOD_ADLER;
}
(b << 16) | a
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SingleFileEntryType {
File,
Directory,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SingleFileExtent {
pub data_offset: u64,
pub data_size: u64,
pub sparse: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFileSource {
pub identifier: Option<i32>,
pub name: Option<String>,
pub evidence_number: Option<String>,
pub location: Option<String>,
pub device_guid: Option<String>,
pub primary_device_guid: Option<String>,
pub drive_type: Option<char>,
pub manufacturer: Option<String>,
pub model: Option<String>,
pub serial_number: Option<String>,
pub domain: Option<String>,
pub ip_address: Option<String>,
pub mac_address: Option<String>,
pub size: Option<u64>,
pub logical_offset: Option<i64>,
pub physical_offset: Option<i64>,
pub acquisition_time: Option<i64>,
pub md5: Option<String>,
pub sha1: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFileSubject {
pub identifier: Option<u32>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFilePermission {
pub name: Option<String>,
pub identifier: Option<String>,
pub property_type: Option<u32>,
pub access_mask: Option<u32>,
pub ace_flags: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFilePermissionGroup {
pub name: Option<String>,
pub identifier: Option<String>,
pub property_type: Option<u32>,
pub access_mask: Option<u32>,
pub ace_flags: Option<u32>,
pub permissions: Vec<SingleFilePermission>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFileAttribute {
pub name: Option<String>,
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFileEntry {
pub identifier: Option<u64>,
pub file_entry_type: Option<SingleFileEntryType>,
pub flags: Option<u32>,
pub guid: Option<String>,
pub name: Option<String>,
pub short_name: Option<String>,
pub size: Option<u64>,
pub logical_offset: Option<i64>,
pub physical_offset: Option<i64>,
pub duplicate_data_offset: Option<i64>,
pub source_identifier: Option<i32>,
pub subject_identifier: Option<u32>,
pub permission_group_index: Option<i32>,
pub record_type: Option<u32>,
pub creation_time: Option<i64>,
pub modification_time: Option<i64>,
pub access_time: Option<i64>,
pub entry_modification_time: Option<i64>,
pub deletion_time: Option<i64>,
pub md5: Option<String>,
pub sha1: Option<String>,
pub extents: Vec<SingleFileExtent>,
pub attributes: Vec<SingleFileAttribute>,
pub children: Vec<SingleFileEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFilesInfo {
pub data_size: u64,
pub root: SingleFileEntry,
pub sources: Vec<SingleFileSource>,
pub subjects: Vec<SingleFileSubject>,
pub permission_groups: Vec<SingleFilePermissionGroup>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SingleFilesAuxTables {
pub table_0x21_entries: Vec<u64>,
pub md5_hashes: Vec<[u8; 16]>,
pub table_0x23_entries: Vec<u64>,
}
impl SingleFilesAuxTables {
pub fn is_empty(&self) -> bool {
self.table_0x21_entries.is_empty()
&& self.md5_hashes.is_empty()
&& self.table_0x23_entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageInfo {
pub format: Format,
pub format_profile: FormatProfile,
pub segment_count: usize,
pub segment_paths: Vec<PathBuf>,
pub chunk_size: u64,
pub logical_size: u64,
pub acquisition_complete: bool,
pub header_codepage: HeaderCodepage,
pub header_values_date_format: HeaderDateFormat,
pub media: MediaInfo,
pub metadata: EwfMetadata,
pub stored_hashes: StoredHashes,
pub acquisition_errors: Vec<AcquisitionError>,
pub memory_extents: Vec<MemoryExtent>,
pub single_files: Option<SingleFilesInfo>,
pub ewf2_single_files_tables: SingleFilesAuxTables,
pub ewf2_increment_data: Vec<Vec<u8>>,
pub ewf2_final_information: Option<Vec<u8>>,
pub ewf2_restart_data: Option<String>,
pub ewf2_analytical_data: Option<String>,
pub sessions: Vec<SectorRange>,
pub tracks: Vec<SectorRange>,
}
#[cfg(feature = "verify")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyResult {
pub computed_md5: Option<[u8; 16]>,
pub computed_sha1: Option<[u8; 20]>,
pub md5_match: Option<bool>,
pub sha1_match: Option<bool>,
}