From e3bd3ab6df1ca9bf74d69a164986bd08f6ca4761 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Mon, 13 Apr 2026 15:34:51 +0100 Subject: [PATCH] Implement EWF archive support. --- Aaru.Archives/EWF/Consts.cs | 84 ++++ Aaru.Archives/EWF/Enums.cs | 89 +++++ Aaru.Archives/EWF/EwfArchive.cs | 115 ++++++ Aaru.Archives/EWF/EwfFileStream.cs | 122 ++++++ Aaru.Archives/EWF/Files.cs | 192 +++++++++ Aaru.Archives/EWF/Helpers.cs | 376 ++++++++++++++++++ Aaru.Archives/EWF/Info.cs | 78 ++++ Aaru.Archives/EWF/Open.cs | 371 +++++++++++++++++ Aaru.Archives/EWF/Structs.cs | 223 +++++++++++ Aaru.Archives/EWF/Xattrs.cs | 93 +++++ .../Localization/Localization.Designer.cs | 6 + .../Localization/Localization.es.resx | 3 + Aaru.Archives/Localization/Localization.resx | 3 + Aaru.Images/EWF/Identify.cs | 5 +- README.md | 1 + 15 files changed, 1758 insertions(+), 3 deletions(-) create mode 100644 Aaru.Archives/EWF/Consts.cs create mode 100644 Aaru.Archives/EWF/Enums.cs create mode 100644 Aaru.Archives/EWF/EwfArchive.cs create mode 100644 Aaru.Archives/EWF/EwfFileStream.cs create mode 100644 Aaru.Archives/EWF/Files.cs create mode 100644 Aaru.Archives/EWF/Helpers.cs create mode 100644 Aaru.Archives/EWF/Info.cs create mode 100644 Aaru.Archives/EWF/Open.cs create mode 100644 Aaru.Archives/EWF/Structs.cs create mode 100644 Aaru.Archives/EWF/Xattrs.cs diff --git a/Aaru.Archives/EWF/Consts.cs b/Aaru.Archives/EWF/Consts.cs new file mode 100644 index 000000000..855a82957 --- /dev/null +++ b/Aaru.Archives/EWF/Consts.cs @@ -0,0 +1,84 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Consts.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains constants for Expert Witness Format logical evidence files. +// +// --[ 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.Archives; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public sealed partial class EwfArchive +{ + /// 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 logical evidence signature: "LEF2\x0D\x0A\x81\x00" + static readonly byte[] LEF2_SIGNATURE = [0x4C, 0x45, 0x46, 0x32, 0x0D, 0x0A, 0x81, 0x00]; + + /// EWF v1 disk image signature (needed for segment traversal) + static readonly byte[] EVF_SIGNATURE = [0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]; + + /// EWF v2 disk image signature (needed for segment traversal) + static readonly byte[] EVF2_SIGNATURE = [0x45, 0x56, 0x46, 0x32, 0x0D, 0x0A, 0x81, 0x00]; + + const int SIGNATURE_LENGTH = 8; + + 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_LTREE = "ltree"; + const string SECTION_TYPE_LTYPES = "ltypes"; + const string SECTION_TYPE_DONE = "done"; + const string SECTION_TYPE_NEXT = "next"; + + const int SECTION_DESCRIPTOR_V1_SIZE = 76; + const int FILE_HEADER_V1_SIZE = 13; + const int FILE_HEADER_V2_SIZE = 32; + + const uint DEFAULT_SECTORS_PER_CHUNK = 64; + const uint DEFAULT_BYTES_PER_SECTOR = 512; + const uint DEFAULT_CHUNK_SIZE = DEFAULT_SECTORS_PER_CHUNK * DEFAULT_BYTES_PER_SECTOR; + + const uint TABLE_ENTRY_V1_COMPRESSED_FLAG = 0x80000000; + const uint TABLE_ENTRY_V1_OFFSET_MASK = 0x7FFFFFFF; + + const int VOLUME_SECTION_SIZE_ENCASE = 1052; + const int VOLUME_SECTION_SIZE_SMART = 94; + + const int MAX_CACHE_SIZE = 16777216; + + /// Ltree header size in bytes + const int LTREE_HEADER_SIZE = 48; +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Enums.cs b/Aaru.Archives/EWF/Enums.cs new file mode 100644 index 000000000..12d671431 --- /dev/null +++ b/Aaru.Archives/EWF/Enums.cs @@ -0,0 +1,89 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Enums.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains enumerations for Expert Witness Format logical evidence files. +// +// --[ 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.Diagnostics.CodeAnalysis; + +namespace Aaru.Archives; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public sealed partial class EwfArchive +{ + /// EWF v2 compression method + 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 + } + + /// LEF file entry flags from opr field + [Flags] + enum EwfLefEntryFlags : uint + { + ReadOnly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Archive = 0x00000008, + Symlink = 0x00000010, + Deleted = 0x00000080, + HardLink = 0x00001000, + AltStream = 0x00002000, + Folder = 0x02000000, + Sparse = 0x04000000 + } +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/EwfArchive.cs b/Aaru.Archives/EWF/EwfArchive.cs new file mode 100644 index 000000000..74e3d3825 --- /dev/null +++ b/Aaru.Archives/EWF/EwfArchive.cs @@ -0,0 +1,115 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : EwfArchive.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages Expert Witness Format logical evidence files. +// +// --[ 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 Aaru.CommonTypes.Interfaces; + +namespace Aaru.Archives; + +/// +/// Implements reading Expert Witness Format logical evidence (L01/Lx01) files +public sealed partial class EwfArchive : IArchive +{ + const string MODULE_NAME = "EWF Logical Evidence plugin"; + uint _bytesPerSector; + + /// Cache of decompressed chunks + Dictionary _chunkCache; + + internal uint _chunkSize; + + /// Map from chunk index to location in segment file + Dictionary _chunkTable; + + /// Compression method for EWF v2 + EwfCompressionMethod _compressionMethod; + + /// Parsed file entries from ltree + List _entries; + + /// Whether the image is EWF v2 format + bool _isV2; + + int _maxChunkCache; + uint _sectorsPerChunk; + + /// Ordered list of open segment file streams + List _segmentStreams; + +#region IArchive Members + + /// + public string Name => Localization.EwfArchive_Name; + + /// + public Guid Id => new("A7B9F4C1-8E63-4D2B-9A1C-5F7E3D6B8C02"); + + /// + public string Author => Authors.NataliaPortillo; + + /// + public bool Opened { get; private set; } + + /// + public ArchiveSupportedFeature ArchiveFeatures => ArchiveSupportedFeature.SupportsFilenames | + ArchiveSupportedFeature.SupportsCompression | + ArchiveSupportedFeature.SupportsSubdirectories | + ArchiveSupportedFeature.HasExplicitDirectories | + ArchiveSupportedFeature.HasEntryTimestamp | + ArchiveSupportedFeature.SupportsXAttrs; + + /// + public int NumberOfEntries => Opened ? _entries.Count : -1; + + /// + public void Close() + { + if(!Opened) return; + + _chunkCache?.Clear(); + _chunkTable?.Clear(); + _entries?.Clear(); + + if(_segmentStreams != null) + { + foreach(Stream stream in _segmentStreams) stream.Close(); + + _segmentStreams.Clear(); + } + + Opened = false; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/EwfFileStream.cs b/Aaru.Archives/EWF/EwfFileStream.cs new file mode 100644 index 000000000..fd3934392 --- /dev/null +++ b/Aaru.Archives/EWF/EwfFileStream.cs @@ -0,0 +1,122 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : EwfFileStream.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Stream wrapper for reading file data from EWF logical evidence chunks. +// +// --[ 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; + +namespace Aaru.Archives; + +/// Read-only stream that reads file data from EWF logical evidence chunks. +sealed class EwfFileStream : Stream +{ + readonly EwfArchive _archive; + readonly long _dataOffset; + long _position; + + public EwfFileStream(EwfArchive archive, long dataOffset, long dataSize) + { + _archive = archive; + _dataOffset = dataOffset; + Length = dataSize; + _position = 0; + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length { get; } + + public override long Position + { + get => _position; + set => _position = Math.Max(0, Math.Min(value, Length)); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if(_position >= Length) return 0; + + var toRead = (int)Math.Min(count, Length - _position); + var totalRead = 0; + + while(totalRead < toRead) + { + // Calculate which byte in the overall media data this maps to + long mediaOffset = _dataOffset + _position; + + // Calculate chunk index and offset within chunk + var chunkIndex = (ulong)(mediaOffset / _archive._chunkSize); + var offsetInChunk = (int)(mediaOffset % _archive._chunkSize); + + byte[] chunkData = _archive.ReadChunk(chunkIndex); + + if(chunkData == null) break; + + int available = chunkData.Length - offsetInChunk; + int toCopy = Math.Min(toRead - totalRead, available); + + Array.Copy(chunkData, offsetInChunk, buffer, offset + totalRead, toCopy); + + totalRead += toCopy; + _position += toCopy; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch(origin) + { + case SeekOrigin.Begin: + Position = offset; + + break; + case SeekOrigin.Current: + Position += offset; + + break; + case SeekOrigin.End: + Position = Length + offset; + + break; + } + + return Position; + } + + public override void Flush() {} + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Files.cs b/Aaru.Archives/EWF/Files.cs new file mode 100644 index 000000000..9671a27a0 --- /dev/null +++ b/Aaru.Archives/EWF/Files.cs @@ -0,0 +1,192 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Files.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains file access methods for Expert Witness Format logical evidence. +// +// --[ 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.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Filters; +using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ +#region IArchive Members + + /// + public ErrorNumber GetFilename(int entryNumber, out string fileName) + { + fileName = null; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + fileName = _entries[entryNumber].FullPath; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetCompressedSize(int entryNumber, out long length) + { + length = -1; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + length = _entries[entryNumber].DataSize; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetUncompressedSize(int entryNumber, out long length) + { + length = -1; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + length = _entries[entryNumber].Size; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetEntryNumber(string fileName, bool caseInsensitiveMatch, out int entryNumber) + { + entryNumber = -1; + + if(!Opened) return ErrorNumber.NotOpened; + + StringComparison comparison = caseInsensitiveMatch + ? StringComparison.CurrentCultureIgnoreCase + : StringComparison.CurrentCulture; + + for(var i = 0; i < _entries.Count; i++) + { + if(!_entries[i].FullPath.Equals(fileName, comparison)) continue; + + entryNumber = i; + + return ErrorNumber.NoError; + } + + return ErrorNumber.NoSuchFile; + } + + /// + public ErrorNumber Stat(int entryNumber, out FileEntryInfo stat) + { + stat = null; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + EwfFileEntry entry = _entries[entryNumber]; + + stat = new FileEntryInfo + { + Length = entry.Size, + Attributes = entry.IsDirectory ? FileAttributes.Directory : FileAttributes.File, + Blocks = entry.Size / 512, + BlockSize = 512 + }; + + if(entry.Size % 512 != 0) stat.Blocks++; + + if(entry.CreationTime != DateTime.MinValue) + { + stat.CreationTime = entry.CreationTime; + stat.CreationTimeUtc = entry.CreationTime; + } + + if(entry.ModifyTime != DateTime.MinValue) + { + stat.LastWriteTime = entry.ModifyTime; + stat.LastWriteTimeUtc = entry.ModifyTime; + } + + if(entry.AccessTime != DateTime.MinValue) + { + stat.AccessTime = entry.AccessTime; + stat.AccessTimeUtc = entry.AccessTime; + } + + // Map EWF flags to file attributes + if((entry.Flags & (uint)EwfLefEntryFlags.ReadOnly) != 0) stat.Attributes |= FileAttributes.ReadOnly; + + if((entry.Flags & (uint)EwfLefEntryFlags.Hidden) != 0) stat.Attributes |= FileAttributes.Hidden; + + if((entry.Flags & (uint)EwfLefEntryFlags.System) != 0) stat.Attributes |= FileAttributes.System; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetEntry(int entryNumber, out IFilter filter) + { + filter = null; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + EwfFileEntry entry = _entries[entryNumber]; + + if(entry.IsDirectory) return ErrorNumber.IsDirectory; + + Stream stream; + + if(entry.Size == 0) + stream = new MemoryStream([]); + else + stream = new EwfFileStream(this, entry.DataOffset, entry.Size); + + filter = new ZZZNoFilter(); + ErrorNumber errno = filter.Open(stream); + + if(errno == ErrorNumber.NoError) return ErrorNumber.NoError; + + stream.Close(); + + return errno; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Helpers.cs b/Aaru.Archives/EWF/Helpers.cs new file mode 100644 index 000000000..aeffb3259 --- /dev/null +++ b/Aaru.Archives/EWF/Helpers.cs @@ -0,0 +1,376 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Helpers.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains helper methods for Expert Witness Format logical evidence files. +// +// --[ 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.Globalization; +using System.IO; +using System.IO.Compression; +using System.Text; +using Aaru.Compression; +using Aaru.Logging; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ + /// Generates the next segment filename following EWF naming conventions. + 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; + + bool isV2 = ext.Length == 5; + bool isLower = char.IsLower(ext[1]); + + if(isV2) + { + string numPart = ext.Substring(3); + char prefix1 = ext[1]; + char prefix2 = ext[2]; + + 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 _)) + { + char a = isLower ? 'a' : 'A'; + + return Path.Combine(dir, name + $".{prefix1}{prefix2}{a}{a}"); + } + + 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; + } + + char firstChar = ext[1]; + string suffix = ext.Substring(2); + + if(int.TryParse(suffix, out int segNum) && segNum < 99) + return Path.Combine(dir, name + $".{firstChar}{segNum + 1:D2}"); + + if(int.TryParse(suffix, out _)) + { + char a = isLower ? 'a' : 'A'; + + return Path.Combine(dir, name + $".{firstChar}{a}{a}"); + } + + char ch1 = ext[1]; + char ch2 = ext[2]; + char ch3 = ext[3]; + 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; + } + + /// Decompresses zlib (RFC 1950) compressed data. + 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]; + + 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; + } + + /// Parses the ltree text data into a flat list of file entries. + List ParseLtreeData(byte[] decompressedData) + { + string text; + + // ltree data is UTF-16 LE, may or may not have BOM + if(decompressedData.Length >= 2 && decompressedData[0] == 0xFF && decompressedData[1] == 0xFE) + text = Encoding.Unicode.GetString(decompressedData, 2, decompressedData.Length - 2); + else + text = Encoding.Unicode.GetString(decompressedData); + + var entries = new List(); + + string[] lines = text.Split('\n'); + var lineIndex = 0; + + // Find the "entry" category + while(lineIndex < lines.Length) + { + string line = lines[lineIndex].Trim('\r', ' '); + + if(line == "entry") + { + lineIndex++; + + break; + } + + lineIndex++; + } + + if(lineIndex >= lines.Length) + { + AaruLogging.Debug(MODULE_NAME, "No entry category found in ltree data"); + + return entries; + } + + // Skip empty lines after category name + while(lineIndex < lines.Length && string.IsNullOrWhiteSpace(lines[lineIndex].Trim('\r', ' '))) lineIndex++; + + if(lineIndex >= lines.Length) return entries; + + // Read type indicators line (tab-separated field names) + string typeIndicatorLine = lines[lineIndex].Trim('\r', ' '); + string[] fieldNames = typeIndicatorLine.Split('\t'); + lineIndex++; + + // Skip empty lines + while(lineIndex < lines.Length && string.IsNullOrWhiteSpace(lines[lineIndex].Trim('\r', ' '))) lineIndex++; + + // Parse root entry and its children recursively + ParseLtreeEntries(lines, ref lineIndex, fieldNames, "", entries); + + return entries; + } + + /// Recursively parses ltree entries. + void ParseLtreeEntries(string[] lines, ref int lineIndex, string[] fieldNames, string parentPath, + List entries) + { + if(lineIndex >= lines.Length) return; + + // First line of an entry: " " + string headerLine = lines[lineIndex].Trim('\r', ' '); + lineIndex++; + + string[] headerParts = headerLine.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries); + + if(headerParts.Length < 2) return; + + // Parse the values line + if(lineIndex >= lines.Length) return; + + string valuesLine = lines[lineIndex].Trim('\r', ' '); + lineIndex++; + + string[] values = valuesLine.Split('\t'); + + // Build field→value map + var fieldMap = new Dictionary(); + + for(var i = 0; i < fieldNames.Length && i < values.Length; i++) + { + string key = fieldNames[i].Trim(); + + if(!string.IsNullOrEmpty(key)) fieldMap[key] = values[i].Trim(); + } + + // Extract entry properties + fieldMap.TryGetValue("n", out string entryName); + fieldMap.TryGetValue("id", out string entryId); + fieldMap.TryGetValue("ls", out string logicalSize); + fieldMap.TryGetValue("cr", out string creationTime); + fieldMap.TryGetValue("wr", out string writeTime); + fieldMap.TryGetValue("ac", out string accessTime); + fieldMap.TryGetValue("ha", out string md5Hash); + fieldMap.TryGetValue("sha", out string sha1Hash); + fieldMap.TryGetValue("opr", out string flagsStr); + fieldMap.TryGetValue("p", out string isParent); + fieldMap.TryGetValue("du", out string duplicateOffset); + fieldMap.TryGetValue("be", out string binaryExtents); + + bool isDir = isParent == "1" || + !string.IsNullOrEmpty(flagsStr) && + uint.TryParse(flagsStr, out uint fv) && + (fv & (uint)EwfLefEntryFlags.Folder) != 0; + + // Build full path + string fullPath; + + if(string.IsNullOrEmpty(entryName) || entryName == "NoName") + fullPath = parentPath; + else + fullPath = string.IsNullOrEmpty(parentPath) ? entryName : parentPath + "/" + entryName; + + // Parse numeric fields + long.TryParse(entryId, out long id); + long.TryParse(logicalSize, out long size); + uint.TryParse(flagsStr, out uint flags); + + // Parse data location from binary extents + long dataOffset = 0; + long dataSize = 0; + + if(!string.IsNullOrEmpty(binaryExtents)) ParseBinaryExtents(binaryExtents, out dataOffset, out dataSize); + + // Parse duplicate offset if no binary extents + if(dataSize == 0 && !string.IsNullOrEmpty(duplicateOffset) && duplicateOffset != "-1") + long.TryParse(duplicateOffset, out dataOffset); + + // Parse timestamps + DateTime creation = ParsePosixTimestamp(creationTime); + DateTime modify = ParsePosixTimestamp(writeTime); + DateTime access = ParsePosixTimestamp(accessTime); + + // Skip root entry (empty path), but add all others + if(!string.IsNullOrEmpty(fullPath)) + { + entries.Add(new EwfFileEntry + { + Id = id, + Name = entryName ?? "", + FullPath = fullPath, + IsDirectory = isDir, + Size = size, + DataOffset = dataOffset, + DataSize = dataSize > 0 ? dataSize : size, + CreationTime = creation, + ModifyTime = modify, + AccessTime = access, + Md5Hash = md5Hash, + Sha1Hash = sha1Hash, + Flags = flags + }); + } + + // Parse children + if(!int.TryParse(headerParts[1], out int numChildren)) return; + + for(var i = 0; i < numChildren; i++) + { + if(lineIndex >= lines.Length) break; + + ParseLtreeEntries(lines, ref lineIndex, fieldNames, fullPath, entries); + } + } + + /// Parses binary extents field: "count offset_hex size_hex" + static void ParseBinaryExtents(string extents, out long offset, out long size) + { + offset = 0; + size = 0; + + string[] parts = extents.Split([' '], StringSplitOptions.RemoveEmptyEntries); + + if(parts.Length < 3) return; + + // First part is count, then pairs of offset+size in hex + long.TryParse(parts[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out offset); + long.TryParse(parts[2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out size); + } + + /// Parses a POSIX timestamp string to DateTime. + static DateTime ParsePosixTimestamp(string timestamp) + { + if(string.IsNullOrEmpty(timestamp) || !long.TryParse(timestamp, out long epoch) || epoch <= 0) + return DateTime.MinValue; + + try + { + return DateTimeOffset.FromUnixTimeSeconds(epoch).UtcDateTime; + } + catch + { + return DateTime.MinValue; + } + } + + /// Reads and decompresses a chunk. + internal byte[] ReadChunk(ulong chunkIndex) + { + if(_chunkCache.TryGetValue(chunkIndex, out byte[] cached)) return cached; + + if(!_chunkTable.TryGetValue(chunkIndex, out (int segmentIndex, long offset, uint size, bool compressed) chunk)) + return null; + + Stream segStream = _segmentStreams[chunk.segmentIndex]; + segStream.Seek(chunk.offset, SeekOrigin.Begin); + + byte[] chunkData; + + if(chunk.compressed) + { + var compressedData = new byte[chunk.size]; + segStream.ReadExactly(compressedData, 0, (int)chunk.size); + + if(_isV2 && _compressionMethod == EwfCompressionMethod.Bzip2) + { + chunkData = new byte[_chunkSize]; + BZip2.DecodeBuffer(compressedData, chunkData); + } + else + chunkData = DecompressZlib(compressedData, (int)_chunkSize); + } + else + { + uint dataLen = chunk.size >= 4 ? chunk.size - 4 : chunk.size; + chunkData = new byte[dataLen]; + segStream.ReadExactly(chunkData, 0, (int)dataLen); + } + + if(_chunkCache.Count >= _maxChunkCache) _chunkCache.Clear(); + + _chunkCache[chunkIndex] = chunkData; + + return chunkData; + } +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Info.cs b/Aaru.Archives/EWF/Info.cs new file mode 100644 index 000000000..f44a420c4 --- /dev/null +++ b/Aaru.Archives/EWF/Info.cs @@ -0,0 +1,78 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Info.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Identifies Expert Witness Format logical evidence files. +// +// --[ 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 System.Text; +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ +#region IArchive Members + + /// + public bool Identify(IFilter filter) + { + if(filter.DataForkLength < SIGNATURE_LENGTH) return false; + + Stream stream = filter.GetDataForkStream(); + stream.Position = 0; + + var signatureBytes = new byte[SIGNATURE_LENGTH]; + stream.ReadExactly(signatureBytes, 0, SIGNATURE_LENGTH); + + if(signatureBytes.AsSpan().SequenceEqual(LVF_SIGNATURE)) return true; + + if(signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE)) return true; + + return false; + } + + /// + public void GetInformation(IFilter filter, Encoding encoding, out string information) + { + information = ""; + + if(!Opened) return; + + var sb = new StringBuilder(); + sb.AppendLine("[bold][blue]EWF Logical Evidence File:[/][/]"); + sb.AppendFormat("[slateblue1]Format:[/] [green]{0}[/]", _isV2 ? "EWF2 (Lx01)" : "EWF (L01)").AppendLine(); + sb.AppendFormat("[slateblue1]Files:[/] [teal]{0}[/]", _entries?.Count ?? 0).AppendLine(); + + information = sb.ToString(); + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Open.cs b/Aaru.Archives/EWF/Open.cs new file mode 100644 index 000000000..1a49a4338 --- /dev/null +++ b/Aaru.Archives/EWF/Open.cs @@ -0,0 +1,371 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Open.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Opens Expert Witness Format logical evidence files. +// +// --[ 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.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Logging; +using Marshal = Aaru.Helpers.Marshal; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ +#region IArchive Members + + /// + public ErrorNumber Open(IFilter filter, Encoding encoding) + { + if(filter.DataForkLength < SIGNATURE_LENGTH) return ErrorNumber.InvalidArgument; + + Stream stream = filter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + var signatureBytes = new byte[SIGNATURE_LENGTH]; + stream.ReadExactly(signatureBytes, 0, SIGNATURE_LENGTH); + + _isV2 = signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE); + + // Discover segment files + _segmentStreams = []; + List segmentFilters = DiscoverSegments(filter); + + foreach(IFilter f in segmentFilters) _segmentStreams.Add(f.GetDataForkStream()); + + // Initialize data structures + _chunkTable = new Dictionary(); + _chunkCache = new Dictionary(); + _entries = []; + + _sectorsPerChunk = DEFAULT_SECTORS_PER_CHUNK; + _bytesPerSector = DEFAULT_BYTES_PER_SECTOR; + _chunkSize = DEFAULT_CHUNK_SIZE; + + ulong currentChunk = 0; + var volumeFound = false; + + byte[] ltreeDecompressed = 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 ltreeDecompressed); + else + ParseSegmentV1(segStream, segIdx, ref currentChunk, ref volumeFound, ref ltreeDecompressed); + } + + _chunkSize = _sectorsPerChunk * _bytesPerSector; + _maxChunkCache = (int)(MAX_CACHE_SIZE / _chunkSize); + + if(_maxChunkCache < 1) _maxChunkCache = 1; + + // Parse ltree data into file entries + if(ltreeDecompressed is { Length: > 0 }) _entries = ParseLtreeData(ltreeDecompressed); + + AaruLogging.Debug(MODULE_NAME, "Parsed {0} file entries from ltree", _entries.Count); + AaruLogging.Debug(MODULE_NAME, "Chunk table has {0} entries", _chunkTable.Count); + + Opened = true; + + return ErrorNumber.NoError; + } + +#endregion + + 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; + } + + void ParseSegmentV1(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound, + ref byte[] ltreeDecompressed) + { + segStream.Seek(FILE_HEADER_V1_SIZE, SeekOrigin.Begin); + + while(segStream.Position < segStream.Length) + { + long sectionStart = segStream.Position; + + 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); + + string sectionType = Encoding.ASCII.GetString(descriptor.type_string).TrimEnd('\0'); + long dataSize = (long)descriptor.size - SECTION_DESCRIPTOR_V1_SIZE; + + AaruLogging.Debug(MODULE_NAME, + "Section type: {0} at offset {1}, next at {2}, size {3}", + sectionType, + sectionStart, + descriptor.next_offset, + descriptor.size); + + switch(sectionType) + { + case SECTION_TYPE_VOLUME: + case SECTION_TYPE_DATA: + if(!volumeFound) ParseVolumeSection(segStream, dataSize, ref volumeFound); + + break; + + case SECTION_TYPE_TABLE: + case SECTION_TYPE_TABLE2: + ParseTableSectionV1(segStream, segIdx, dataSize, ref currentChunk, (long)descriptor.next_offset); + + break; + + case SECTION_TYPE_LTREE: + if(ltreeDecompressed == null && dataSize > LTREE_HEADER_SIZE) + ltreeDecompressed = ParseLtreeSection(segStream, dataSize); + + break; + } + + if(descriptor.next_offset == 0 || sectionType is SECTION_TYPE_DONE or SECTION_TYPE_NEXT) break; + + segStream.Seek((long)descriptor.next_offset, SeekOrigin.Begin); + } + } + + void ParseVolumeSection(Stream segStream, long dataSize, ref bool volumeFound) + { + if(dataSize < VOLUME_SECTION_SIZE_SMART) return; + + var volumeData = new byte[dataSize]; + segStream.ReadExactly(volumeData, 0, (int)dataSize); + + if(dataSize < VOLUME_SECTION_SIZE_ENCASE) + { + EwfVolumeSmartSection smartVol = + Marshal.ByteArrayToStructureLittleEndian(volumeData); + + _sectorsPerChunk = smartVol.sectors_per_chunk; + _bytesPerSector = smartVol.bytes_per_sector; + } + else + { + EwfVolumeSection vol = Marshal.ByteArrayToStructureLittleEndian(volumeData); + + _sectorsPerChunk = vol.sectors_per_chunk; + _bytesPerSector = vol.bytes_per_sector; + } + + volumeFound = true; + } + + void ParseTableSectionV1(Stream segStream, int segIdx, long dataSize, ref ulong currentChunk, + long nextSectionOffset) + { + if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf()) return; + + var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf()]; + segStream.ReadExactly(tableHeaderBytes, 0, tableHeaderBytes.Length); + + EwfTableHeaderV1 tableHeader = Marshal.ByteArrayToStructureLittleEndian(tableHeaderBytes); + + var entryCount = (int)tableHeader.number_of_entries; + var entryData = new byte[entryCount * 4]; + segStream.ReadExactly(entryData, 0, entryData.Length); + + // Skip entries checksum + 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)); + + 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 + { + if(nextSectionOffset > offset) + size = (uint)(nextSectionOffset - offset); + else + size = _chunkSize + 4; + } + + ulong chunkIndex = currentChunk + (ulong)i; + + if(!_chunkTable.ContainsKey(chunkIndex)) _chunkTable[chunkIndex] = (segIdx, offset, size, compressed); + } + + currentChunk += (ulong)entryCount; + } + + byte[] ParseLtreeSection(Stream segStream, long dataSize) + { + // Read ltree header (48 bytes) + var headerBytes = new byte[LTREE_HEADER_SIZE]; + segStream.ReadExactly(headerBytes, 0, LTREE_HEADER_SIZE); + + EwfLtreeHeader ltreeHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + + AaruLogging.Debug(MODULE_NAME, "Ltree data size: {0}", ltreeHeader.data_size); + + // Read the compressed ltree data + long compressedSize = dataSize - LTREE_HEADER_SIZE; + + if(compressedSize <= 0) return null; + + var compressedData = new byte[compressedSize]; + segStream.ReadExactly(compressedData, 0, (int)compressedSize); + + // Decompress + try + { + return DecompressZlib(compressedData, (int)ltreeHeader.data_size); + } + catch(Exception ex) + { + AaruLogging.Debug(MODULE_NAME, "Failed to decompress ltree data: {0}", ex.Message); + + return null; + } + } + + void ParseSegmentV2(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound, + ref byte[] ltreeDecompressed) + { + segStream.Seek(0, SeekOrigin.Begin); + + var headerBytes = new byte[FILE_HEADER_V2_SIZE]; + segStream.ReadExactly(headerBytes, 0, FILE_HEADER_V2_SIZE); + + EwfFileHeaderV2 fileHeader = Marshal.ByteArrayToStructureLittleEndian(headerBytes); + _compressionMethod = (EwfCompressionMethod)fileHeader.compression_method; + + 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.ReadExactly(descBytes, 0, descriptorSize); + + EwfSectionDescriptorV2 descriptor = + Marshal.ByteArrayToStructureLittleEndian(descBytes); + + var sectionType = (EwfSectionTypeV2)descriptor.type; + long dataStart = position - (long)descriptor.data_size; + + switch(sectionType) + { + case EwfSectionTypeV2.SectorTable: + segStream.Seek(dataStart, SeekOrigin.Begin); + ParseTableSectionV2(segStream, segIdx, (long)descriptor.data_size, ref currentChunk); + + break; + + case EwfSectionTypeV2.SingleFilesTree: + if(ltreeDecompressed == null && (long)descriptor.data_size > LTREE_HEADER_SIZE) + { + segStream.Seek(dataStart, SeekOrigin.Begin); + ltreeDecompressed = ParseLtreeSection(segStream, (long)descriptor.data_size); + } + + break; + } + + if(descriptor.previous_offset == 0) break; + + position = (long)descriptor.previous_offset; + } + } + + 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.ReadExactly(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(); + + for(var i = 0; i < entryCount; i++) + { + var entryBytes = new byte[entrySize]; + segStream.ReadExactly(entryBytes, 0, entrySize); + + EwfTableEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian(entryBytes); + 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; + } +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Structs.cs b/Aaru.Archives/EWF/Structs.cs new file mode 100644 index 000000000..2fa893316 --- /dev/null +++ b/Aaru.Archives/EWF/Structs.cs @@ -0,0 +1,223 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Structs.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains structures for Expert Witness Format logical evidence files. +// +// --[ 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.Runtime.InteropServices; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ +#region Internal types + + /// Parsed file entry from the ltree + sealed class EwfFileEntry + { + public long Id { get; set; } + public string Name { get; set; } + public string FullPath { get; set; } + public bool IsDirectory { get; set; } + public long Size { get; set; } + public long DataOffset { get; set; } + public long DataSize { get; set; } + public DateTime CreationTime { get; set; } + public DateTime ModifyTime { get; set; } + public DateTime AccessTime { get; set; } + public string Md5Hash { get; set; } + public string Sha1Hash { get; set; } + public uint Flags { get; set; } + } + +#endregion + +#region On-disk structures + + [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; + } + + [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; + } + + [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; + } + + [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; + } + + [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; + } + + [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; + } + + [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; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfTableEntryV2 + { + public ulong chunk_data_offset; + public uint chunk_data_size; + public uint chunk_data_flags; + } + + [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; + } + + /// Ltree section header, 48 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct EwfLtreeHeader + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] integrity_hash; + public ulong data_size; + public uint checksum; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] unknown1; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/EWF/Xattrs.cs b/Aaru.Archives/EWF/Xattrs.cs new file mode 100644 index 000000000..6e87cee00 --- /dev/null +++ b/Aaru.Archives/EWF/Xattrs.cs @@ -0,0 +1,93 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Xattrs.cs +// Author(s) : Natalia Portillo +// +// Component : EWF logical evidence plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains extended attribute methods for Expert Witness Format logical +// evidence. +// +// --[ 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.Text; +using Aaru.CommonTypes.Enums; + +namespace Aaru.Archives; + +public sealed partial class EwfArchive +{ +#region IArchive Members + + /// + public ErrorNumber ListXAttr(int entryNumber, out List xattrs) + { + xattrs = null; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + EwfFileEntry entry = _entries[entryNumber]; + + xattrs = []; + + if(!string.IsNullOrEmpty(entry.Md5Hash) && entry.Md5Hash != "0" && !entry.Md5Hash.StartsWith("00000000")) + xattrs.Add("md5"); + + if(!string.IsNullOrEmpty(entry.Sha1Hash) && entry.Sha1Hash != "0" && !entry.Sha1Hash.StartsWith("00000000")) + xattrs.Add("sha1"); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetXattr(int entryNumber, string xattr, ref byte[] buffer) + { + buffer = null; + + if(!Opened) return ErrorNumber.NotOpened; + + if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange; + + EwfFileEntry entry = _entries[entryNumber]; + + switch(xattr) + { + case "md5" when !string.IsNullOrEmpty(entry.Md5Hash): + buffer = Encoding.UTF8.GetBytes(entry.Md5Hash); + + return ErrorNumber.NoError; + case "sha1" when !string.IsNullOrEmpty(entry.Sha1Hash): + buffer = Encoding.UTF8.GetBytes(entry.Sha1Hash); + + return ErrorNumber.NoError; + default: + return ErrorNumber.NoSuchExtendedAttribute; + } + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/Localization/Localization.Designer.cs b/Aaru.Archives/Localization/Localization.Designer.cs index 04288ce2e..b58bcfb88 100644 --- a/Aaru.Archives/Localization/Localization.Designer.cs +++ b/Aaru.Archives/Localization/Localization.Designer.cs @@ -698,5 +698,11 @@ namespace Aaru.Archives { return ResourceManager.GetString("Archive_contains_0_files", resourceCulture); } } + + internal static string EwfArchive_Name { + get { + return ResourceManager.GetString("EwfArchive_Name", resourceCulture); + } + } } } diff --git a/Aaru.Archives/Localization/Localization.es.resx b/Aaru.Archives/Localization/Localization.es.resx index c723101dd..b0b40c9f2 100644 --- a/Aaru.Archives/Localization/Localization.es.resx +++ b/Aaru.Archives/Localization/Localization.es.resx @@ -342,4 +342,7 @@ [slateblue1]El archivo contiene [green]{0}[/] ficheros[/] + + Evidencia Lógica Expert Witness Format (EWF-L) + \ No newline at end of file diff --git a/Aaru.Archives/Localization/Localization.resx b/Aaru.Archives/Localization/Localization.resx index c8cbb1d25..0ca087037 100644 --- a/Aaru.Archives/Localization/Localization.resx +++ b/Aaru.Archives/Localization/Localization.resx @@ -350,4 +350,7 @@ [slateblue1]Archive contains [green]{0}[/] files[/] + + Expert Witness Format Logical Evidence (EWF-L) + \ No newline at end of file diff --git a/Aaru.Images/EWF/Identify.cs b/Aaru.Images/EWF/Identify.cs index 173af7c22..33336d003 100644 --- a/Aaru.Images/EWF/Identify.cs +++ b/Aaru.Images/EWF/Identify.cs @@ -57,11 +57,10 @@ public sealed partial class Ewf 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; + // LVF and LEF2 signatures are for logical evidence files (L01/Lx01), + // handled by the IArchive plugin in Aaru.Archives return false; } diff --git a/README.md b/README.md index ed99dce9c..7e1a798ff 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ Supported archives ================== * ARC (.ARC) +* Expert Witness Format (EWF) * HA (.HA) * PAK (.PAK) * Symbian Installation File (.SIS)