diff --git a/Aaru.Images/EWF/Consts.cs b/Aaru.Images/EWF/Consts.cs new file mode 100644 index 000000000..9433e84ef --- /dev/null +++ b/Aaru.Images/EWF/Consts.cs @@ -0,0 +1,110 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Consts.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains constants for Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Diagnostics.CodeAnalysis; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public sealed partial class Ewf +{ + /// EWF v1 disk image signature: "EVF\x09\x0D\x0A\xFF\x00" + static readonly byte[] EVF_SIGNATURE = [0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]; + + /// EWF v1 logical evidence signature: "LVF\x09\x0D\x0A\xFF\x00" + static readonly byte[] LVF_SIGNATURE = [0x4C, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]; + + /// EWF v2 disk image signature: "EVF2\x0D\x0A\x81\x00" + static readonly byte[] EVF2_SIGNATURE = [0x45, 0x56, 0x46, 0x32, 0x0D, 0x0A, 0x81, 0x00]; + + /// EWF v2 logical evidence signature: "LEF2\x0D\x0A\x81\x00" + static readonly byte[] LEF2_SIGNATURE = [0x4C, 0x45, 0x46, 0x32, 0x0D, 0x0A, 0x81, 0x00]; + + /// SMART volume section signature + static readonly byte[] SMART_SIGNATURE = [0x53, 0x4D, 0x41, 0x52, 0x54]; + + const int SIGNATURE_LENGTH = 8; + + /// Section type strings for EWF v1 + const string SECTION_TYPE_HEADER = "header"; + const string SECTION_TYPE_HEADER2 = "header2"; + const string SECTION_TYPE_VOLUME = "volume"; + const string SECTION_TYPE_DATA = "data"; + const string SECTION_TYPE_TABLE = "table"; + const string SECTION_TYPE_TABLE2 = "table2"; + const string SECTION_TYPE_SECTORS = "sectors"; + const string SECTION_TYPE_HASH = "hash"; + const string SECTION_TYPE_DIGEST = "digest"; + const string SECTION_TYPE_ERROR2 = "error2"; + const string SECTION_TYPE_SESSION = "session"; + const string SECTION_TYPE_DONE = "done"; + const string SECTION_TYPE_NEXT = "next"; + + /// EWF v1 section descriptor size + const int SECTION_DESCRIPTOR_V1_SIZE = 76; + + /// Default sectors per chunk + const uint DEFAULT_SECTORS_PER_CHUNK = 64; + + /// Default bytes per sector + const uint DEFAULT_BYTES_PER_SECTOR = 512; + + /// Default chunk size (64 * 512 = 32768) + const uint DEFAULT_CHUNK_SIZE = DEFAULT_SECTORS_PER_CHUNK * DEFAULT_BYTES_PER_SECTOR; + + /// Compression flag in EWF v1 table entry (most significant bit) + const uint TABLE_ENTRY_V1_COMPRESSED_FLAG = 0x80000000; + + /// Offset mask for EWF v1 table entry (lower 31 bits) + const uint TABLE_ENTRY_V1_OFFSET_MASK = 0x7FFFFFFF; + + /// Maximum cached chunks + const int MAX_CACHE_SIZE = 16777216; + + /// Maximum cached sectors + const int MAX_CACHED_SECTORS = MAX_CACHE_SIZE / 512; + + /// EWF v1 file header size + const int FILE_HEADER_V1_SIZE = 13; + + /// EWF v2 file header size + const int FILE_HEADER_V2_SIZE = 32; + + /// EWF v1 EnCase 5+ volume section data size + const int VOLUME_SECTION_SIZE_ENCASE = 1052; + + /// EWF v1 SMART/old EnCase volume section data size + const int VOLUME_SECTION_SIZE_SMART = 94; + + /// Session entry flag: audio track + const uint SESSION_ENTRY_FLAG_IS_AUDIO = 0x00000001; +} \ No newline at end of file diff --git a/Aaru.Images/EWF/EWF.cs b/Aaru.Images/EWF/EWF.cs new file mode 100644 index 000000000..79c817ddf --- /dev/null +++ b/Aaru.Images/EWF/EWF.cs @@ -0,0 +1,121 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : EWF.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Collections.Generic; +using System.IO; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +/// +/// Implements reading Expert Witness Format (EWF) disk images +public sealed partial class Ewf : IOpticalMediaImage +{ + const string MODULE_NAME = "EWF plugin"; + + /// List of bad sector ranges from error2 sections + List<(ulong start, uint count)> _badSectors; + uint _bytesPerSector; + + /// Cache of decompressed chunks + Dictionary _chunkCache; + uint _chunkSize; + + /// Map from chunk index to location in segment file + Dictionary _chunkTable; + + /// Compression method for EWF v2 + EwfCompressionMethod _compressionMethod; + + /// Parsed header metadata key/value pairs + Dictionary _headerValues; + + ImageInfo _imageInfo; + + /// Whether the image uses the SMART volume section format + bool _isSmart; + + /// Whether the image is EWF v2 format + bool _isV2; + + int _maxChunkCache; + + /// Stored MD5 hash from hash/digest section + byte[] _md5Stored; + + /// Populated metadata from header sections + Metadata _metadata; + + /// Cache of read sectors + Dictionary _sectorCache; + + uint _sectorsPerChunk; + + /// Ordered list of open segment file streams + List _segmentStreams; + + /// Session list for optical media + List _sessions; + + /// Stored SHA1 hash from digest section + byte[] _sha1Stored; + + /// Track list for optical media + List _tracks; + + public Ewf() => _imageInfo = new ImageInfo + { + ReadableSectorTags = [], + ReadableMediaTags = [], + HasPartitions = false, + HasSessions = false, + Version = null, + Application = null, + ApplicationVersion = null, + Creator = null, + Comments = null, + MediaManufacturer = null, + MediaModel = null, + MediaSerialNumber = null, + MediaBarcode = null, + MediaPartNumber = null, + MediaSequence = 0, + LastMediaSequence = 0, + DriveManufacturer = null, + DriveModel = null, + DriveSerialNumber = null, + DriveFirmwareRevision = null + }; +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Enums.cs b/Aaru.Images/EWF/Enums.cs new file mode 100644 index 000000000..5c4d5e752 --- /dev/null +++ b/Aaru.Images/EWF/Enums.cs @@ -0,0 +1,99 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Enums.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains enumerations for Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Diagnostics.CodeAnalysis; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public sealed partial class Ewf +{ + /// EWF media type as stored in the volume section + enum EwfMediaType : byte + { + Removable = 0x00, + Fixed = 0x01, + Optical = 0x03, + SingleFiles = 0x0E, + Memory = 0x10 + } + + /// EWF media flags as stored in the volume section + enum EwfMediaFlags : byte + { + Image = 0x01, + Physical = 0x02, + Fastbloc = 0x04, + Tableau = 0x08 + } + + /// EWF v1 compression level + enum EwfCompressionLevel : byte + { + None = 0x00, + Fast = 0x01, + Best = 0x02 + } + + /// EWF v2 compression method as stored in the file header + enum EwfCompressionMethod : ushort + { + None = 0, + Deflate = 1, + Bzip2 = 2 + } + + /// EWF v2 section type codes + enum EwfSectionTypeV2 : uint + { + DeviceInformation = 0x00000001, + CaseData = 0x00000002, + SectorData = 0x00000003, + SectorTable = 0x00000004, + ErrorTable = 0x00000005, + SessionTable = 0x00000006, + IncrementData = 0x00000007, + Md5Hash = 0x00000008, + Sha1Hash = 0x00000009, + RestartData = 0x0000000A, + EncryptionKeys = 0x0000000B, + MemoryExtentsTable = 0x0000000C, + Next = 0x0000000D, + FinalInformation = 0x0000000E, + Done = 0x0000000F, + AnalyticalData = 0x00000010, + SingleFilesData = 0x00000020, + SingleFilesTree = 0x00000021, + SingleFilesMetadata = 0x00000022, + SingleFilesSource = 0x00000023 + } +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Helpers.cs b/Aaru.Images/EWF/Helpers.cs new file mode 100644 index 000000000..64f9f44af --- /dev/null +++ b/Aaru.Images/EWF/Helpers.cs @@ -0,0 +1,342 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Helpers.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains helper methods for Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ + /// + /// Generates the next segment filename following EWF naming conventions. + /// E01→E02→...→E99→EAA→EAB→...→EZZ→FAA→...→ZZZ for v1 EnCase. + /// Ex01→Ex02→...→Ex99→ExAA→...→ExZZ for v2. + /// s01→s02→...→s99→saa→...→szz for SMART. + /// + static string GetNextSegmentFilename(string currentPath) + { + string dir = Path.GetDirectoryName(currentPath) ?? ""; + string name = Path.GetFileNameWithoutExtension(currentPath); + string ext = Path.GetExtension(currentPath); + + if(string.IsNullOrEmpty(ext) || ext.Length < 2) return null; + + // Determine if we're in v2 mode (.Ex01 has 5-char extension) + bool isV2 = ext.Length == 5; // .Ex01 + bool isLower = char.IsLower(ext[1]); + + if(isV2) + { + // .Ex01 → .Ex02 → ... → .Ex99 → .ExAA → ... → .ExZZ + string numPart = ext.Substring(3); // "01" from ".Ex01" + char prefix1 = ext[1]; // 'E' + char prefix2 = ext[2]; // 'x' + + if(int.TryParse(numPart, out int num) && num < 99) + return Path.Combine(dir, name + ext.Substring(0, 3) + (num + 1).ToString("D2")); + + if(int.TryParse(numPart, out _)) + { + // 99 → AA + char a = isLower ? 'a' : 'A'; + + return Path.Combine(dir, name + $".{prefix1}{prefix2}{a}{a}"); + } + + // AA → AB → ... → AZ → BA → ... → ZZ + char c1 = ext[3]; + char c2 = ext[4]; + + char baseChar = isLower ? 'a' : 'A'; + char maxChar = isLower ? 'z' : 'Z'; + + if(c2 < maxChar) return Path.Combine(dir, name + $".{prefix1}{prefix2}{c1}{(char)(c2 + 1)}"); + + if(c1 < maxChar) return Path.Combine(dir, name + $".{prefix1}{prefix2}{(char)(c1 + 1)}{baseChar}"); + + return null; // ZZ reached + } + + // v1: .E01 → .E02 → ... → .E99 → .EAA → ... → .EZZ → .FAA → ... → .ZZZ + // SMART: .s01 → .s02 → ... → .s99 → .saa → ... → .szz + char firstChar = ext[1]; // 'E' or 's' + string suffix = ext.Substring(2); // "01" from ".E01" + + if(int.TryParse(suffix, out int segNum) && segNum < 99) + return Path.Combine(dir, name + $".{firstChar}{segNum + 1:D2}"); + + if(int.TryParse(suffix, out _)) + { + // 99 → AA (or aa for lowercase) + char a = isLower ? 'a' : 'A'; + + return Path.Combine(dir, name + $".{firstChar}{a}{a}"); + } + + // Letter suffixes: EAA → EAB → ... → EAZ → EBA → ... → EZZ → FAA → ... → ZZZ + char ch1 = ext[1]; // first letter (E, F, ..., Z) or (s for SMART - but SMART only goes to szz) + char ch2 = ext[2]; // second letter + char ch3 = ext[3]; // third letter + + char lBase = isLower ? 'a' : 'A'; + char lMax = isLower ? 'z' : 'Z'; + + if(ch3 < lMax) return Path.Combine(dir, name + $".{ch1}{ch2}{(char)(ch3 + 1)}"); + + if(ch2 < lMax) return Path.Combine(dir, name + $".{ch1}{(char)(ch2 + 1)}{lBase}"); + + if(ch1 < lMax) return Path.Combine(dir, name + $".{(char)(ch1 + 1)}{lBase}{lBase}"); + + return null; // ZZZ reached + } + + /// + /// Parses EWF header text from a raw (already decompressed) byte array. + /// header sections use ASCII; header2 sections use UTF-16 LE. + /// + static Dictionary ParseHeaderText(byte[] data, bool isHeader2) + { + string text; + + if(isHeader2) + { + // UTF-16 LE, may have BOM + if(data.Length >= 2 && data[0] == 0xFF && data[1] == 0xFE) + text = Encoding.Unicode.GetString(data, 2, data.Length - 2); + else + text = Encoding.Unicode.GetString(data); + } + else + text = Encoding.ASCII.GetString(data); + + var result = new Dictionary(); + + // Header text format is category-based with newline-delimited sections. + // Most common format: + // Line 1: category number (e.g., "1" or "3") + // Line 2: "main" or empty + // Line 3: tab-separated field identifiers (e.g., "c\tn\ta\te\tt\tav\tov\tm\tu\tp\tr") + // Line 4: tab-separated field values + // The remaining lines may repeat the pattern or be empty. + + string[] lines = text.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + + if(lines.Length < 4) return result; + + // Try to find the field identifiers and values lines + // Look for the line containing tab-separated single-letter or short identifiers + for(var i = 0; i < lines.Length - 1; i++) + { + string line = lines[i].Trim(); + + if(!line.Contains('\t')) continue; + + string[] keys = line.Split('\t'); + + // Verify this looks like an identifier line (short keys) + var isKeyLine = true; + + foreach(string key in keys) + { + if(key.Length > 4) + { + isKeyLine = false; + + break; + } + } + + if(!isKeyLine) continue; + + // Next line should contain the values + if(i + 1 >= lines.Length) break; + + string[] values = lines[i + 1].Trim().Split('\t'); + + for(var j = 0; j < keys.Length && j < values.Length; j++) + { + string key = keys[j].Trim(); + string value = values[j].Trim(); + + if(!string.IsNullOrEmpty(key) && !result.ContainsKey(key)) result[key] = value; + } + + break; // Only process the first key/value pair set + } + + return result; + } + + /// + /// Decompresses zlib (RFC 1950) compressed data by stripping the 2-byte header + /// and using DeflateStream. + /// + static byte[] DecompressZlib(byte[] compressedData, int uncompressedSize) + { + if(compressedData == null || compressedData.Length < 3 || uncompressedSize <= 0) + return new byte[Math.Max(uncompressedSize, 0)]; + + var decompressed = new byte[uncompressedSize]; + + // Skip 2-byte zlib header (CMF + FLG) + using var ms = new MemoryStream(compressedData, 2, compressedData.Length - 2); + using var deflate = new DeflateStream(ms, CompressionMode.Decompress); + var totalRead = 0; + + while(totalRead < uncompressedSize) + { + int read = deflate.Read(decompressed, totalRead, uncompressedSize - totalRead); + + if(read == 0) break; + + totalRead += read; + } + + return decompressed; + } + + /// + /// Builds session and track lists from EWF session entries using the libewf reconstruction algorithm. + /// + static void BuildSessionsAndTracks(List<(ulong startSector, uint flags)> sessionEntries, ulong totalSectors, + uint bytesPerSector, out List sessions, out List tracks) + { + sessions = []; + tracks = []; + + if(sessionEntries is not { Count: > 0 }) + { + // No session entries — create a single session with one data track + sessions.Add(new Session + { + Sequence = 1, + StartSector = 0, + EndSector = totalSectors - 1, + StartTrack = 1, + EndTrack = 1 + }); + + tracks.Add(new Track + { + Sequence = 1, + Session = 1, + StartSector = 0, + EndSector = totalSectors - 1, + Type = TrackType.Data, + BytesPerSector = (int)bytesPerSector, + RawBytesPerSector = (int)bytesPerSector, + SubchannelType = TrackSubchannelType.None + }); + + return; + } + + // Build tracks from session entries + // Each entry defines the start of a region; its type is determined by the audio flag. + ushort sessionSequence = 1; + uint trackSequence = 1; + ulong sessionStart = 0; + uint sessionStartTrack = 1; + + for(var i = 0; i < sessionEntries.Count; i++) + { + (ulong startSector, uint flags) = sessionEntries[i]; + bool isAudio = (flags & SESSION_ENTRY_FLAG_IS_AUDIO) != 0; + ulong endSector = i + 1 < sessionEntries.Count ? sessionEntries[i + 1].startSector - 1 : totalSectors - 1; + + // If this is a data entry and there was a previous audio entry, we're starting a new session + if(!isAudio && i > 0 && (sessionEntries[i - 1].flags & SESSION_ENTRY_FLAG_IS_AUDIO) != 0) + { + // Close previous session + sessions.Add(new Session + { + Sequence = sessionSequence, + StartSector = sessionStart, + EndSector = startSector - 1, + StartTrack = sessionStartTrack, + EndTrack = trackSequence - 1 + }); + + sessionSequence++; + sessionStart = startSector; + sessionStartTrack = trackSequence; + } + else if(!isAudio && i > 0 && (sessionEntries[i - 1].flags & SESSION_ENTRY_FLAG_IS_AUDIO) == 0) + { + // Data followed by data — new session boundary + sessions.Add(new Session + { + Sequence = sessionSequence, + StartSector = sessionStart, + EndSector = startSector - 1, + StartTrack = sessionStartTrack, + EndTrack = trackSequence - 1 + }); + + sessionSequence++; + sessionStart = startSector; + sessionStartTrack = trackSequence; + } + + tracks.Add(new Track + { + Sequence = trackSequence, + Session = sessionSequence, + StartSector = startSector, + EndSector = endSector, + Type = isAudio ? TrackType.Audio : TrackType.CdMode1, + BytesPerSector = isAudio ? 2352 : (int)bytesPerSector, + RawBytesPerSector = isAudio ? 2352 : (int)bytesPerSector, + SubchannelType = TrackSubchannelType.None + }); + + trackSequence++; + } + + // Close last session + sessions.Add(new Session + { + Sequence = sessionSequence, + StartSector = sessionStart, + EndSector = totalSectors - 1, + StartTrack = sessionStartTrack, + EndTrack = trackSequence - 1 + }); + } +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Identify.cs b/Aaru.Images/EWF/Identify.cs new file mode 100644 index 000000000..173af7c22 --- /dev/null +++ b/Aaru.Images/EWF/Identify.cs @@ -0,0 +1,70 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Identify.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Identifies Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.IO; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; +using Aaru.Logging; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region IOpticalMediaImage Members + + /// + public bool Identify(IFilter imageFilter) + { + Stream stream = imageFilter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + if(stream.Length < SIGNATURE_LENGTH) return false; + + var signatureBytes = new byte[SIGNATURE_LENGTH]; + stream.EnsureRead(signatureBytes, 0, SIGNATURE_LENGTH); + + AaruLogging.Debug(MODULE_NAME, Localization.Ewf_Identify_Signature_0, BitConverter.ToString(signatureBytes)); + + if(signatureBytes.AsSpan().SequenceEqual(EVF_SIGNATURE)) return true; + + if(signatureBytes.AsSpan().SequenceEqual(LVF_SIGNATURE)) return true; + + if(signatureBytes.AsSpan().SequenceEqual(EVF2_SIGNATURE)) return true; + + if(signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE)) return true; + + return false; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Open.cs b/Aaru.Images/EWF/Open.cs new file mode 100644 index 000000000..6d9d1961b --- /dev/null +++ b/Aaru.Images/EWF/Open.cs @@ -0,0 +1,808 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Open.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Opens Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; +using Aaru.Logging; +using Marshal = Aaru.Helpers.Marshal; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region IOpticalMediaImage Members + + /// + public ErrorNumber Open(IFilter imageFilter) + { + Stream stream = imageFilter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + if(stream.Length < SIGNATURE_LENGTH) return ErrorNumber.InvalidArgument; + + // Read signature to determine version + var signatureBytes = new byte[SIGNATURE_LENGTH]; + stream.EnsureRead(signatureBytes, 0, SIGNATURE_LENGTH); + + _isV2 = signatureBytes.AsSpan().SequenceEqual(EVF2_SIGNATURE) || + signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE); + + AaruLogging.Debug(MODULE_NAME, "EWF version {0}", _isV2 ? "2" : "1"); + + // Discover all segment files + _segmentStreams = []; + List segmentFilters = DiscoverSegments(imageFilter); + + if(segmentFilters.Count == 0) return ErrorNumber.InvalidArgument; + + foreach(IFilter filter in segmentFilters) _segmentStreams.Add(filter.GetDataForkStream()); + + // Initialize data structures + _chunkTable = new Dictionary(); + _chunkCache = new Dictionary(); + _sectorCache = new Dictionary(); + _badSectors = []; + _headerValues = new Dictionary(); + _md5Stored = null; + _sha1Stored = null; + _isSmart = false; + + _sectorsPerChunk = DEFAULT_SECTORS_PER_CHUNK; + _bytesPerSector = DEFAULT_BYTES_PER_SECTOR; + _chunkSize = DEFAULT_CHUNK_SIZE; + + ulong totalSectors = 0; + uint chsC = 0; + uint chsH = 0; + uint chsS = 0; + byte mediaType = 0; + byte mediaFlags = 0; + var volumeFound = false; + + var sessionEntries = new List<(ulong startSector, uint flags)>(); + ulong currentChunk = 0; + + byte[] headerData = null; + byte[] header2Data = null; + + // Process each segment file + for(var segIdx = 0; segIdx < _segmentStreams.Count; segIdx++) + { + Stream segStream = _segmentStreams[segIdx]; + + if(_isV2) + { + ParseSegmentV2(segStream, + segIdx, + ref currentChunk, + ref volumeFound, + ref totalSectors, + ref mediaType, + ref mediaFlags, + ref chsC, + ref chsH, + ref chsS, + sessionEntries); + } + else + { + ParseSegmentV1(segStream, + segIdx, + ref currentChunk, + ref volumeFound, + ref totalSectors, + ref headerData, + ref header2Data, + ref mediaType, + ref mediaFlags, + ref chsC, + ref chsH, + ref chsS, + sessionEntries); + } + } + + if(!volumeFound) + { + AaruLogging.Error(MODULE_NAME, "No volume section found in any segment file"); + + return ErrorNumber.InvalidArgument; + } + + _chunkSize = _sectorsPerChunk * _bytesPerSector; + _maxChunkCache = (int)(MAX_CACHE_SIZE / _chunkSize); + + if(_maxChunkCache < 1) _maxChunkCache = 1; + + // Parse header metadata (prefer header2 over header) + if(header2Data is { Length: > 0 }) + _headerValues = ParseHeaderText(header2Data, true); + else if(headerData is { Length: > 0 }) _headerValues = ParseHeaderText(headerData, false); + + // Populate image info + _imageInfo.Sectors = totalSectors; + _imageInfo.SectorSize = _bytesPerSector; + _imageInfo.Cylinders = chsC; + _imageInfo.Heads = chsH; + _imageInfo.SectorsPerTrack = chsS; + _imageInfo.ImageSize = totalSectors * _bytesPerSector; + _imageInfo.Application = "EnCase"; + _imageInfo.CreationTime = imageFilter.CreationTime; + _imageInfo.LastModificationTime = imageFilter.LastWriteTime; + + if(_isSmart) _imageInfo.Application = "SMART"; + + // Map header values to image info + if(_headerValues.TryGetValue("a", out string description)) _imageInfo.Comments = description; + + if(_headerValues.TryGetValue("av", out string appVersion)) _imageInfo.ApplicationVersion = appVersion; + + if(_headerValues.TryGetValue("ov", out string osVersion)) _imageInfo.Creator = osVersion; + + if(_headerValues.TryGetValue("md", out string model)) _imageInfo.MediaModel = model; + + if(_headerValues.TryGetValue("sn", out string serial)) _imageInfo.MediaSerialNumber = serial; + + // Determine media type + if(_isSmart) + { + _imageInfo.MediaType = MediaType.GENERIC_HDD; + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + } + else + { + var ewfMediaType = (EwfMediaType)mediaType; + + switch(ewfMediaType) + { + case EwfMediaType.Fixed: + _imageInfo.MediaType = MediaType.GENERIC_HDD; + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + + break; + case EwfMediaType.Removable: + _imageInfo.MediaType = MediaType.FlashDrive; + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + + break; + case EwfMediaType.Optical: + _imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc; + + // Refine optical type based on sector size and count + if(_bytesPerSector == 2048) + { + _imageInfo.MediaType = totalSectors <= 360000 + ? MediaType.CDROM + : totalSectors <= 2295104 + ? MediaType.DVDROM + : MediaType.BDROM; + } + else + _imageInfo.MediaType = MediaType.CD; + + break; + case EwfMediaType.Memory: + _imageInfo.MediaType = MediaType.GENERIC_HDD; + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + + break; + default: + _imageInfo.MediaType = MediaType.Unknown; + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + + break; + } + } + + // Build sessions and tracks + if(_imageInfo.MetadataMediaType == MetadataMediaType.OpticalDisc) + { + _imageInfo.HasSessions = true; + _imageInfo.HasPartitions = true; + + BuildSessionsAndTracks(sessionEntries, totalSectors, _bytesPerSector, out _sessions, out _tracks); + } + else + { + _sessions = null; + _tracks = null; + } + + // Build metadata from header values + _metadata = null; + + if(_headerValues.Count > 0) BuildAaruMetadata(); + + _imageInfo.Version = _isV2 ? "2" : "1"; + + AaruLogging.Debug(MODULE_NAME, "Image has {0} sectors of {1} bytes", totalSectors, _bytesPerSector); + AaruLogging.Debug(MODULE_NAME, "Chunk table has {0} entries", _chunkTable.Count); + + return ErrorNumber.NoError; + } + +#endregion + + /// Discovers all segment files related to the primary image file. + List DiscoverSegments(IFilter primaryFilter) + { + List segments = [primaryFilter]; + string currentPath = primaryFilter.BasePath; + + while(true) + { + string nextPath = GetNextSegmentFilename(currentPath); + + if(nextPath == null) break; + + IFilter nextFilter = PluginRegister.Singleton.GetFilter(nextPath); + + if(nextFilter == null) break; + + segments.Add(nextFilter); + currentPath = nextPath; + } + + AaruLogging.Debug(MODULE_NAME, "Found {0} segment files", segments.Count); + + return segments; + } + + /// Parses an EWF v1 segment file, extracting all sections. + void ParseSegmentV1(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound, + ref ulong totalSectors, ref byte[] headerData, ref byte[] header2Data, ref byte mediaType, + ref byte mediaFlags, ref uint chsC, ref uint chsH, ref uint chsS, + List<(ulong startSector, uint flags)> sessionEntries) + { + // Skip file header (13 bytes) + segStream.Seek(FILE_HEADER_V1_SIZE, SeekOrigin.Begin); + + while(segStream.Position < segStream.Length) + { + long sectionStart = segStream.Position; + + // Read section descriptor + var descBytes = new byte[SECTION_DESCRIPTOR_V1_SIZE]; + int bytesRead = segStream.Read(descBytes, 0, SECTION_DESCRIPTOR_V1_SIZE); + + if(bytesRead < SECTION_DESCRIPTOR_V1_SIZE) break; + + EwfSectionDescriptorV1 descriptor = + Marshal.ByteArrayToStructureLittleEndian(descBytes); + + // Get section type string (null-terminated) + string sectionType = Encoding.ASCII.GetString(descriptor.type_string).TrimEnd('\0'); + + AaruLogging.Debug(MODULE_NAME, + "Section type: {0} at offset {1}, next at {2}, size {3}", + sectionType, + sectionStart, + descriptor.next_offset, + descriptor.size); + + // Calculate data size (section size minus section descriptor size) + long dataSize = (long)descriptor.size - SECTION_DESCRIPTOR_V1_SIZE; + + switch(sectionType) + { + case SECTION_TYPE_HEADER: + if(headerData == null && dataSize > 0) + { + var compressedHeader = new byte[dataSize]; + segStream.EnsureRead(compressedHeader, 0, (int)dataSize); + + try + { + headerData = DecompressZlib(compressedHeader, (int)dataSize * 10); + } + catch(Exception ex) + { + AaruLogging.Debug(MODULE_NAME, "Failed to decompress header: {0}", ex.Message); + } + } + + break; + + case SECTION_TYPE_HEADER2: + if(header2Data == null && dataSize > 0) + { + var compressedHeader2 = new byte[dataSize]; + segStream.EnsureRead(compressedHeader2, 0, (int)dataSize); + + try + { + header2Data = DecompressZlib(compressedHeader2, (int)dataSize * 10); + } + catch(Exception ex) + { + AaruLogging.Debug(MODULE_NAME, "Failed to decompress header2: {0}", ex.Message); + } + } + + break; + + case SECTION_TYPE_VOLUME: + case SECTION_TYPE_DATA: + if(!volumeFound) + { + ParseVolumeSection(segStream, + dataSize, + ref volumeFound, + ref totalSectors, + ref mediaType, + ref mediaFlags, + ref chsC, + ref chsH, + ref chsS); + } + + break; + + case SECTION_TYPE_TABLE: + case SECTION_TYPE_TABLE2: + ParseTableSectionV1(segStream, segIdx, dataSize, ref currentChunk, (long)descriptor.next_offset); + + break; + + case SECTION_TYPE_HASH: + if(_md5Stored == null && + dataSize >= System.Runtime.InteropServices.Marshal.SizeOf()) + { + var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(hashBytes, 0, hashBytes.Length); + + EwfHashSection hash = Marshal.ByteArrayToStructureLittleEndian(hashBytes); + + _md5Stored = hash.md5_hash; + + AaruLogging.Debug(MODULE_NAME, + "MD5 from hash section: {0}", + BitConverter.ToString(_md5Stored).Replace("-", "").ToLowerInvariant()); + } + + break; + + case SECTION_TYPE_DIGEST: + if(dataSize >= System.Runtime.InteropServices.Marshal.SizeOf()) + { + var digestBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(digestBytes, 0, digestBytes.Length); + + EwfDigestSection digest = + Marshal.ByteArrayToStructureLittleEndian(digestBytes); + + _md5Stored = digest.md5_hash; + _sha1Stored = digest.sha1_hash; + + AaruLogging.Debug(MODULE_NAME, + "MD5 from digest section: {0}", + BitConverter.ToString(_md5Stored).Replace("-", "").ToLowerInvariant()); + + AaruLogging.Debug(MODULE_NAME, + "SHA1 from digest section: {0}", + BitConverter.ToString(_sha1Stored).Replace("-", "").ToLowerInvariant()); + } + + break; + + case SECTION_TYPE_ERROR2: + ParseErrorSectionV1(segStream, dataSize); + + break; + + case SECTION_TYPE_SESSION: + ParseSessionSectionV1(segStream, dataSize, sessionEntries); + + break; + } + + // Move to next section + if(descriptor.next_offset == 0 || sectionType is SECTION_TYPE_DONE or SECTION_TYPE_NEXT) break; + + segStream.Seek((long)descriptor.next_offset, SeekOrigin.Begin); + } + } + + /// Parses the volume (or data) section to extract media parameters. + void ParseVolumeSection(Stream segStream, long dataSize, ref bool volumeFound, ref ulong totalSectors, + ref byte mediaType, ref byte mediaFlags, ref uint chsC, ref uint chsH, ref uint chsS) + { + if(dataSize < VOLUME_SECTION_SIZE_SMART) return; + + // Read all the volume data available + var volumeData = new byte[dataSize]; + segStream.EnsureRead(volumeData, 0, (int)dataSize); + + // Determine format by checking if it's a small (94-byte) or large (1052-byte) volume section + if(dataSize < VOLUME_SECTION_SIZE_ENCASE) + { + // Small volume section: SMART or old EnCase format (94 bytes of data) + EwfVolumeSmartSection smartVol = + Marshal.ByteArrayToStructureLittleEndian(volumeData); + + _sectorsPerChunk = smartVol.sectors_per_chunk; + _bytesPerSector = smartVol.bytes_per_sector; + totalSectors = smartVol.number_of_sectors; + + // Check SMART signature + if(smartVol.signature != null && smartVol.signature.AsSpan().SequenceEqual(SMART_SIGNATURE)) + _isSmart = true; + + AaruLogging.Debug(MODULE_NAME, + "{0} volume: {1} sectors, {2} bytes/sector, {3} sectors/chunk", + _isSmart ? "SMART" : "Old EnCase", + totalSectors, + _bytesPerSector, + _sectorsPerChunk); + } + else + { + // Large volume section: EnCase 5+ format (1052 bytes of data) + EwfVolumeSection vol = Marshal.ByteArrayToStructureLittleEndian(volumeData); + + _sectorsPerChunk = vol.sectors_per_chunk; + _bytesPerSector = vol.bytes_per_sector; + totalSectors = vol.number_of_sectors; + chsC = vol.chs_cylinders; + chsH = vol.chs_heads; + chsS = vol.chs_sectors; + mediaType = vol.media_type; + mediaFlags = vol.media_flags; + + AaruLogging.Debug(MODULE_NAME, + "EnCase volume: {0} sectors, {1} bytes/sector, {2} sectors/chunk, C/H/S={3}/{4}/{5}", + totalSectors, + _bytesPerSector, + _sectorsPerChunk, + chsC, + chsH, + chsS); + + AaruLogging.Debug(MODULE_NAME, + "Media type: {0}, media flags: {1}, compression: {2}", + (EwfMediaType)vol.media_type, + (EwfMediaFlags)vol.media_flags, + (EwfCompressionLevel)vol.compression_level); + } + + volumeFound = true; + } + + /// Parses an EWF v1 table section, building the chunk→location mapping. + void ParseTableSectionV1(Stream segStream, int segIdx, long dataSize, ref ulong currentChunk, + long nextSectionOffset) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + // Read table header + var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(tableHeaderBytes, 0, tableHeaderBytes.Length); + + EwfTableHeaderV1 tableHeader = Marshal.ByteArrayToStructureLittleEndian(tableHeaderBytes); + + AaruLogging.Debug(MODULE_NAME, + "Table: {0} entries, base offset {1}", + tableHeader.number_of_entries, + tableHeader.base_offset); + + // Read all table entries + var entryCount = (int)tableHeader.number_of_entries; + var entryData = new byte[entryCount * 4]; + segStream.EnsureRead(entryData, 0, entryData.Length); + + // Skip entries checksum (4 bytes) + segStream.Seek(4, SeekOrigin.Current); + + for(var i = 0; i < entryCount; i++) + { + var rawEntry = BitConverter.ToUInt32(entryData, i * 4); + bool compressed = (rawEntry & TABLE_ENTRY_V1_COMPRESSED_FLAG) != 0; + var offset = (long)(tableHeader.base_offset + (rawEntry & TABLE_ENTRY_V1_OFFSET_MASK)); + + // Calculate chunk size: distance to next chunk or next section for the last entry + uint size; + + if(i + 1 < entryCount) + { + var nextRawEntry = BitConverter.ToUInt32(entryData, (i + 1) * 4); + var nextOffset = (long)(tableHeader.base_offset + (nextRawEntry & TABLE_ENTRY_V1_OFFSET_MASK)); + size = (uint)(nextOffset - offset); + } + else + { + // For the last entry, use the next section offset to compute size + if(nextSectionOffset > offset) + size = (uint)(nextSectionOffset - offset); + else + size = _chunkSize + 4; // fallback: uncompressed chunk + Adler-32 + } + + ulong chunkIndex = currentChunk + (ulong)i; + + if(!_chunkTable.ContainsKey(chunkIndex)) _chunkTable[chunkIndex] = (segIdx, offset, size, compressed); + } + + currentChunk += (ulong)entryCount; + } + + /// Parses an EWF v1 error2 section. + void ParseErrorSectionV1(Stream segStream, long dataSize) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(headerBytes, 0, headerBytes.Length); + + EwfErrorHeaderV1 errorHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + + int entrySize = System.Runtime.InteropServices.Marshal.SizeOf(); + + for(var i = 0; i < (int)errorHeader.number_of_entries; i++) + { + var entryBytes = new byte[entrySize]; + segStream.EnsureRead(entryBytes, 0, entrySize); + + EwfErrorEntryV1 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + + _badSectors.Add((entry.start_sector, entry.number_of_sectors)); + + AaruLogging.Debug(MODULE_NAME, + "Bad sector range: start={0}, count={1}", + entry.start_sector, + entry.number_of_sectors); + } + } + + /// Parses an EWF v1 session section. + void ParseSessionSectionV1(Stream segStream, long dataSize, List<(ulong startSector, uint flags)> sessionEntries) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(headerBytes, 0, headerBytes.Length); + + EwfSessionHeaderV1 sessionHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + + AaruLogging.Debug(MODULE_NAME, "Session section: {0} entries", sessionHeader.number_of_entries); + + int entrySize = System.Runtime.InteropServices.Marshal.SizeOf(); + + for(var i = 0; i < (int)sessionHeader.number_of_entries; i++) + { + var entryBytes = new byte[entrySize]; + segStream.EnsureRead(entryBytes, 0, entrySize); + + EwfSessionEntryV1 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + + sessionEntries.Add((entry.start_sector, entry.flags)); + + AaruLogging.Debug(MODULE_NAME, "Session entry: start={0}, flags=0x{1:X8}", entry.start_sector, entry.flags); + } + } + + /// Parses an EWF v2 segment file. + void ParseSegmentV2(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound, + ref ulong totalSectors, ref byte mediaType, ref byte mediaFlags, ref uint chsC, ref uint chsH, + ref uint chsS, List<(ulong startSector, uint flags)> sessionEntries) + { + // Read v2 file header + segStream.Seek(0, SeekOrigin.Begin); + + var headerBytes = new byte[FILE_HEADER_V2_SIZE]; + segStream.EnsureRead(headerBytes, 0, FILE_HEADER_V2_SIZE); + + EwfFileHeaderV2 fileHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + + _compressionMethod = (EwfCompressionMethod)fileHeader.compression_method; + + AaruLogging.Debug(MODULE_NAME, + "EWF v2 segment {0}, compression method: {1}", + fileHeader.segment_number, + _compressionMethod); + + // EWF v2 sections are read backward from the end of the file + int descriptorSize = System.Runtime.InteropServices.Marshal.SizeOf(); + long position = segStream.Length - descriptorSize; + + while(position >= FILE_HEADER_V2_SIZE) + { + segStream.Seek(position, SeekOrigin.Begin); + + var descBytes = new byte[descriptorSize]; + segStream.EnsureRead(descBytes, 0, descriptorSize); + + EwfSectionDescriptorV2 descriptor = + Marshal.ByteArrayToStructureLittleEndian(descBytes); + + var sectionType = (EwfSectionTypeV2)descriptor.type; + long dataStart = position - (long)descriptor.data_size; + + AaruLogging.Debug(MODULE_NAME, + "V2 section type: {0} at {1}, data size {2}, previous at {3}", + sectionType, + position, + descriptor.data_size, + descriptor.previous_offset); + + switch(sectionType) + { + case EwfSectionTypeV2.SectorTable: + segStream.Seek(dataStart, SeekOrigin.Begin); + ParseTableSectionV2(segStream, segIdx, (long)descriptor.data_size, ref currentChunk); + + break; + + case EwfSectionTypeV2.Md5Hash: + if(_md5Stored == null && + (long)descriptor.data_size >= System.Runtime.InteropServices.Marshal.SizeOf()) + { + segStream.Seek(dataStart, SeekOrigin.Begin); + + var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(hashBytes, 0, hashBytes.Length); + + EwfMd5HashV2 hash = Marshal.ByteArrayToStructureLittleEndian(hashBytes); + + _md5Stored = hash.md5_hash; + } + + break; + + case EwfSectionTypeV2.Sha1Hash: + if(_sha1Stored == null && + (long)descriptor.data_size >= System.Runtime.InteropServices.Marshal.SizeOf()) + { + segStream.Seek(dataStart, SeekOrigin.Begin); + + var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(hashBytes, 0, hashBytes.Length); + + EwfSha1HashV2 hash = Marshal.ByteArrayToStructureLittleEndian(hashBytes); + + _sha1Stored = hash.sha1_hash; + } + + break; + + case EwfSectionTypeV2.ErrorTable: + segStream.Seek(dataStart, SeekOrigin.Begin); + ParseErrorSectionV2(segStream, (long)descriptor.data_size); + + break; + + case EwfSectionTypeV2.SessionTable: + segStream.Seek(dataStart, SeekOrigin.Begin); + ParseSessionSectionV2(segStream, (long)descriptor.data_size, sessionEntries); + + break; + } + + // Move to previous section + if(descriptor.previous_offset == 0) break; + + position = (long)descriptor.previous_offset; + } + } + + /// Parses an EWF v2 table section. + void ParseTableSectionV2(Stream segStream, int segIdx, long dataSize, ref ulong currentChunk) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(tableHeaderBytes, 0, tableHeaderBytes.Length); + + EwfTableHeaderV2 tableHeader = Marshal.ByteArrayToStructureLittleEndian(tableHeaderBytes); + ulong firstChunk = tableHeader.first_chunk_number; + var entryCount = (int)tableHeader.number_of_entries; + int entrySize = System.Runtime.InteropServices.Marshal.SizeOf(); + + AaruLogging.Debug(MODULE_NAME, "V2 Table: {0} entries, first chunk {1}", entryCount, firstChunk); + + for(var i = 0; i < entryCount; i++) + { + var entryBytes = new byte[entrySize]; + segStream.EnsureRead(entryBytes, 0, entrySize); + + EwfTableEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + + // Flag 0x00000001 means uncompressed in v2 + bool compressed = (entry.chunk_data_flags & 0x00000001) == 0; + + ulong chunkIndex = firstChunk + (ulong)i; + + if(!_chunkTable.ContainsKey(chunkIndex)) + _chunkTable[chunkIndex] = (segIdx, (long)entry.chunk_data_offset, entry.chunk_data_size, compressed); + } + + if(firstChunk + (ulong)entryCount > currentChunk) currentChunk = firstChunk + (ulong)entryCount; + } + + /// Parses an EWF v2 error section. + void ParseErrorSectionV2(Stream segStream, long dataSize) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(headerBytes, 0, headerBytes.Length); + + EwfErrorHeaderV2 errorHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + int entrySize = System.Runtime.InteropServices.Marshal.SizeOf(); + + for(var i = 0; i < (int)errorHeader.number_of_entries; i++) + { + var entryBytes = new byte[entrySize]; + segStream.EnsureRead(entryBytes, 0, entrySize); + + EwfErrorEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + + _badSectors.Add((entry.start_sector, entry.number_of_sectors)); + } + } + + /// Parses an EWF v2 session section. + void ParseSessionSectionV2(Stream segStream, long dataSize, List<(ulong startSector, uint flags)> sessionEntries) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.EnsureRead(headerBytes, 0, headerBytes.Length); + + EwfSessionHeaderV2 sessionHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + int entrySize = System.Runtime.InteropServices.Marshal.SizeOf(); + + for(var i = 0; i < (int)sessionHeader.number_of_entries; i++) + { + var entryBytes = new byte[entrySize]; + segStream.EnsureRead(entryBytes, 0, entrySize); + + EwfSessionEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + + sessionEntries.Add((entry.start_sector, entry.flags)); + } + } + + /// Builds AaruMetadata from parsed header values. + void BuildAaruMetadata() + { + // EWF header metadata doesn't map cleanly to AaruMetadata, but we can store some info + // in the image's Comments and Creator fields (already done above). + // For full metadata support, the AaruMetadata object would need forensic-specific fields. + // For now, we leave _metadata as null and expose data through ImageInfo fields. + } +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Optical.cs b/Aaru.Images/EWF/Optical.cs new file mode 100644 index 000000000..92bb1411f --- /dev/null +++ b/Aaru.Images/EWF/Optical.cs @@ -0,0 +1,188 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Optical.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains IOpticalMediaImage implementation for Expert Witness Format. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region IOpticalMediaImage Members + + /// + public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return ErrorNumber.NotSupported; + + Track trk = _tracks?.FirstOrDefault(t => t.Sequence == track); + + if(trk == null) return ErrorNumber.SectorNotFound; + + return ReadSector(trk.StartSector + sectorAddress, false, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer) + { + buffer = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return ErrorNumber.NotSupported; + + Track trk = _tracks?.FirstOrDefault(t => t.Sequence == track); + + if(trk == null) return ErrorNumber.SectorNotFound; + + return ReadSectors(trk.StartSector + sectorAddress, false, length, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer, + out SectorStatus sectorStatus) => + ReadSector(sectorAddress, track, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) => + ReadSectors(sectorAddress, length, track, out buffer, out sectorStatus); + + /// + public List GetSessionTracks(Session session) + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null) return null; + + return _tracks.Where(t => t.Session == session.Sequence).ToList(); + } + + /// + public List GetSessionTracks(ushort session) + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null) return null; + + return _tracks.Where(t => t.Session == session).ToList(); + } + + /// + public bool? VerifySector(ulong sectorAddress) => null; + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, out List failingLbas, + out List unknownLbas) + { + failingLbas = []; + unknownLbas = []; + + if(_badSectors is not { Count: > 0 }) + { + for(ulong i = sectorAddress; i < sectorAddress + length; i++) unknownLbas.Add(i); + + return null; + } + + for(ulong i = sectorAddress; i < sectorAddress + length; i++) + { + var isBad = false; + + foreach((ulong start, uint count) in _badSectors) + { + if(i < start || i >= start + count) continue; + + isBad = true; + + break; + } + + if(isBad) + failingLbas.Add(i); + else + unknownLbas.Add(i); + } + + if(unknownLbas.Count > 0) return null; + + return failingLbas.Count <= 0; + } + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List failingLbas, + out List unknownLbas) + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null) + { + failingLbas = []; + unknownLbas = []; + + for(ulong i = sectorAddress; i < sectorAddress + length; i++) unknownLbas.Add(i); + + return null; + } + + Track trk = _tracks.FirstOrDefault(t => t.Sequence == track); + + if(trk == null) + { + failingLbas = []; + unknownLbas = []; + + return null; + } + + return VerifySectors(trk.StartSector + sectorAddress, length, out failingLbas, out unknownLbas); + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Properties.cs b/Aaru.Images/EWF/Properties.cs new file mode 100644 index 000000000..285f0e833 --- /dev/null +++ b/Aaru.Images/EWF/Properties.cs @@ -0,0 +1,152 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Properties.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains properties for Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using Aaru.CommonTypes; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Partition = Aaru.CommonTypes.Partition; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region IOpticalMediaImage Members + + /// + + // ReSharper disable once ConvertToAutoProperty + public ImageInfo Info => _imageInfo; + + /// + public string Name => Localization.Ewf_Name; + + /// + public Guid Id => new("E5F3B186-2382-4BDE-89B2-A6FC2B464C6F"); + + /// + public string Author => Authors.NataliaPortillo; + + /// + public string Format => _isSmart + ? "Expert Witness Format (SMART)" + : _isV2 + ? "Expert Witness Format v2" + : "Expert Witness Format"; + + /// + public List DumpHardware => null; + + /// + public Metadata AaruMetadata => _metadata; + + /// + public IEnumerable SupportedMediaTags => []; + + /// + public IEnumerable SupportedSectorTags => []; + + /// + public IEnumerable SupportedMediaTypes => + [ + MediaType.Unknown, MediaType.GENERIC_HDD, MediaType.FlashDrive, MediaType.CompactFlash, + MediaType.CompactFlashType2, MediaType.PCCardTypeI, MediaType.PCCardTypeII, MediaType.PCCardTypeIII, + MediaType.PCCardTypeIV, MediaType.CD, MediaType.CDDA, MediaType.CDG, MediaType.CDEG, MediaType.CDI, + MediaType.CDROM, MediaType.CDROMXA, MediaType.CDPLUS, MediaType.CDR, MediaType.CDRW, MediaType.CDMRW, + MediaType.DVDROM, MediaType.DVDR, MediaType.DVDRW, MediaType.DVDPR, MediaType.DVDPRW, MediaType.DVDRDL, + MediaType.DVDPRWDL, MediaType.DVDPRDL, MediaType.DVDRAM, MediaType.DVDRWDL, MediaType.BDROM, MediaType.BDR, + MediaType.BDRE, MediaType.BDRXL, MediaType.BDREXL + ]; + + /// + public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => []; + + /// + public IEnumerable KnownExtensions => [".E01", ".Ex01", ".s01"]; + + /// + public bool IsWriting => false; + + /// + public string ErrorMessage => null; + + /// + public List Tracks + { + get + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null; + + return _tracks; + } + } + + /// + public List Sessions + { + get + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null; + + return _sessions; + } + } + + /// + public List Partitions + { + get + { + if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null; + + List parts = + [ + new() + { + Start = 0, + Length = _imageInfo.Sectors, + Offset = 0, + Sequence = 0, + Type = "MODE1/2048", + Size = _imageInfo.Sectors * _imageInfo.SectorSize + } + ]; + + return parts; + } + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Read.cs b/Aaru.Images/EWF/Read.cs new file mode 100644 index 000000000..ec79ef793 --- /dev/null +++ b/Aaru.Images/EWF/Read.cs @@ -0,0 +1,181 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Read.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Reads Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.IO; +using Aaru.CommonTypes.Enums; +using Aaru.Compression; +using Aaru.Helpers; +using Aaru.Logging; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ + /// Reads and decompresses a chunk, using cache when possible. + ErrorNumber ReadChunk(ulong chunkIndex, out byte[] chunkData) + { + chunkData = null; + + // Check cache + if(_chunkCache.TryGetValue(chunkIndex, out chunkData)) return ErrorNumber.NoError; + + if(!_chunkTable.TryGetValue(chunkIndex, out (int segmentIndex, long offset, uint size, bool compressed) chunk)) + { + AaruLogging.Error(MODULE_NAME, "Chunk {0} not found in chunk table", chunkIndex); + + return ErrorNumber.SectorNotFound; + } + + Stream segStream = _segmentStreams[chunk.segmentIndex]; + segStream.Seek(chunk.offset, SeekOrigin.Begin); + + if(chunk.compressed) + { + var compressedData = new byte[chunk.size]; + segStream.EnsureRead(compressedData, 0, (int)chunk.size); + + if(_isV2 && _compressionMethod == EwfCompressionMethod.Bzip2) + { + // BZip2 decompression + chunkData = new byte[_chunkSize]; + BZip2.DecodeBuffer(compressedData, chunkData); + } + else + { + // Zlib/Deflate decompression (default for v1, and deflate method for v2) + chunkData = DecompressZlib(compressedData, (int)_chunkSize); + } + } + else + { + // Uncompressed chunk: data + 4-byte Adler-32 checksum + uint dataLen = chunk.size >= 4 ? chunk.size - 4 : chunk.size; + chunkData = new byte[dataLen]; + segStream.EnsureRead(chunkData, 0, (int)dataLen); + + // Skip checksum (we trust the data for now) + if(chunk.size >= 4) segStream.Seek(4, SeekOrigin.Current); + } + + // Cache chunk + if(_chunkCache.Count >= _maxChunkCache) _chunkCache.Clear(); + + _chunkCache[chunkIndex] = chunkData; + + return ErrorNumber.NoError; + } + + /// Determines the sector status by checking against the bad sectors list. + SectorStatus GetSectorStatus(ulong sectorAddress) + { + foreach((ulong start, uint count) in _badSectors) + { + if(sectorAddress >= start && sectorAddress < start + count) return SectorStatus.Errored; + } + + return SectorStatus.Dumped; + } + +#region IOpticalMediaImage Members + + /// + public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + + if(sectorAddress >= _imageInfo.Sectors) return ErrorNumber.OutOfRange; + + // Check sector cache + if(_sectorCache.TryGetValue(sectorAddress, out buffer)) + { + sectorStatus = GetSectorStatus(sectorAddress); + + return ErrorNumber.NoError; + } + + // Calculate which chunk contains this sector + ulong chunkIndex = sectorAddress / _sectorsPerChunk; + + // Read and decompress the chunk + ErrorNumber errno = ReadChunk(chunkIndex, out byte[] chunkData); + + if(errno != ErrorNumber.NoError) return errno; + + // Extract the requested sector from the chunk + ulong sectorInChunk = sectorAddress % _sectorsPerChunk; + var sectorOffset = (long)(sectorInChunk * _bytesPerSector); + + // Handle last chunk which may be smaller + if(sectorOffset + _bytesPerSector > chunkData.Length) return ErrorNumber.OutOfRange; + + buffer = new byte[_bytesPerSector]; + Array.Copy(chunkData, sectorOffset, buffer, 0, _bytesPerSector); + + sectorStatus = GetSectorStatus(sectorAddress); + + // Cache sector + if(_sectorCache.Count >= MAX_CACHED_SECTORS) _sectorCache.Clear(); + + _sectorCache[sectorAddress] = buffer; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange; + + buffer = new byte[length * _bytesPerSector]; + sectorStatus = new SectorStatus[length]; + + for(uint i = 0; i < length; i++) + { + ErrorNumber errno = ReadSector(sectorAddress + i, negative, out byte[] sector, out SectorStatus status); + + if(errno != ErrorNumber.NoError) return errno; + + Array.Copy(sector, 0, buffer, i * _bytesPerSector, _bytesPerSector); + sectorStatus[i] = status; + } + + return ErrorNumber.NoError; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Structs.cs b/Aaru.Images/EWF/Structs.cs new file mode 100644 index 000000000..4151da955 --- /dev/null +++ b/Aaru.Images/EWF/Structs.cs @@ -0,0 +1,418 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Structs.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains structures for Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Runtime.InteropServices; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region Nested type: EwfFileHeaderV1 + + /// EWF v1 file header, 13 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfFileHeaderV1 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] signature; + public byte fields_start; + public ushort segment_number; + public ushort fields_end; + } + +#endregion + +#region Nested type: EwfFileHeaderV2 + + /// EWF v2 file header, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfFileHeaderV2 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] signature; + public byte major_version; + public byte minor_version; + public ushort compression_method; + public uint segment_number; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] set_identifier; + } + +#endregion + +#region Nested type: EwfSectionDescriptorV1 + + /// EWF v1 section descriptor, 76 bytes. Sections are forward-linked. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSectionDescriptorV1 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] type_string; + public ulong next_offset; + public ulong size; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)] + public byte[] padding; + public uint checksum; + } + +#endregion + +#region Nested type: EwfSectionDescriptorV2 + + /// EWF v2 section descriptor, 64 bytes. Sections are backward-linked. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSectionDescriptorV2 + { + public uint type; + public uint data_flags; + public ulong previous_offset; + public ulong data_size; + public uint descriptor_size; + public uint padding_size; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] data_integrity_hash; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] reserved; + public uint checksum; + } + +#endregion + +#region Nested type: EwfVolumeSection + + /// EWF v1 EnCase volume section data, 1024 bytes (followed by checksum) + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfVolumeSection + { + public byte media_type; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] unknown1; + public uint number_of_chunks; + public uint sectors_per_chunk; + public uint bytes_per_sector; + public ulong number_of_sectors; + public uint chs_cylinders; + public uint chs_heads; + public uint chs_sectors; + public byte media_flags; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] unknown2; + public uint palm_volume_start_sector; + public uint unknown3; + public uint smart_logs_start_sector; + public byte compression_level; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] unknown4; + public uint error_granularity; + public uint unknown5; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] set_identifier; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 963)] + public byte[] unknown6; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] + public byte[] signature; + public uint checksum; + } + +#endregion + +#region Nested type: EwfVolumeSmartSection + + /// EWF SMART/old EnCase volume section data, 94 bytes total + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfVolumeSmartSection + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] unknown1; + public uint number_of_chunks; + public uint sectors_per_chunk; + public uint bytes_per_sector; + public uint number_of_sectors; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] unknown2; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 45)] + public byte[] unknown3; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] + public byte[] signature; + public uint checksum; + } + +#endregion + +#region Nested type: EwfTableHeaderV1 + + /// EWF v1 table section header, 24 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfTableHeaderV1 + { + public uint number_of_entries; + public uint padding1; + public ulong base_offset; + public uint padding2; + public uint checksum; + } + +#endregion + +#region Nested type: EwfTableHeaderV2 + + /// EWF v2 table section header, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfTableHeaderV2 + { + public ulong first_chunk_number; + public uint number_of_entries; + public uint unknown1; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] padding; + } + +#endregion + +#region Nested type: EwfTableEntryV2 + + /// EWF v2 table entry, 16 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfTableEntryV2 + { + public ulong chunk_data_offset; + public uint chunk_data_size; + public uint chunk_data_flags; + } + +#endregion + +#region Nested type: EwfHashSection + + /// EWF v1 hash section, 36 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfHashSection + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] md5_hash; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] unknown1; + public uint checksum; + } + +#endregion + +#region Nested type: EwfDigestSection + + /// EWF v1 digest section, 80 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfDigestSection + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] md5_hash; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] sha1_hash; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)] + public byte[] padding1; + public uint checksum; + } + +#endregion + +#region Nested type: EwfSessionHeaderV1 + + /// EWF v1 session section header, 36 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSessionHeaderV1 + { + public uint number_of_entries; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] + public byte[] unknown1; + public uint checksum; + } + +#endregion + +#region Nested type: EwfSessionEntryV1 + + /// EWF v1 session entry, 32 bytes. Note: flags first, then start_sector. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSessionEntryV1 + { + public uint flags; + public uint start_sector; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] + public byte[] unknown1; + } + +#endregion + +#region Nested type: EwfSessionHeaderV2 + + /// EWF v2 session section header, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSessionHeaderV2 + { + public uint number_of_entries; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] unknown1; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] padding; + } + +#endregion + +#region Nested type: EwfSessionEntryV2 + + /// EWF v2 session entry, 32 bytes. Note: start_sector first, then flags (reversed from v1). + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSessionEntryV2 + { + public ulong start_sector; + public uint flags; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] unknown1; + } + +#endregion + +#region Nested type: EwfErrorHeaderV1 + + /// EWF v1 error2 section header, 520 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfErrorHeaderV1 + { + public uint number_of_entries; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)] + public byte[] unknown1; + public uint checksum; + } + +#endregion + +#region Nested type: EwfErrorEntryV1 + + /// EWF v1 error entry, 8 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfErrorEntryV1 + { + public uint start_sector; + public uint number_of_sectors; + } + +#endregion + +#region Nested type: EwfErrorHeaderV2 + + /// EWF v2 error section header, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfErrorHeaderV2 + { + public uint number_of_entries; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] unknown1; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] padding; + } + +#endregion + +#region Nested type: EwfErrorEntryV2 + + /// EWF v2 error entry, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfErrorEntryV2 + { + public ulong start_sector; + public uint number_of_sectors; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] unknown1; + } + +#endregion + +#region Nested type: EwfMd5HashV2 + + /// EWF v2 MD5 hash section, 32 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfMd5HashV2 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] md5_hash; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)] + public byte[] padding; + } + +#endregion + +#region Nested type: EwfSha1HashV2 + + /// EWF v2 SHA1 hash section, 48 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfSha1HashV2 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] sha1_hash; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] + public byte[] padding; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/EWF/Unsupported.cs b/Aaru.Images/EWF/Unsupported.cs new file mode 100644 index 000000000..c2ecdc25a --- /dev/null +++ b/Aaru.Images/EWF/Unsupported.cs @@ -0,0 +1,77 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Unsupported.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains features unsupported by Expert Witness Format disk images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using Aaru.CommonTypes.Enums; + +namespace Aaru.Images; + +public sealed partial class Ewf +{ +#region IOpticalMediaImage Members + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer) + { + buffer = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer) + { + buffer = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer, + out SectorStatus sectorStatus) => + ReadSector(sectorAddress, negative, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) => + ReadSectors(sectorAddress, negative, length, out buffer, out sectorStatus); + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs index 68d7859fa..cec54f675 100644 --- a/Aaru.Images/Localization/Localization.Designer.cs +++ b/Aaru.Images/Localization/Localization.Designer.cs @@ -5967,5 +5967,17 @@ namespace Aaru.Images { return ResourceManager.GetString("Redumper_disc_image", resourceCulture); } } + + internal static string Ewf_Name { + get { + return ResourceManager.GetString("Ewf_Name", resourceCulture); + } + } + + internal static string Ewf_Identify_Signature_0 { + get { + return ResourceManager.GetString("Ewf_Identify_Signature_0", resourceCulture); + } + } } } diff --git a/Aaru.Images/Localization/Localization.es.resx b/Aaru.Images/Localization/Localization.es.resx index 0504c5d39..5b4c15805 100644 --- a/Aaru.Images/Localization/Localization.es.resx +++ b/Aaru.Images/Localization/Localization.es.resx @@ -2976,4 +2976,10 @@ Imagen de disco DVD crudo de Redumper + + Formato Expert Witness (EWF) + + + Firma: {0} + \ No newline at end of file diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx index 9a6c849c9..0f20d7b56 100644 --- a/Aaru.Images/Localization/Localization.resx +++ b/Aaru.Images/Localization/Localization.resx @@ -2995,4 +2995,10 @@ Redumper raw DVD disc image + + Expert Witness Format (EWF) + + + Signature: {0} + \ No newline at end of file diff --git a/README.md b/README.md index 53e29e113..ed99dce9c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Media image formats we can read * DiscJuggler images * Dreamcast GDI * Easy CD Creator images (.CIF) +* Expert Witness Format (EWF) * HD-Copy disk images * HxCStream * MAME Compressed Hunks of Data (CHD)