Implement EWF disk image support.

This commit is contained in:
2026-04-13 15:08:39 +01:00
parent 0640250084
commit ffb2977dc3
15 changed files with 2591 additions and 0 deletions

110
Aaru.Images/EWF/Consts.cs Normal file
View File

@@ -0,0 +1,110 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Consts.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains constants for Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Diagnostics.CodeAnalysis;
namespace Aaru.Images;
[SuppressMessage("ReSharper", "UnusedMember.Local")]
public sealed partial class Ewf
{
/// <summary>EWF v1 disk image signature: "EVF\x09\x0D\x0A\xFF\x00"</summary>
static readonly byte[] EVF_SIGNATURE = [0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00];
/// <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 disk image signature: "EVF2\x0D\x0A\x81\x00"</summary>
static readonly byte[] EVF2_SIGNATURE = [0x45, 0x56, 0x46, 0x32, 0x0D, 0x0A, 0x81, 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>SMART volume section signature</summary>
static readonly byte[] SMART_SIGNATURE = [0x53, 0x4D, 0x41, 0x52, 0x54];
const int SIGNATURE_LENGTH = 8;
/// <summary>Section type strings for EWF v1</summary>
const string SECTION_TYPE_HEADER = "header";
const string SECTION_TYPE_HEADER2 = "header2";
const string SECTION_TYPE_VOLUME = "volume";
const string SECTION_TYPE_DATA = "data";
const string SECTION_TYPE_TABLE = "table";
const string SECTION_TYPE_TABLE2 = "table2";
const string SECTION_TYPE_SECTORS = "sectors";
const string SECTION_TYPE_HASH = "hash";
const string SECTION_TYPE_DIGEST = "digest";
const string SECTION_TYPE_ERROR2 = "error2";
const string SECTION_TYPE_SESSION = "session";
const string SECTION_TYPE_DONE = "done";
const string SECTION_TYPE_NEXT = "next";
/// <summary>EWF v1 section descriptor size</summary>
const int SECTION_DESCRIPTOR_V1_SIZE = 76;
/// <summary>Default sectors per chunk</summary>
const uint DEFAULT_SECTORS_PER_CHUNK = 64;
/// <summary>Default bytes per sector</summary>
const uint DEFAULT_BYTES_PER_SECTOR = 512;
/// <summary>Default chunk size (64 * 512 = 32768)</summary>
const uint DEFAULT_CHUNK_SIZE = DEFAULT_SECTORS_PER_CHUNK * DEFAULT_BYTES_PER_SECTOR;
/// <summary>Compression flag in EWF v1 table entry (most significant bit)</summary>
const uint TABLE_ENTRY_V1_COMPRESSED_FLAG = 0x80000000;
/// <summary>Offset mask for EWF v1 table entry (lower 31 bits)</summary>
const uint TABLE_ENTRY_V1_OFFSET_MASK = 0x7FFFFFFF;
/// <summary>Maximum cached chunks</summary>
const int MAX_CACHE_SIZE = 16777216;
/// <summary>Maximum cached sectors</summary>
const int MAX_CACHED_SECTORS = MAX_CACHE_SIZE / 512;
/// <summary>EWF v1 file header size</summary>
const int FILE_HEADER_V1_SIZE = 13;
/// <summary>EWF v2 file header size</summary>
const int FILE_HEADER_V2_SIZE = 32;
/// <summary>EWF v1 EnCase 5+ volume section data size</summary>
const int VOLUME_SECTION_SIZE_ENCASE = 1052;
/// <summary>EWF v1 SMART/old EnCase volume section data size</summary>
const int VOLUME_SECTION_SIZE_SMART = 94;
/// <summary>Session entry flag: audio track</summary>
const uint SESSION_ENTRY_FLAG_IS_AUDIO = 0x00000001;
}

121
Aaru.Images/EWF/EWF.cs Normal file
View File

@@ -0,0 +1,121 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : EWF.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Manages Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Collections.Generic;
using System.IO;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Track = Aaru.CommonTypes.Structs.Track;
namespace Aaru.Images;
/// <inheritdoc cref="Aaru.CommonTypes.Interfaces.IOpticalMediaImage" />
/// <summary>Implements reading Expert Witness Format (EWF) disk images</summary>
public sealed partial class Ewf : IOpticalMediaImage
{
const string MODULE_NAME = "EWF plugin";
/// <summary>List of bad sector ranges from error2 sections</summary>
List<(ulong start, uint count)> _badSectors;
uint _bytesPerSector;
/// <summary>Cache of decompressed chunks</summary>
Dictionary<ulong, byte[]> _chunkCache;
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 header metadata key/value pairs</summary>
Dictionary<string, string> _headerValues;
ImageInfo _imageInfo;
/// <summary>Whether the image uses the SMART volume section format</summary>
bool _isSmart;
/// <summary>Whether the image is EWF v2 format</summary>
bool _isV2;
int _maxChunkCache;
/// <summary>Stored MD5 hash from hash/digest section</summary>
byte[] _md5Stored;
/// <summary>Populated metadata from header sections</summary>
Metadata _metadata;
/// <summary>Cache of read sectors</summary>
Dictionary<ulong, byte[]> _sectorCache;
uint _sectorsPerChunk;
/// <summary>Ordered list of open segment file streams</summary>
List<Stream> _segmentStreams;
/// <summary>Session list for optical media</summary>
List<Session> _sessions;
/// <summary>Stored SHA1 hash from digest section</summary>
byte[] _sha1Stored;
/// <summary>Track list for optical media</summary>
List<Track> _tracks;
public Ewf() => _imageInfo = new ImageInfo
{
ReadableSectorTags = [],
ReadableMediaTags = [],
HasPartitions = false,
HasSessions = false,
Version = null,
Application = null,
ApplicationVersion = null,
Creator = null,
Comments = null,
MediaManufacturer = null,
MediaModel = null,
MediaSerialNumber = null,
MediaBarcode = null,
MediaPartNumber = null,
MediaSequence = 0,
LastMediaSequence = 0,
DriveManufacturer = null,
DriveModel = null,
DriveSerialNumber = null,
DriveFirmwareRevision = null
};
}

99
Aaru.Images/EWF/Enums.cs Normal file
View File

@@ -0,0 +1,99 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Enums.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains enumerations for Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Diagnostics.CodeAnalysis;
namespace Aaru.Images;
[SuppressMessage("ReSharper", "UnusedMember.Local")]
public sealed partial class Ewf
{
/// <summary>EWF media type as stored in the volume section</summary>
enum EwfMediaType : byte
{
Removable = 0x00,
Fixed = 0x01,
Optical = 0x03,
SingleFiles = 0x0E,
Memory = 0x10
}
/// <summary>EWF media flags as stored in the volume section</summary>
enum EwfMediaFlags : byte
{
Image = 0x01,
Physical = 0x02,
Fastbloc = 0x04,
Tableau = 0x08
}
/// <summary>EWF v1 compression level</summary>
enum EwfCompressionLevel : byte
{
None = 0x00,
Fast = 0x01,
Best = 0x02
}
/// <summary>EWF v2 compression method as stored in the file header</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
}
}

342
Aaru.Images/EWF/Helpers.cs Normal file
View File

@@ -0,0 +1,342 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Helpers.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains helper methods for Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Track = Aaru.CommonTypes.Structs.Track;
namespace Aaru.Images;
public sealed partial class Ewf
{
/// <summary>
/// Generates the next segment filename following EWF naming conventions.
/// E01→E02→...→E99→EAA→EAB→...→EZZ→FAA→...→ZZZ for v1 EnCase.
/// Ex01→Ex02→...→Ex99→ExAA→...→ExZZ for v2.
/// s01→s02→...→s99→saa→...→szz for SMART.
/// </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;
// Determine if we're in v2 mode (.Ex01 has 5-char extension)
bool isV2 = ext.Length == 5; // .Ex01
bool isLower = char.IsLower(ext[1]);
if(isV2)
{
// .Ex01 → .Ex02 → ... → .Ex99 → .ExAA → ... → .ExZZ
string numPart = ext.Substring(3); // "01" from ".Ex01"
char prefix1 = ext[1]; // 'E'
char prefix2 = ext[2]; // 'x'
if(int.TryParse(numPart, out int num) && num < 99)
return Path.Combine(dir, name + ext.Substring(0, 3) + (num + 1).ToString("D2"));
if(int.TryParse(numPart, out _))
{
// 99 → AA
char a = isLower ? 'a' : 'A';
return Path.Combine(dir, name + $".{prefix1}{prefix2}{a}{a}");
}
// AA → AB → ... → AZ → BA → ... → ZZ
char c1 = ext[3];
char c2 = ext[4];
char baseChar = isLower ? 'a' : 'A';
char maxChar = isLower ? 'z' : 'Z';
if(c2 < maxChar) return Path.Combine(dir, name + $".{prefix1}{prefix2}{c1}{(char)(c2 + 1)}");
if(c1 < maxChar) return Path.Combine(dir, name + $".{prefix1}{prefix2}{(char)(c1 + 1)}{baseChar}");
return null; // ZZ reached
}
// v1: .E01 → .E02 → ... → .E99 → .EAA → ... → .EZZ → .FAA → ... → .ZZZ
// SMART: .s01 → .s02 → ... → .s99 → .saa → ... → .szz
char firstChar = ext[1]; // 'E' or 's'
string suffix = ext.Substring(2); // "01" from ".E01"
if(int.TryParse(suffix, out int segNum) && segNum < 99)
return Path.Combine(dir, name + $".{firstChar}{segNum + 1:D2}");
if(int.TryParse(suffix, out _))
{
// 99 → AA (or aa for lowercase)
char a = isLower ? 'a' : 'A';
return Path.Combine(dir, name + $".{firstChar}{a}{a}");
}
// Letter suffixes: EAA → EAB → ... → EAZ → EBA → ... → EZZ → FAA → ... → ZZZ
char ch1 = ext[1]; // first letter (E, F, ..., Z) or (s for SMART - but SMART only goes to szz)
char ch2 = ext[2]; // second letter
char ch3 = ext[3]; // third letter
char lBase = isLower ? 'a' : 'A';
char lMax = isLower ? 'z' : 'Z';
if(ch3 < lMax) return Path.Combine(dir, name + $".{ch1}{ch2}{(char)(ch3 + 1)}");
if(ch2 < lMax) return Path.Combine(dir, name + $".{ch1}{(char)(ch2 + 1)}{lBase}");
if(ch1 < lMax) return Path.Combine(dir, name + $".{(char)(ch1 + 1)}{lBase}{lBase}");
return null; // ZZZ reached
}
/// <summary>
/// Parses EWF header text from a raw (already decompressed) byte array.
/// header sections use ASCII; header2 sections use UTF-16 LE.
/// </summary>
static Dictionary<string, string> ParseHeaderText(byte[] data, bool isHeader2)
{
string text;
if(isHeader2)
{
// UTF-16 LE, may have BOM
if(data.Length >= 2 && data[0] == 0xFF && data[1] == 0xFE)
text = Encoding.Unicode.GetString(data, 2, data.Length - 2);
else
text = Encoding.Unicode.GetString(data);
}
else
text = Encoding.ASCII.GetString(data);
var result = new Dictionary<string, string>();
// Header text format is category-based with newline-delimited sections.
// Most common format:
// Line 1: category number (e.g., "1" or "3")
// Line 2: "main" or empty
// Line 3: tab-separated field identifiers (e.g., "c\tn\ta\te\tt\tav\tov\tm\tu\tp\tr")
// Line 4: tab-separated field values
// The remaining lines may repeat the pattern or be empty.
string[] lines = text.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
if(lines.Length < 4) return result;
// Try to find the field identifiers and values lines
// Look for the line containing tab-separated single-letter or short identifiers
for(var i = 0; i < lines.Length - 1; i++)
{
string line = lines[i].Trim();
if(!line.Contains('\t')) continue;
string[] keys = line.Split('\t');
// Verify this looks like an identifier line (short keys)
var isKeyLine = true;
foreach(string key in keys)
{
if(key.Length > 4)
{
isKeyLine = false;
break;
}
}
if(!isKeyLine) continue;
// Next line should contain the values
if(i + 1 >= lines.Length) break;
string[] values = lines[i + 1].Trim().Split('\t');
for(var j = 0; j < keys.Length && j < values.Length; j++)
{
string key = keys[j].Trim();
string value = values[j].Trim();
if(!string.IsNullOrEmpty(key) && !result.ContainsKey(key)) result[key] = value;
}
break; // Only process the first key/value pair set
}
return result;
}
/// <summary>
/// Decompresses zlib (RFC 1950) compressed data by stripping the 2-byte header
/// and using DeflateStream.
/// </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];
// Skip 2-byte zlib header (CMF + FLG)
using var ms = new MemoryStream(compressedData, 2, compressedData.Length - 2);
using var deflate = new DeflateStream(ms, CompressionMode.Decompress);
var totalRead = 0;
while(totalRead < uncompressedSize)
{
int read = deflate.Read(decompressed, totalRead, uncompressedSize - totalRead);
if(read == 0) break;
totalRead += read;
}
return decompressed;
}
/// <summary>
/// Builds session and track lists from EWF session entries using the libewf reconstruction algorithm.
/// </summary>
static void BuildSessionsAndTracks(List<(ulong startSector, uint flags)> sessionEntries, ulong totalSectors,
uint bytesPerSector, out List<Session> sessions, out List<Track> tracks)
{
sessions = [];
tracks = [];
if(sessionEntries is not { Count: > 0 })
{
// No session entries — create a single session with one data track
sessions.Add(new Session
{
Sequence = 1,
StartSector = 0,
EndSector = totalSectors - 1,
StartTrack = 1,
EndTrack = 1
});
tracks.Add(new Track
{
Sequence = 1,
Session = 1,
StartSector = 0,
EndSector = totalSectors - 1,
Type = TrackType.Data,
BytesPerSector = (int)bytesPerSector,
RawBytesPerSector = (int)bytesPerSector,
SubchannelType = TrackSubchannelType.None
});
return;
}
// Build tracks from session entries
// Each entry defines the start of a region; its type is determined by the audio flag.
ushort sessionSequence = 1;
uint trackSequence = 1;
ulong sessionStart = 0;
uint sessionStartTrack = 1;
for(var i = 0; i < sessionEntries.Count; i++)
{
(ulong startSector, uint flags) = sessionEntries[i];
bool isAudio = (flags & SESSION_ENTRY_FLAG_IS_AUDIO) != 0;
ulong endSector = i + 1 < sessionEntries.Count ? sessionEntries[i + 1].startSector - 1 : totalSectors - 1;
// If this is a data entry and there was a previous audio entry, we're starting a new session
if(!isAudio && i > 0 && (sessionEntries[i - 1].flags & SESSION_ENTRY_FLAG_IS_AUDIO) != 0)
{
// Close previous session
sessions.Add(new Session
{
Sequence = sessionSequence,
StartSector = sessionStart,
EndSector = startSector - 1,
StartTrack = sessionStartTrack,
EndTrack = trackSequence - 1
});
sessionSequence++;
sessionStart = startSector;
sessionStartTrack = trackSequence;
}
else if(!isAudio && i > 0 && (sessionEntries[i - 1].flags & SESSION_ENTRY_FLAG_IS_AUDIO) == 0)
{
// Data followed by data — new session boundary
sessions.Add(new Session
{
Sequence = sessionSequence,
StartSector = sessionStart,
EndSector = startSector - 1,
StartTrack = sessionStartTrack,
EndTrack = trackSequence - 1
});
sessionSequence++;
sessionStart = startSector;
sessionStartTrack = trackSequence;
}
tracks.Add(new Track
{
Sequence = trackSequence,
Session = sessionSequence,
StartSector = startSector,
EndSector = endSector,
Type = isAudio ? TrackType.Audio : TrackType.CdMode1,
BytesPerSector = isAudio ? 2352 : (int)bytesPerSector,
RawBytesPerSector = isAudio ? 2352 : (int)bytesPerSector,
SubchannelType = TrackSubchannelType.None
});
trackSequence++;
}
// Close last session
sessions.Add(new Session
{
Sequence = sessionSequence,
StartSector = sessionStart,
EndSector = totalSectors - 1,
StartTrack = sessionStartTrack,
EndTrack = trackSequence - 1
});
}
}

View File

@@ -0,0 +1,70 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Identify.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Identifies Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.IO;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region IOpticalMediaImage Members
/// <inheritdoc />
public bool Identify(IFilter imageFilter)
{
Stream stream = imageFilter.GetDataForkStream();
stream.Seek(0, SeekOrigin.Begin);
if(stream.Length < SIGNATURE_LENGTH) return false;
var signatureBytes = new byte[SIGNATURE_LENGTH];
stream.EnsureRead(signatureBytes, 0, SIGNATURE_LENGTH);
AaruLogging.Debug(MODULE_NAME, Localization.Ewf_Identify_Signature_0, BitConverter.ToString(signatureBytes));
if(signatureBytes.AsSpan().SequenceEqual(EVF_SIGNATURE)) return true;
if(signatureBytes.AsSpan().SequenceEqual(LVF_SIGNATURE)) return true;
if(signatureBytes.AsSpan().SequenceEqual(EVF2_SIGNATURE)) return true;
if(signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE)) return true;
return false;
}
#endregion
}

808
Aaru.Images/EWF/Open.cs Normal file
View File

@@ -0,0 +1,808 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Open.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Opens Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <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;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
using Aaru.Logging;
using Marshal = Aaru.Helpers.Marshal;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region IOpticalMediaImage Members
/// <inheritdoc />
public ErrorNumber Open(IFilter imageFilter)
{
Stream stream = imageFilter.GetDataForkStream();
stream.Seek(0, SeekOrigin.Begin);
if(stream.Length < SIGNATURE_LENGTH) return ErrorNumber.InvalidArgument;
// Read signature to determine version
var signatureBytes = new byte[SIGNATURE_LENGTH];
stream.EnsureRead(signatureBytes, 0, SIGNATURE_LENGTH);
_isV2 = signatureBytes.AsSpan().SequenceEqual(EVF2_SIGNATURE) ||
signatureBytes.AsSpan().SequenceEqual(LEF2_SIGNATURE);
AaruLogging.Debug(MODULE_NAME, "EWF version {0}", _isV2 ? "2" : "1");
// Discover all segment files
_segmentStreams = [];
List<IFilter> segmentFilters = DiscoverSegments(imageFilter);
if(segmentFilters.Count == 0) return ErrorNumber.InvalidArgument;
foreach(IFilter filter in segmentFilters) _segmentStreams.Add(filter.GetDataForkStream());
// Initialize data structures
_chunkTable = new Dictionary<ulong, (int segmentIndex, long offset, uint size, bool compressed)>();
_chunkCache = new Dictionary<ulong, byte[]>();
_sectorCache = new Dictionary<ulong, byte[]>();
_badSectors = [];
_headerValues = new Dictionary<string, string>();
_md5Stored = null;
_sha1Stored = null;
_isSmart = false;
_sectorsPerChunk = DEFAULT_SECTORS_PER_CHUNK;
_bytesPerSector = DEFAULT_BYTES_PER_SECTOR;
_chunkSize = DEFAULT_CHUNK_SIZE;
ulong totalSectors = 0;
uint chsC = 0;
uint chsH = 0;
uint chsS = 0;
byte mediaType = 0;
byte mediaFlags = 0;
var volumeFound = false;
var sessionEntries = new List<(ulong startSector, uint flags)>();
ulong currentChunk = 0;
byte[] headerData = null;
byte[] header2Data = null;
// Process each segment file
for(var segIdx = 0; segIdx < _segmentStreams.Count; segIdx++)
{
Stream segStream = _segmentStreams[segIdx];
if(_isV2)
{
ParseSegmentV2(segStream,
segIdx,
ref currentChunk,
ref volumeFound,
ref totalSectors,
ref mediaType,
ref mediaFlags,
ref chsC,
ref chsH,
ref chsS,
sessionEntries);
}
else
{
ParseSegmentV1(segStream,
segIdx,
ref currentChunk,
ref volumeFound,
ref totalSectors,
ref headerData,
ref header2Data,
ref mediaType,
ref mediaFlags,
ref chsC,
ref chsH,
ref chsS,
sessionEntries);
}
}
if(!volumeFound)
{
AaruLogging.Error(MODULE_NAME, "No volume section found in any segment file");
return ErrorNumber.InvalidArgument;
}
_chunkSize = _sectorsPerChunk * _bytesPerSector;
_maxChunkCache = (int)(MAX_CACHE_SIZE / _chunkSize);
if(_maxChunkCache < 1) _maxChunkCache = 1;
// Parse header metadata (prefer header2 over header)
if(header2Data is { Length: > 0 })
_headerValues = ParseHeaderText(header2Data, true);
else if(headerData is { Length: > 0 }) _headerValues = ParseHeaderText(headerData, false);
// Populate image info
_imageInfo.Sectors = totalSectors;
_imageInfo.SectorSize = _bytesPerSector;
_imageInfo.Cylinders = chsC;
_imageInfo.Heads = chsH;
_imageInfo.SectorsPerTrack = chsS;
_imageInfo.ImageSize = totalSectors * _bytesPerSector;
_imageInfo.Application = "EnCase";
_imageInfo.CreationTime = imageFilter.CreationTime;
_imageInfo.LastModificationTime = imageFilter.LastWriteTime;
if(_isSmart) _imageInfo.Application = "SMART";
// Map header values to image info
if(_headerValues.TryGetValue("a", out string description)) _imageInfo.Comments = description;
if(_headerValues.TryGetValue("av", out string appVersion)) _imageInfo.ApplicationVersion = appVersion;
if(_headerValues.TryGetValue("ov", out string osVersion)) _imageInfo.Creator = osVersion;
if(_headerValues.TryGetValue("md", out string model)) _imageInfo.MediaModel = model;
if(_headerValues.TryGetValue("sn", out string serial)) _imageInfo.MediaSerialNumber = serial;
// Determine media type
if(_isSmart)
{
_imageInfo.MediaType = MediaType.GENERIC_HDD;
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
}
else
{
var ewfMediaType = (EwfMediaType)mediaType;
switch(ewfMediaType)
{
case EwfMediaType.Fixed:
_imageInfo.MediaType = MediaType.GENERIC_HDD;
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
break;
case EwfMediaType.Removable:
_imageInfo.MediaType = MediaType.FlashDrive;
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
break;
case EwfMediaType.Optical:
_imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc;
// Refine optical type based on sector size and count
if(_bytesPerSector == 2048)
{
_imageInfo.MediaType = totalSectors <= 360000
? MediaType.CDROM
: totalSectors <= 2295104
? MediaType.DVDROM
: MediaType.BDROM;
}
else
_imageInfo.MediaType = MediaType.CD;
break;
case EwfMediaType.Memory:
_imageInfo.MediaType = MediaType.GENERIC_HDD;
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
break;
default:
_imageInfo.MediaType = MediaType.Unknown;
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
break;
}
}
// Build sessions and tracks
if(_imageInfo.MetadataMediaType == MetadataMediaType.OpticalDisc)
{
_imageInfo.HasSessions = true;
_imageInfo.HasPartitions = true;
BuildSessionsAndTracks(sessionEntries, totalSectors, _bytesPerSector, out _sessions, out _tracks);
}
else
{
_sessions = null;
_tracks = null;
}
// Build metadata from header values
_metadata = null;
if(_headerValues.Count > 0) BuildAaruMetadata();
_imageInfo.Version = _isV2 ? "2" : "1";
AaruLogging.Debug(MODULE_NAME, "Image has {0} sectors of {1} bytes", totalSectors, _bytesPerSector);
AaruLogging.Debug(MODULE_NAME, "Chunk table has {0} entries", _chunkTable.Count);
return ErrorNumber.NoError;
}
#endregion
/// <summary>Discovers all segment files related to the primary image file.</summary>
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;
}
/// <summary>Parses an EWF v1 segment file, extracting all sections.</summary>
void ParseSegmentV1(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound,
ref ulong totalSectors, ref byte[] headerData, ref byte[] header2Data, ref byte mediaType,
ref byte mediaFlags, ref uint chsC, ref uint chsH, ref uint chsS,
List<(ulong startSector, uint flags)> sessionEntries)
{
// Skip file header (13 bytes)
segStream.Seek(FILE_HEADER_V1_SIZE, SeekOrigin.Begin);
while(segStream.Position < segStream.Length)
{
long sectionStart = segStream.Position;
// Read section descriptor
var descBytes = new byte[SECTION_DESCRIPTOR_V1_SIZE];
int bytesRead = segStream.Read(descBytes, 0, SECTION_DESCRIPTOR_V1_SIZE);
if(bytesRead < SECTION_DESCRIPTOR_V1_SIZE) break;
EwfSectionDescriptorV1 descriptor =
Marshal.ByteArrayToStructureLittleEndian<EwfSectionDescriptorV1>(descBytes);
// Get section type string (null-terminated)
string sectionType = Encoding.ASCII.GetString(descriptor.type_string).TrimEnd('\0');
AaruLogging.Debug(MODULE_NAME,
"Section type: {0} at offset {1}, next at {2}, size {3}",
sectionType,
sectionStart,
descriptor.next_offset,
descriptor.size);
// Calculate data size (section size minus section descriptor size)
long dataSize = (long)descriptor.size - SECTION_DESCRIPTOR_V1_SIZE;
switch(sectionType)
{
case SECTION_TYPE_HEADER:
if(headerData == null && dataSize > 0)
{
var compressedHeader = new byte[dataSize];
segStream.EnsureRead(compressedHeader, 0, (int)dataSize);
try
{
headerData = DecompressZlib(compressedHeader, (int)dataSize * 10);
}
catch(Exception ex)
{
AaruLogging.Debug(MODULE_NAME, "Failed to decompress header: {0}", ex.Message);
}
}
break;
case SECTION_TYPE_HEADER2:
if(header2Data == null && dataSize > 0)
{
var compressedHeader2 = new byte[dataSize];
segStream.EnsureRead(compressedHeader2, 0, (int)dataSize);
try
{
header2Data = DecompressZlib(compressedHeader2, (int)dataSize * 10);
}
catch(Exception ex)
{
AaruLogging.Debug(MODULE_NAME, "Failed to decompress header2: {0}", ex.Message);
}
}
break;
case SECTION_TYPE_VOLUME:
case SECTION_TYPE_DATA:
if(!volumeFound)
{
ParseVolumeSection(segStream,
dataSize,
ref volumeFound,
ref totalSectors,
ref mediaType,
ref mediaFlags,
ref chsC,
ref chsH,
ref chsS);
}
break;
case SECTION_TYPE_TABLE:
case SECTION_TYPE_TABLE2:
ParseTableSectionV1(segStream, segIdx, dataSize, ref currentChunk, (long)descriptor.next_offset);
break;
case SECTION_TYPE_HASH:
if(_md5Stored == null &&
dataSize >= System.Runtime.InteropServices.Marshal.SizeOf<EwfHashSection>())
{
var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfHashSection>()];
segStream.EnsureRead(hashBytes, 0, hashBytes.Length);
EwfHashSection hash = Marshal.ByteArrayToStructureLittleEndian<EwfHashSection>(hashBytes);
_md5Stored = hash.md5_hash;
AaruLogging.Debug(MODULE_NAME,
"MD5 from hash section: {0}",
BitConverter.ToString(_md5Stored).Replace("-", "").ToLowerInvariant());
}
break;
case SECTION_TYPE_DIGEST:
if(dataSize >= System.Runtime.InteropServices.Marshal.SizeOf<EwfDigestSection>())
{
var digestBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfDigestSection>()];
segStream.EnsureRead(digestBytes, 0, digestBytes.Length);
EwfDigestSection digest =
Marshal.ByteArrayToStructureLittleEndian<EwfDigestSection>(digestBytes);
_md5Stored = digest.md5_hash;
_sha1Stored = digest.sha1_hash;
AaruLogging.Debug(MODULE_NAME,
"MD5 from digest section: {0}",
BitConverter.ToString(_md5Stored).Replace("-", "").ToLowerInvariant());
AaruLogging.Debug(MODULE_NAME,
"SHA1 from digest section: {0}",
BitConverter.ToString(_sha1Stored).Replace("-", "").ToLowerInvariant());
}
break;
case SECTION_TYPE_ERROR2:
ParseErrorSectionV1(segStream, dataSize);
break;
case SECTION_TYPE_SESSION:
ParseSessionSectionV1(segStream, dataSize, sessionEntries);
break;
}
// Move to next section
if(descriptor.next_offset == 0 || sectionType is SECTION_TYPE_DONE or SECTION_TYPE_NEXT) break;
segStream.Seek((long)descriptor.next_offset, SeekOrigin.Begin);
}
}
/// <summary>Parses the volume (or data) section to extract media parameters.</summary>
void ParseVolumeSection(Stream segStream, long dataSize, ref bool volumeFound, ref ulong totalSectors,
ref byte mediaType, ref byte mediaFlags, ref uint chsC, ref uint chsH, ref uint chsS)
{
if(dataSize < VOLUME_SECTION_SIZE_SMART) return;
// Read all the volume data available
var volumeData = new byte[dataSize];
segStream.EnsureRead(volumeData, 0, (int)dataSize);
// Determine format by checking if it's a small (94-byte) or large (1052-byte) volume section
if(dataSize < VOLUME_SECTION_SIZE_ENCASE)
{
// Small volume section: SMART or old EnCase format (94 bytes of data)
EwfVolumeSmartSection smartVol =
Marshal.ByteArrayToStructureLittleEndian<EwfVolumeSmartSection>(volumeData);
_sectorsPerChunk = smartVol.sectors_per_chunk;
_bytesPerSector = smartVol.bytes_per_sector;
totalSectors = smartVol.number_of_sectors;
// Check SMART signature
if(smartVol.signature != null && smartVol.signature.AsSpan().SequenceEqual(SMART_SIGNATURE))
_isSmart = true;
AaruLogging.Debug(MODULE_NAME,
"{0} volume: {1} sectors, {2} bytes/sector, {3} sectors/chunk",
_isSmart ? "SMART" : "Old EnCase",
totalSectors,
_bytesPerSector,
_sectorsPerChunk);
}
else
{
// Large volume section: EnCase 5+ format (1052 bytes of data)
EwfVolumeSection vol = Marshal.ByteArrayToStructureLittleEndian<EwfVolumeSection>(volumeData);
_sectorsPerChunk = vol.sectors_per_chunk;
_bytesPerSector = vol.bytes_per_sector;
totalSectors = vol.number_of_sectors;
chsC = vol.chs_cylinders;
chsH = vol.chs_heads;
chsS = vol.chs_sectors;
mediaType = vol.media_type;
mediaFlags = vol.media_flags;
AaruLogging.Debug(MODULE_NAME,
"EnCase volume: {0} sectors, {1} bytes/sector, {2} sectors/chunk, C/H/S={3}/{4}/{5}",
totalSectors,
_bytesPerSector,
_sectorsPerChunk,
chsC,
chsH,
chsS);
AaruLogging.Debug(MODULE_NAME,
"Media type: {0}, media flags: {1}, compression: {2}",
(EwfMediaType)vol.media_type,
(EwfMediaFlags)vol.media_flags,
(EwfCompressionLevel)vol.compression_level);
}
volumeFound = true;
}
/// <summary>Parses an EWF v1 table section, building the chunk→location mapping.</summary>
void ParseTableSectionV1(Stream segStream, int segIdx, long dataSize, ref ulong currentChunk,
long nextSectionOffset)
{
if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf<EwfTableHeaderV1>()) return;
// Read table header
var tableHeaderBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfTableHeaderV1>()];
segStream.EnsureRead(tableHeaderBytes, 0, tableHeaderBytes.Length);
EwfTableHeaderV1 tableHeader = Marshal.ByteArrayToStructureLittleEndian<EwfTableHeaderV1>(tableHeaderBytes);
AaruLogging.Debug(MODULE_NAME,
"Table: {0} entries, base offset {1}",
tableHeader.number_of_entries,
tableHeader.base_offset);
// Read all table entries
var entryCount = (int)tableHeader.number_of_entries;
var entryData = new byte[entryCount * 4];
segStream.EnsureRead(entryData, 0, entryData.Length);
// Skip entries checksum (4 bytes)
segStream.Seek(4, SeekOrigin.Current);
for(var i = 0; i < entryCount; i++)
{
var rawEntry = BitConverter.ToUInt32(entryData, i * 4);
bool compressed = (rawEntry & TABLE_ENTRY_V1_COMPRESSED_FLAG) != 0;
var offset = (long)(tableHeader.base_offset + (rawEntry & TABLE_ENTRY_V1_OFFSET_MASK));
// Calculate chunk size: distance to next chunk or next section for the last entry
uint size;
if(i + 1 < entryCount)
{
var nextRawEntry = BitConverter.ToUInt32(entryData, (i + 1) * 4);
var nextOffset = (long)(tableHeader.base_offset + (nextRawEntry & TABLE_ENTRY_V1_OFFSET_MASK));
size = (uint)(nextOffset - offset);
}
else
{
// For the last entry, use the next section offset to compute size
if(nextSectionOffset > offset)
size = (uint)(nextSectionOffset - offset);
else
size = _chunkSize + 4; // fallback: uncompressed chunk + Adler-32
}
ulong chunkIndex = currentChunk + (ulong)i;
if(!_chunkTable.ContainsKey(chunkIndex)) _chunkTable[chunkIndex] = (segIdx, offset, size, compressed);
}
currentChunk += (ulong)entryCount;
}
/// <summary>Parses an EWF v1 error2 section.</summary>
void ParseErrorSectionV1(Stream segStream, long dataSize)
{
if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorHeaderV1>()) return;
var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorHeaderV1>()];
segStream.EnsureRead(headerBytes, 0, headerBytes.Length);
EwfErrorHeaderV1 errorHeader = Marshal.ByteArrayToStructureLittleEndian<EwfErrorHeaderV1>(headerBytes);
int entrySize = System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorEntryV1>();
for(var i = 0; i < (int)errorHeader.number_of_entries; i++)
{
var entryBytes = new byte[entrySize];
segStream.EnsureRead(entryBytes, 0, entrySize);
EwfErrorEntryV1 entry = Marshal.ByteArrayToStructureLittleEndian<EwfErrorEntryV1>(entryBytes);
_badSectors.Add((entry.start_sector, entry.number_of_sectors));
AaruLogging.Debug(MODULE_NAME,
"Bad sector range: start={0}, count={1}",
entry.start_sector,
entry.number_of_sectors);
}
}
/// <summary>Parses an EWF v1 session section.</summary>
void ParseSessionSectionV1(Stream segStream, long dataSize, List<(ulong startSector, uint flags)> sessionEntries)
{
if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionHeaderV1>()) return;
var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionHeaderV1>()];
segStream.EnsureRead(headerBytes, 0, headerBytes.Length);
EwfSessionHeaderV1 sessionHeader = Marshal.ByteArrayToStructureLittleEndian<EwfSessionHeaderV1>(headerBytes);
AaruLogging.Debug(MODULE_NAME, "Session section: {0} entries", sessionHeader.number_of_entries);
int entrySize = System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionEntryV1>();
for(var i = 0; i < (int)sessionHeader.number_of_entries; i++)
{
var entryBytes = new byte[entrySize];
segStream.EnsureRead(entryBytes, 0, entrySize);
EwfSessionEntryV1 entry = Marshal.ByteArrayToStructureLittleEndian<EwfSessionEntryV1>(entryBytes);
sessionEntries.Add((entry.start_sector, entry.flags));
AaruLogging.Debug(MODULE_NAME, "Session entry: start={0}, flags=0x{1:X8}", entry.start_sector, entry.flags);
}
}
/// <summary>Parses an EWF v2 segment file.</summary>
void ParseSegmentV2(Stream segStream, int segIdx, ref ulong currentChunk, ref bool volumeFound,
ref ulong totalSectors, ref byte mediaType, ref byte mediaFlags, ref uint chsC, ref uint chsH,
ref uint chsS, List<(ulong startSector, uint flags)> sessionEntries)
{
// Read v2 file header
segStream.Seek(0, SeekOrigin.Begin);
var headerBytes = new byte[FILE_HEADER_V2_SIZE];
segStream.EnsureRead(headerBytes, 0, FILE_HEADER_V2_SIZE);
EwfFileHeaderV2 fileHeader = Marshal.ByteArrayToStructureLittleEndian<EwfFileHeaderV2>(headerBytes);
_compressionMethod = (EwfCompressionMethod)fileHeader.compression_method;
AaruLogging.Debug(MODULE_NAME,
"EWF v2 segment {0}, compression method: {1}",
fileHeader.segment_number,
_compressionMethod);
// EWF v2 sections are read backward from the end of the file
int descriptorSize = System.Runtime.InteropServices.Marshal.SizeOf<EwfSectionDescriptorV2>();
long position = segStream.Length - descriptorSize;
while(position >= FILE_HEADER_V2_SIZE)
{
segStream.Seek(position, SeekOrigin.Begin);
var descBytes = new byte[descriptorSize];
segStream.EnsureRead(descBytes, 0, descriptorSize);
EwfSectionDescriptorV2 descriptor =
Marshal.ByteArrayToStructureLittleEndian<EwfSectionDescriptorV2>(descBytes);
var sectionType = (EwfSectionTypeV2)descriptor.type;
long dataStart = position - (long)descriptor.data_size;
AaruLogging.Debug(MODULE_NAME,
"V2 section type: {0} at {1}, data size {2}, previous at {3}",
sectionType,
position,
descriptor.data_size,
descriptor.previous_offset);
switch(sectionType)
{
case EwfSectionTypeV2.SectorTable:
segStream.Seek(dataStart, SeekOrigin.Begin);
ParseTableSectionV2(segStream, segIdx, (long)descriptor.data_size, ref currentChunk);
break;
case EwfSectionTypeV2.Md5Hash:
if(_md5Stored == null &&
(long)descriptor.data_size >= System.Runtime.InteropServices.Marshal.SizeOf<EwfMd5HashV2>())
{
segStream.Seek(dataStart, SeekOrigin.Begin);
var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfMd5HashV2>()];
segStream.EnsureRead(hashBytes, 0, hashBytes.Length);
EwfMd5HashV2 hash = Marshal.ByteArrayToStructureLittleEndian<EwfMd5HashV2>(hashBytes);
_md5Stored = hash.md5_hash;
}
break;
case EwfSectionTypeV2.Sha1Hash:
if(_sha1Stored == null &&
(long)descriptor.data_size >= System.Runtime.InteropServices.Marshal.SizeOf<EwfSha1HashV2>())
{
segStream.Seek(dataStart, SeekOrigin.Begin);
var hashBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfSha1HashV2>()];
segStream.EnsureRead(hashBytes, 0, hashBytes.Length);
EwfSha1HashV2 hash = Marshal.ByteArrayToStructureLittleEndian<EwfSha1HashV2>(hashBytes);
_sha1Stored = hash.sha1_hash;
}
break;
case EwfSectionTypeV2.ErrorTable:
segStream.Seek(dataStart, SeekOrigin.Begin);
ParseErrorSectionV2(segStream, (long)descriptor.data_size);
break;
case EwfSectionTypeV2.SessionTable:
segStream.Seek(dataStart, SeekOrigin.Begin);
ParseSessionSectionV2(segStream, (long)descriptor.data_size, sessionEntries);
break;
}
// Move to previous section
if(descriptor.previous_offset == 0) break;
position = (long)descriptor.previous_offset;
}
}
/// <summary>Parses an EWF v2 table section.</summary>
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.EnsureRead(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>();
AaruLogging.Debug(MODULE_NAME, "V2 Table: {0} entries, first chunk {1}", entryCount, firstChunk);
for(var i = 0; i < entryCount; i++)
{
var entryBytes = new byte[entrySize];
segStream.EnsureRead(entryBytes, 0, entrySize);
EwfTableEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian<EwfTableEntryV2>(entryBytes);
// Flag 0x00000001 means uncompressed in v2
bool compressed = (entry.chunk_data_flags & 0x00000001) == 0;
ulong chunkIndex = firstChunk + (ulong)i;
if(!_chunkTable.ContainsKey(chunkIndex))
_chunkTable[chunkIndex] = (segIdx, (long)entry.chunk_data_offset, entry.chunk_data_size, compressed);
}
if(firstChunk + (ulong)entryCount > currentChunk) currentChunk = firstChunk + (ulong)entryCount;
}
/// <summary>Parses an EWF v2 error section.</summary>
void ParseErrorSectionV2(Stream segStream, long dataSize)
{
if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorHeaderV2>()) return;
var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorHeaderV2>()];
segStream.EnsureRead(headerBytes, 0, headerBytes.Length);
EwfErrorHeaderV2 errorHeader = Marshal.ByteArrayToStructureLittleEndian<EwfErrorHeaderV2>(headerBytes);
int entrySize = System.Runtime.InteropServices.Marshal.SizeOf<EwfErrorEntryV2>();
for(var i = 0; i < (int)errorHeader.number_of_entries; i++)
{
var entryBytes = new byte[entrySize];
segStream.EnsureRead(entryBytes, 0, entrySize);
EwfErrorEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian<EwfErrorEntryV2>(entryBytes);
_badSectors.Add((entry.start_sector, entry.number_of_sectors));
}
}
/// <summary>Parses an EWF v2 session section.</summary>
void ParseSessionSectionV2(Stream segStream, long dataSize, List<(ulong startSector, uint flags)> sessionEntries)
{
if(dataSize < System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionHeaderV2>()) return;
var headerBytes = new byte[System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionHeaderV2>()];
segStream.EnsureRead(headerBytes, 0, headerBytes.Length);
EwfSessionHeaderV2 sessionHeader = Marshal.ByteArrayToStructureLittleEndian<EwfSessionHeaderV2>(headerBytes);
int entrySize = System.Runtime.InteropServices.Marshal.SizeOf<EwfSessionEntryV2>();
for(var i = 0; i < (int)sessionHeader.number_of_entries; i++)
{
var entryBytes = new byte[entrySize];
segStream.EnsureRead(entryBytes, 0, entrySize);
EwfSessionEntryV2 entry = Marshal.ByteArrayToStructureLittleEndian<EwfSessionEntryV2>(entryBytes);
sessionEntries.Add((entry.start_sector, entry.flags));
}
}
/// <summary>Builds AaruMetadata from parsed header values.</summary>
void BuildAaruMetadata()
{
// EWF header metadata doesn't map cleanly to AaruMetadata, but we can store some info
// in the image's Comments and Creator fields (already done above).
// For full metadata support, the AaruMetadata object would need forensic-specific fields.
// For now, we leave _metadata as null and expose data through ImageInfo fields.
}
}

188
Aaru.Images/EWF/Optical.cs Normal file
View File

@@ -0,0 +1,188 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Optical.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains IOpticalMediaImage implementation for Expert Witness Format.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Collections.Generic;
using System.Linq;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Track = Aaru.CommonTypes.Structs.Track;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region IOpticalMediaImage Members
/// <inheritdoc />
public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus)
{
buffer = null;
sectorStatus = SectorStatus.NotDumped;
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return ErrorNumber.NotSupported;
Track trk = _tracks?.FirstOrDefault(t => t.Sequence == track);
if(trk == null) return ErrorNumber.SectorNotFound;
return ReadSector(trk.StartSector + sectorAddress, false, out buffer, out sectorStatus);
}
/// <inheritdoc />
public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotSupported;
}
/// <inheritdoc />
public ErrorNumber ReadSectors(ulong sectorAddress, uint length, uint track, out byte[] buffer,
out SectorStatus[] sectorStatus)
{
buffer = null;
sectorStatus = null;
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return ErrorNumber.NotSupported;
Track trk = _tracks?.FirstOrDefault(t => t.Sequence == track);
if(trk == null) return ErrorNumber.SectorNotFound;
return ReadSectors(trk.StartSector + sectorAddress, false, length, out buffer, out sectorStatus);
}
/// <inheritdoc />
public ErrorNumber ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag,
out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotSupported;
}
/// <inheritdoc />
public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer,
out SectorStatus sectorStatus) =>
ReadSector(sectorAddress, track, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectorsLong(ulong sectorAddress, uint length, uint track, out byte[] buffer,
out SectorStatus[] sectorStatus) =>
ReadSectors(sectorAddress, length, track, out buffer, out sectorStatus);
/// <inheritdoc />
public List<Track> GetSessionTracks(Session session)
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null) return null;
return _tracks.Where(t => t.Session == session.Sequence).ToList();
}
/// <inheritdoc />
public List<Track> GetSessionTracks(ushort session)
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null) return null;
return _tracks.Where(t => t.Session == session).ToList();
}
/// <inheritdoc />
public bool? VerifySector(ulong sectorAddress) => null;
/// <inheritdoc />
public bool? VerifySectors(ulong sectorAddress, uint length, out List<ulong> failingLbas,
out List<ulong> unknownLbas)
{
failingLbas = [];
unknownLbas = [];
if(_badSectors is not { Count: > 0 })
{
for(ulong i = sectorAddress; i < sectorAddress + length; i++) unknownLbas.Add(i);
return null;
}
for(ulong i = sectorAddress; i < sectorAddress + length; i++)
{
var isBad = false;
foreach((ulong start, uint count) in _badSectors)
{
if(i < start || i >= start + count) continue;
isBad = true;
break;
}
if(isBad)
failingLbas.Add(i);
else
unknownLbas.Add(i);
}
if(unknownLbas.Count > 0) return null;
return failingLbas.Count <= 0;
}
/// <inheritdoc />
public bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List<ulong> failingLbas,
out List<ulong> unknownLbas)
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc || _tracks == null)
{
failingLbas = [];
unknownLbas = [];
for(ulong i = sectorAddress; i < sectorAddress + length; i++) unknownLbas.Add(i);
return null;
}
Track trk = _tracks.FirstOrDefault(t => t.Sequence == track);
if(trk == null)
{
failingLbas = [];
unknownLbas = [];
return null;
}
return VerifySectors(trk.StartSector + sectorAddress, length, out failingLbas, out unknownLbas);
}
#endregion
}

View File

@@ -0,0 +1,152 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Properties.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains properties for Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Partition = Aaru.CommonTypes.Partition;
using Track = Aaru.CommonTypes.Structs.Track;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region IOpticalMediaImage Members
/// <inheritdoc />
// ReSharper disable once ConvertToAutoProperty
public ImageInfo Info => _imageInfo;
/// <inheritdoc />
public string Name => Localization.Ewf_Name;
/// <inheritdoc />
public Guid Id => new("E5F3B186-2382-4BDE-89B2-A6FC2B464C6F");
/// <inheritdoc />
public string Author => Authors.NataliaPortillo;
/// <inheritdoc />
public string Format => _isSmart
? "Expert Witness Format (SMART)"
: _isV2
? "Expert Witness Format v2"
: "Expert Witness Format";
/// <inheritdoc />
public List<DumpHardware> DumpHardware => null;
/// <inheritdoc />
public Metadata AaruMetadata => _metadata;
/// <inheritdoc />
public IEnumerable<MediaTagType> SupportedMediaTags => [];
/// <inheritdoc />
public IEnumerable<SectorTagType> SupportedSectorTags => [];
/// <inheritdoc />
public IEnumerable<MediaType> SupportedMediaTypes =>
[
MediaType.Unknown, MediaType.GENERIC_HDD, MediaType.FlashDrive, MediaType.CompactFlash,
MediaType.CompactFlashType2, MediaType.PCCardTypeI, MediaType.PCCardTypeII, MediaType.PCCardTypeIII,
MediaType.PCCardTypeIV, MediaType.CD, MediaType.CDDA, MediaType.CDG, MediaType.CDEG, MediaType.CDI,
MediaType.CDROM, MediaType.CDROMXA, MediaType.CDPLUS, MediaType.CDR, MediaType.CDRW, MediaType.CDMRW,
MediaType.DVDROM, MediaType.DVDR, MediaType.DVDRW, MediaType.DVDPR, MediaType.DVDPRW, MediaType.DVDRDL,
MediaType.DVDPRWDL, MediaType.DVDPRDL, MediaType.DVDRAM, MediaType.DVDRWDL, MediaType.BDROM, MediaType.BDR,
MediaType.BDRE, MediaType.BDRXL, MediaType.BDREXL
];
/// <inheritdoc />
public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => [];
/// <inheritdoc />
public IEnumerable<string> KnownExtensions => [".E01", ".Ex01", ".s01"];
/// <inheritdoc />
public bool IsWriting => false;
/// <inheritdoc />
public string ErrorMessage => null;
/// <inheritdoc />
public List<Track> Tracks
{
get
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null;
return _tracks;
}
}
/// <inheritdoc />
public List<Session> Sessions
{
get
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null;
return _sessions;
}
}
/// <inheritdoc />
public List<Partition> Partitions
{
get
{
if(_imageInfo.MetadataMediaType != MetadataMediaType.OpticalDisc) return null;
List<Partition> parts =
[
new()
{
Start = 0,
Length = _imageInfo.Sectors,
Offset = 0,
Sequence = 0,
Type = "MODE1/2048",
Size = _imageInfo.Sectors * _imageInfo.SectorSize
}
];
return parts;
}
}
#endregion
}

181
Aaru.Images/EWF/Read.cs Normal file
View File

@@ -0,0 +1,181 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Read.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Reads Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.IO;
using Aaru.CommonTypes.Enums;
using Aaru.Compression;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Images;
public sealed partial class Ewf
{
/// <summary>Reads and decompresses a chunk, using cache when possible.</summary>
ErrorNumber ReadChunk(ulong chunkIndex, out byte[] chunkData)
{
chunkData = null;
// Check cache
if(_chunkCache.TryGetValue(chunkIndex, out chunkData)) return ErrorNumber.NoError;
if(!_chunkTable.TryGetValue(chunkIndex, out (int segmentIndex, long offset, uint size, bool compressed) chunk))
{
AaruLogging.Error(MODULE_NAME, "Chunk {0} not found in chunk table", chunkIndex);
return ErrorNumber.SectorNotFound;
}
Stream segStream = _segmentStreams[chunk.segmentIndex];
segStream.Seek(chunk.offset, SeekOrigin.Begin);
if(chunk.compressed)
{
var compressedData = new byte[chunk.size];
segStream.EnsureRead(compressedData, 0, (int)chunk.size);
if(_isV2 && _compressionMethod == EwfCompressionMethod.Bzip2)
{
// BZip2 decompression
chunkData = new byte[_chunkSize];
BZip2.DecodeBuffer(compressedData, chunkData);
}
else
{
// Zlib/Deflate decompression (default for v1, and deflate method for v2)
chunkData = DecompressZlib(compressedData, (int)_chunkSize);
}
}
else
{
// Uncompressed chunk: data + 4-byte Adler-32 checksum
uint dataLen = chunk.size >= 4 ? chunk.size - 4 : chunk.size;
chunkData = new byte[dataLen];
segStream.EnsureRead(chunkData, 0, (int)dataLen);
// Skip checksum (we trust the data for now)
if(chunk.size >= 4) segStream.Seek(4, SeekOrigin.Current);
}
// Cache chunk
if(_chunkCache.Count >= _maxChunkCache) _chunkCache.Clear();
_chunkCache[chunkIndex] = chunkData;
return ErrorNumber.NoError;
}
/// <summary>Determines the sector status by checking against the bad sectors list.</summary>
SectorStatus GetSectorStatus(ulong sectorAddress)
{
foreach((ulong start, uint count) in _badSectors)
{
if(sectorAddress >= start && sectorAddress < start + count) return SectorStatus.Errored;
}
return SectorStatus.Dumped;
}
#region IOpticalMediaImage Members
/// <inheritdoc />
public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
{
buffer = null;
sectorStatus = SectorStatus.NotDumped;
if(sectorAddress >= _imageInfo.Sectors) return ErrorNumber.OutOfRange;
// Check sector cache
if(_sectorCache.TryGetValue(sectorAddress, out buffer))
{
sectorStatus = GetSectorStatus(sectorAddress);
return ErrorNumber.NoError;
}
// Calculate which chunk contains this sector
ulong chunkIndex = sectorAddress / _sectorsPerChunk;
// Read and decompress the chunk
ErrorNumber errno = ReadChunk(chunkIndex, out byte[] chunkData);
if(errno != ErrorNumber.NoError) return errno;
// Extract the requested sector from the chunk
ulong sectorInChunk = sectorAddress % _sectorsPerChunk;
var sectorOffset = (long)(sectorInChunk * _bytesPerSector);
// Handle last chunk which may be smaller
if(sectorOffset + _bytesPerSector > chunkData.Length) return ErrorNumber.OutOfRange;
buffer = new byte[_bytesPerSector];
Array.Copy(chunkData, sectorOffset, buffer, 0, _bytesPerSector);
sectorStatus = GetSectorStatus(sectorAddress);
// Cache sector
if(_sectorCache.Count >= MAX_CACHED_SECTORS) _sectorCache.Clear();
_sectorCache[sectorAddress] = buffer;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
out SectorStatus[] sectorStatus)
{
buffer = null;
sectorStatus = null;
if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange;
buffer = new byte[length * _bytesPerSector];
sectorStatus = new SectorStatus[length];
for(uint i = 0; i < length; i++)
{
ErrorNumber errno = ReadSector(sectorAddress + i, negative, out byte[] sector, out SectorStatus status);
if(errno != ErrorNumber.NoError) return errno;
Array.Copy(sector, 0, buffer, i * _bytesPerSector, _bytesPerSector);
sectorStatus[i] = status;
}
return ErrorNumber.NoError;
}
#endregion
}

418
Aaru.Images/EWF/Structs.cs Normal file
View File

@@ -0,0 +1,418 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Structs.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains structures for Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Runtime.InteropServices;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region Nested type: EwfFileHeaderV1
/// <summary>EWF v1 file header, 13 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfFileHeaderV1
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] signature;
public byte fields_start;
public ushort segment_number;
public ushort fields_end;
}
#endregion
#region Nested type: EwfFileHeaderV2
/// <summary>EWF v2 file header, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfFileHeaderV2
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] signature;
public byte major_version;
public byte minor_version;
public ushort compression_method;
public uint segment_number;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] set_identifier;
}
#endregion
#region Nested type: EwfSectionDescriptorV1
/// <summary>EWF v1 section descriptor, 76 bytes. Sections are forward-linked.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSectionDescriptorV1
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] type_string;
public ulong next_offset;
public ulong size;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)]
public byte[] padding;
public uint checksum;
}
#endregion
#region Nested type: EwfSectionDescriptorV2
/// <summary>EWF v2 section descriptor, 64 bytes. Sections are backward-linked.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSectionDescriptorV2
{
public uint type;
public uint data_flags;
public ulong previous_offset;
public ulong data_size;
public uint descriptor_size;
public uint padding_size;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] data_integrity_hash;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] reserved;
public uint checksum;
}
#endregion
#region Nested type: EwfVolumeSection
/// <summary>EWF v1 EnCase volume section data, 1024 bytes (followed by checksum)</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfVolumeSection
{
public byte media_type;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public byte[] unknown1;
public uint number_of_chunks;
public uint sectors_per_chunk;
public uint bytes_per_sector;
public ulong number_of_sectors;
public uint chs_cylinders;
public uint chs_heads;
public uint chs_sectors;
public byte media_flags;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public byte[] unknown2;
public uint palm_volume_start_sector;
public uint unknown3;
public uint smart_logs_start_sector;
public byte compression_level;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public byte[] unknown4;
public uint error_granularity;
public uint unknown5;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] set_identifier;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 963)]
public byte[] unknown6;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
public byte[] signature;
public uint checksum;
}
#endregion
#region Nested type: EwfVolumeSmartSection
/// <summary>EWF SMART/old EnCase volume section data, 94 bytes total</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfVolumeSmartSection
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] unknown1;
public uint number_of_chunks;
public uint sectors_per_chunk;
public uint bytes_per_sector;
public uint number_of_sectors;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] unknown2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 45)]
public byte[] unknown3;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
public byte[] signature;
public uint checksum;
}
#endregion
#region Nested type: EwfTableHeaderV1
/// <summary>EWF v1 table section header, 24 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfTableHeaderV1
{
public uint number_of_entries;
public uint padding1;
public ulong base_offset;
public uint padding2;
public uint checksum;
}
#endregion
#region Nested type: EwfTableHeaderV2
/// <summary>EWF v2 table section header, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfTableHeaderV2
{
public ulong first_chunk_number;
public uint number_of_entries;
public uint unknown1;
public uint checksum;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] padding;
}
#endregion
#region Nested type: EwfTableEntryV2
/// <summary>EWF v2 table entry, 16 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfTableEntryV2
{
public ulong chunk_data_offset;
public uint chunk_data_size;
public uint chunk_data_flags;
}
#endregion
#region Nested type: EwfHashSection
/// <summary>EWF v1 hash section, 36 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfHashSection
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] md5_hash;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] unknown1;
public uint checksum;
}
#endregion
#region Nested type: EwfDigestSection
/// <summary>EWF v1 digest section, 80 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfDigestSection
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] md5_hash;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] sha1_hash;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)]
public byte[] padding1;
public uint checksum;
}
#endregion
#region Nested type: EwfSessionHeaderV1
/// <summary>EWF v1 session section header, 36 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSessionHeaderV1
{
public uint number_of_entries;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
public byte[] unknown1;
public uint checksum;
}
#endregion
#region Nested type: EwfSessionEntryV1
/// <summary>EWF v1 session entry, 32 bytes. Note: flags first, then start_sector.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSessionEntryV1
{
public uint flags;
public uint start_sector;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)]
public byte[] unknown1;
}
#endregion
#region Nested type: EwfSessionHeaderV2
/// <summary>EWF v2 session section header, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSessionHeaderV2
{
public uint number_of_entries;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] unknown1;
public uint checksum;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] padding;
}
#endregion
#region Nested type: EwfSessionEntryV2
/// <summary>EWF v2 session entry, 32 bytes. Note: start_sector first, then flags (reversed from v1).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSessionEntryV2
{
public ulong start_sector;
public uint flags;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] unknown1;
}
#endregion
#region Nested type: EwfErrorHeaderV1
/// <summary>EWF v1 error2 section header, 520 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfErrorHeaderV1
{
public uint number_of_entries;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)]
public byte[] unknown1;
public uint checksum;
}
#endregion
#region Nested type: EwfErrorEntryV1
/// <summary>EWF v1 error entry, 8 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfErrorEntryV1
{
public uint start_sector;
public uint number_of_sectors;
}
#endregion
#region Nested type: EwfErrorHeaderV2
/// <summary>EWF v2 error section header, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfErrorHeaderV2
{
public uint number_of_entries;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] unknown1;
public uint checksum;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] padding;
}
#endregion
#region Nested type: EwfErrorEntryV2
/// <summary>EWF v2 error entry, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfErrorEntryV2
{
public ulong start_sector;
public uint number_of_sectors;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] unknown1;
}
#endregion
#region Nested type: EwfMd5HashV2
/// <summary>EWF v2 MD5 hash section, 32 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfMd5HashV2
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] md5_hash;
public uint checksum;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public byte[] padding;
}
#endregion
#region Nested type: EwfSha1HashV2
/// <summary>EWF v2 SHA1 hash section, 48 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct EwfSha1HashV2
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] sha1_hash;
public uint checksum;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)]
public byte[] padding;
}
#endregion
}

View File

@@ -0,0 +1,77 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Unsupported.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains features unsupported by Expert Witness Format disk images.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using Aaru.CommonTypes.Enums;
namespace Aaru.Images;
public sealed partial class Ewf
{
#region IOpticalMediaImage Members
/// <inheritdoc />
public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotSupported;
}
/// <inheritdoc />
public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag,
out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotSupported;
}
/// <inheritdoc />
public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotSupported;
}
/// <inheritdoc />
public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer,
out SectorStatus sectorStatus) =>
ReadSector(sectorAddress, negative, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
out SectorStatus[] sectorStatus) =>
ReadSectors(sectorAddress, negative, length, out buffer, out sectorStatus);
#endregion
}

View File

@@ -5967,5 +5967,17 @@ namespace Aaru.Images {
return ResourceManager.GetString("Redumper_disc_image", resourceCulture);
}
}
internal static string Ewf_Name {
get {
return ResourceManager.GetString("Ewf_Name", resourceCulture);
}
}
internal static string Ewf_Identify_Signature_0 {
get {
return ResourceManager.GetString("Ewf_Identify_Signature_0", resourceCulture);
}
}
}
}

View File

@@ -2976,4 +2976,10 @@
<data name="Redumper_disc_image" xml:space="preserve">
<value>Imagen de disco DVD crudo de Redumper</value>
</data>
<data name="Ewf_Name" xml:space="preserve">
<value>Formato Expert Witness (EWF)</value>
</data>
<data name="Ewf_Identify_Signature_0" xml:space="preserve">
<value>Firma: {0}</value>
</data>
</root>

View File

@@ -2995,4 +2995,10 @@
<data name="Redumper_disc_image" xml:space="preserve">
<value>Redumper raw DVD disc image</value>
</data>
<data name="Ewf_Name" xml:space="preserve">
<value>Expert Witness Format (EWF)</value>
</data>
<data name="Ewf_Identify_Signature_0" xml:space="preserve">
<value>Signature: {0}</value>
</data>
</root>

View File

@@ -98,6 +98,7 @@ Media image formats we can read
* DiscJuggler images
* Dreamcast GDI
* Easy CD Creator images (.CIF)
* Expert Witness Format (EWF)
* HD-Copy disk images
* HxCStream
* MAME Compressed Hunks of Data (CHD)