Implement EWF archive support.

This commit is contained in:
2026-04-13 15:34:51 +01:00
parent ffb2977dc3
commit e3bd3ab6df
15 changed files with 1758 additions and 3 deletions

View File

@@ -0,0 +1,84 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Consts.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Diagnostics.CodeAnalysis;
namespace Aaru.Archives;
[SuppressMessage("ReSharper", "UnusedMember.Local")]
public sealed partial class EwfArchive
{
/// <summary>EWF v1 logical evidence signature: "LVF\x09\x0D\x0A\xFF\x00"</summary>
static readonly byte[] LVF_SIGNATURE = [0x4C, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00];
/// <summary>EWF v2 logical evidence signature: "LEF2\x0D\x0A\x81\x00"</summary>
static readonly byte[] LEF2_SIGNATURE = [0x4C, 0x45, 0x46, 0x32, 0x0D, 0x0A, 0x81, 0x00];
/// <summary>EWF v1 disk image signature (needed for segment traversal)</summary>
static readonly byte[] EVF_SIGNATURE = [0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00];
/// <summary>EWF v2 disk image signature (needed for segment traversal)</summary>
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;
/// <summary>Ltree header size in bytes</summary>
const int LTREE_HEADER_SIZE = 48;
}

View File

@@ -0,0 +1,89 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Enums.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Diagnostics.CodeAnalysis;
namespace Aaru.Archives;
[SuppressMessage("ReSharper", "UnusedMember.Local")]
public sealed partial class EwfArchive
{
/// <summary>EWF v2 compression method</summary>
enum EwfCompressionMethod : ushort
{
None = 0,
Deflate = 1,
Bzip2 = 2
}
/// <summary>EWF v2 section type codes</summary>
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
}
/// <summary>LEF file entry flags from opr field</summary>
[Flags]
enum EwfLefEntryFlags : uint
{
ReadOnly = 0x00000001,
Hidden = 0x00000002,
System = 0x00000004,
Archive = 0x00000008,
Symlink = 0x00000010,
Deleted = 0x00000080,
HardLink = 0x00001000,
AltStream = 0x00002000,
Folder = 0x02000000,
Sparse = 0x04000000
}
}

View File

@@ -0,0 +1,115 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : EwfArchive.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Archives;
/// <inheritdoc />
/// <summary>Implements reading Expert Witness Format logical evidence (L01/Lx01) files</summary>
public sealed partial class EwfArchive : IArchive
{
const string MODULE_NAME = "EWF Logical Evidence plugin";
uint _bytesPerSector;
/// <summary>Cache of decompressed chunks</summary>
Dictionary<ulong, byte[]> _chunkCache;
internal uint _chunkSize;
/// <summary>Map from chunk index to location in segment file</summary>
Dictionary<ulong, (int segmentIndex, long offset, uint size, bool compressed)> _chunkTable;
/// <summary>Compression method for EWF v2</summary>
EwfCompressionMethod _compressionMethod;
/// <summary>Parsed file entries from ltree</summary>
List<EwfFileEntry> _entries;
/// <summary>Whether the image is EWF v2 format</summary>
bool _isV2;
int _maxChunkCache;
uint _sectorsPerChunk;
/// <summary>Ordered list of open segment file streams</summary>
List<Stream> _segmentStreams;
#region IArchive Members
/// <inheritdoc />
public string Name => Localization.EwfArchive_Name;
/// <inheritdoc />
public Guid Id => new("A7B9F4C1-8E63-4D2B-9A1C-5F7E3D6B8C02");
/// <inheritdoc />
public string Author => Authors.NataliaPortillo;
/// <inheritdoc />
public bool Opened { get; private set; }
/// <inheritdoc />
public ArchiveSupportedFeature ArchiveFeatures => ArchiveSupportedFeature.SupportsFilenames |
ArchiveSupportedFeature.SupportsCompression |
ArchiveSupportedFeature.SupportsSubdirectories |
ArchiveSupportedFeature.HasExplicitDirectories |
ArchiveSupportedFeature.HasEntryTimestamp |
ArchiveSupportedFeature.SupportsXAttrs;
/// <inheritdoc />
public int NumberOfEntries => Opened ? _entries.Count : -1;
/// <inheritdoc />
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
}

View File

@@ -0,0 +1,122 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : EwfFileStream.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.IO;
namespace Aaru.Archives;
/// <summary>Read-only stream that reads file data from EWF logical evidence chunks.</summary>
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();
}

192
Aaru.Archives/EWF/Files.cs Normal file
View File

@@ -0,0 +1,192 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Files.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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
}

View File

@@ -0,0 +1,376 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Helpers.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
{
/// <summary>Generates the next segment filename following EWF naming conventions.</summary>
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;
}
/// <summary>Decompresses zlib (RFC 1950) compressed data.</summary>
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;
}
/// <summary>Parses the ltree text data into a flat list of file entries.</summary>
List<EwfFileEntry> 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<EwfFileEntry>();
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;
}
/// <summary>Recursively parses ltree entries.</summary>
void ParseLtreeEntries(string[] lines, ref int lineIndex, string[] fieldNames, string parentPath,
List<EwfFileEntry> entries)
{
if(lineIndex >= lines.Length) return;
// First line of an entry: "<is_parent> <num_children>"
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<string, string>();
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);
}
}
/// <summary>Parses binary extents field: "count offset_hex size_hex"</summary>
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);
}
/// <summary>Parses a POSIX timestamp string to DateTime.</summary>
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;
}
}
/// <summary>Reads and decompresses a chunk.</summary>
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;
}
}

78
Aaru.Archives/EWF/Info.cs Normal file
View File

@@ -0,0 +1,78 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Info.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
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;
}
/// <inheritdoc />
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
}

371
Aaru.Archives/EWF/Open.cs Normal file
View File

@@ -0,0 +1,371 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Open.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
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<IFilter> segmentFilters = DiscoverSegments(filter);
foreach(IFilter f in segmentFilters) _segmentStreams.Add(f.GetDataForkStream());
// Initialize data structures
_chunkTable = new Dictionary<ulong, (int segmentIndex, long offset, uint size, bool compressed)>();
_chunkCache = new Dictionary<ulong, byte[]>();
_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<IFilter> DiscoverSegments(IFilter primaryFilter)
{
List<IFilter> 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<EwfSectionDescriptorV1>(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<EwfVolumeSmartSection>(volumeData);
_sectorsPerChunk = smartVol.sectors_per_chunk;
_bytesPerSector = smartVol.bytes_per_sector;
}
else
{
EwfVolumeSection vol = Marshal.ByteArrayToStructureLittleEndian<EwfVolumeSection>(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<EwfTableHeaderV1>()) return;
var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfTableHeaderV1>()];
segStream.ReadExactly(tableHeaderBytes, 0, tableHeaderBytes.Length);
EwfTableHeaderV1 tableHeader = Marshal.ByteArrayToStructureLittleEndian<EwfTableHeaderV1>(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<EwfLtreeHeader>(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<EwfFileHeaderV2>(headerBytes);
_compressionMethod = (EwfCompressionMethod)fileHeader.compression_method;
int descriptorSize = System.Runtime.InteropServices.Marshal.SizeOf<EwfSectionDescriptorV2>();
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<EwfSectionDescriptorV2>(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<EwfTableHeaderV2>()) return;
var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfTableHeaderV2>()];
segStream.ReadExactly(tableHeaderBytes, 0, tableHeaderBytes.Length);
EwfTableHeaderV2 tableHeader = Marshal.ByteArrayToStructureLittleEndian<EwfTableHeaderV2>(tableHeaderBytes);
ulong firstChunk = tableHeader.first_chunk_number;
var entryCount = (int)tableHeader.number_of_entries;
int entrySize = System.Runtime.InteropServices.Marshal.SizeOf<EwfTableEntryV2>();
for(var i = 0; i < entryCount; i++)
{
var entryBytes = new byte[entrySize];
segStream.ReadExactly(entryBytes, 0, entrySize);
EwfTableEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian<EwfTableEntryV2>(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;
}
}

View File

@@ -0,0 +1,223 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Structs.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Runtime.InteropServices;
namespace Aaru.Archives;
public sealed partial class EwfArchive
{
#region Internal types
/// <summary>Parsed file entry from the ltree</summary>
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;
}
/// <summary>Ltree section header, 48 bytes</summary>
[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
}

View File

@@ -0,0 +1,93 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Xattrs.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
public ErrorNumber ListXAttr(int entryNumber, out List<string> 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;
}
/// <inheritdoc />
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
}

View File

@@ -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);
}
}
}
}

View File

@@ -342,4 +342,7 @@
<data name="Archive_contains_0_files" xml:space="preserve">
<value>[slateblue1]El archivo contiene [green]{0}[/] ficheros[/]</value>
</data>
<data name="EwfArchive_Name" xml:space="preserve">
<value>Evidencia Lógica Expert Witness Format (EWF-L)</value>
</data>
</root>

View File

@@ -350,4 +350,7 @@
<data name="Archive_contains_0_files" xml:space="preserve">
<value>[slateblue1]Archive contains [green]{0}[/] files[/]</value>
</data>
<data name="EwfArchive_Name" xml:space="preserve">
<value>Expert Witness Format Logical Evidence (EWF-L)</value>
</data>
</root>

View File

@@ -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;
}

View File

@@ -160,6 +160,7 @@ Supported archives
==================
* ARC (.ARC)
* Expert Witness Format (EWF)
* HA (.HA)
* PAK (.PAK)
* Symbian Installation File (.SIS)