diff --git a/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs b/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs
index 5367a104..8047930e 100644
--- a/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs
+++ b/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
using SabreTools.Data.Models.ISO9660;
using SabreTools.Numerics;
using Xunit;
@@ -9,6 +10,11 @@ namespace SabreTools.Serialization.Readers.Test
{
public class ISO9660Tests
{
+ ///
+ /// File flags of a plain file record
+ ///
+ private const FileFlags NoFlags = 0;
+
[Fact]
public void NullArray_Null()
{
@@ -98,8 +104,8 @@ public void ParseDirectory_ConsistentExtentLocation_ReadsEachDirectoryOnce()
{
// Sector 1 holds directory A at sector 2, which holds directory B at sector 3
byte[] image = new byte[5 * 2048];
- WriteDirectory(image, 1, "A", 2);
- WriteDirectory(image, 2, "B", 3);
+ WriteDirectory(image, 1, ("A", 2, FileFlags.DIRECTORY));
+ WriteDirectory(image, 2, ("B", 3, FileFlags.DIRECTORY));
WriteDirectory(image, 3);
var dr = new DirectoryRecord
{
@@ -118,28 +124,131 @@ public void ParseDirectory_ConsistentExtentLocation_ReadsEachDirectoryOnce()
Assert.Equal(1, stream.SeeksTo(3 * 2048));
}
+ [Fact]
+ public void ParseDirectoryDescriptors_ReadsExtentsInVolumeOrder()
+ {
+ // Root at sector 20 holds A at sector 30 and B at sector 22, A holds C at sector 21
+ byte[] image = new byte[32 * 2048];
+ WriteDirectory(image, 20, ("A", 30, FileFlags.DIRECTORY), ("B", 22, FileFlags.DIRECTORY));
+ WriteDirectory(image, 30, ("C", 21, FileFlags.DIRECTORY));
+ WriteDirectory(image, 22);
+ WriteDirectory(image, 21);
+ var stream = new SeekCountingStream(image);
+
+ var actual = ISO9660.ParseDirectoryDescriptors(stream, 2048, [CreatePrimaryVolumeDescriptor(20)]);
+
+ Assert.NotNull(actual);
+ Assert.Equal(new[] { 20, 21, 22, 30 }, actual.Keys.OrderBy(k => k));
+ Assert.Equal(new long[] { 20 * 2048, 22 * 2048, 30 * 2048, 21 * 2048 }, stream.SeekOrder);
+ }
+
+ [Fact]
+ public void FindDirectoryRecords_NestedPath_ReadsOnlyDirectoriesOnPath()
+ {
+ // Root at sector 20 holds OTHER at sector 22 and SUB at sector 30, SUB holds FILE.TXT;1 at sector 25
+ byte[] image = new byte[32 * 2048];
+ WriteDirectory(image, 20, ("OTHER", 22, FileFlags.DIRECTORY), ("SUB", 30, FileFlags.DIRECTORY));
+ WriteDirectory(image, 22);
+ WriteDirectory(image, 30, ("FILE.TXT;1", 25, NoFlags));
+ var stream = new SeekCountingStream(image);
+
+ var actual = ISO9660.FindDirectoryRecords(stream, 2048, CreatePrimaryVolumeDescriptor(20), "sub/file.txt");
+
+ Assert.NotNull(actual);
+ Assert.Equal(25, Assert.Single(actual).ExtentLocation.LittleEndian);
+ Assert.Equal(new long[] { 20 * 2048, 30 * 2048 }, stream.SeekOrder);
+ }
+
+ [Fact]
+ public void FindDirectoryRecords_MissingPath_Null()
+ {
+ byte[] image = new byte[32 * 2048];
+ WriteDirectory(image, 20, ("SUB", 30, FileFlags.DIRECTORY));
+ WriteDirectory(image, 30);
+
+ var actual = ISO9660.FindDirectoryRecords(new MemoryStream(image), 2048, CreatePrimaryVolumeDescriptor(20), "SUB/MISSING.TXT");
+
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void FindDirectoryRecords_MultiExtentFile_ReturnsAllRecords()
+ {
+ // BIG.BIN;1 is recorded in two extents, at sectors 25 and 26
+ byte[] image = new byte[32 * 2048];
+ WriteDirectory(image, 20, ("BIG.BIN;1", 25, FileFlags.MULTI_EXTENT), ("BIG.BIN;1", 26, NoFlags), ("NEXT.BIN;1", 27, NoFlags));
+
+ var actual = ISO9660.FindDirectoryRecords(new MemoryStream(image), 2048, CreatePrimaryVolumeDescriptor(20), "BIG.BIN");
+
+ Assert.NotNull(actual);
+ Assert.Equal(new[] { 25, 26 }, actual.Select(r => r.ExtentLocation.LittleEndian));
+ }
+
+ [Fact]
+ public void FindDirectoryRecords_SupplementaryVolumeDescriptor_UsesUnicodeNames()
+ {
+ byte[] image = new byte[32 * 2048];
+ WriteDirectory(image, 20, Encoding.BigEndianUnicode, ("Sub", 30, FileFlags.DIRECTORY));
+ WriteDirectory(image, 30, Encoding.BigEndianUnicode, ("File.txt;1", 25, NoFlags));
+ var svd = new SupplementaryVolumeDescriptor
+ {
+ LogicalBlockSize = 2048,
+ RootDirectoryRecord = CreatePrimaryVolumeDescriptor(20).RootDirectoryRecord,
+ };
+
+ var actual = ISO9660.FindDirectoryRecords(new MemoryStream(image), 2048, svd, "Sub/File.txt");
+
+ Assert.NotNull(actual);
+ Assert.Equal(25, Assert.Single(actual).ExtentLocation.LittleEndian);
+ }
+
///
- /// Write a one-sector directory extent with self and parent records and an optional child directory
+ /// Create a primary volume descriptor with 2048-byte blocks and a one-sector root directory
///
- private static void WriteDirectory(byte[] image, int sector, string? childName = null, int childSector = 0)
+ private static PrimaryVolumeDescriptor CreatePrimaryVolumeDescriptor(int rootSector)
+ {
+ return new PrimaryVolumeDescriptor
+ {
+ LogicalBlockSize = 2048,
+ RootDirectoryRecord = new DirectoryRecord
+ {
+ ExtentLocation = rootSector,
+ ExtentLength = 2048,
+ FileFlags = FileFlags.DIRECTORY,
+ },
+ };
+ }
+
+ ///
+ /// Write a one-sector directory extent with self and parent records and the given entries
+ ///
+ private static void WriteDirectory(byte[] image, int sector, params (string Name, int Location, FileFlags Flags)[] entries)
+ => WriteDirectory(image, sector, Encoding.ASCII, entries);
+
+ ///
+ /// Write a one-sector directory extent with self and parent records and the given entries
+ ///
+ private static void WriteDirectory(byte[] image, int sector, Encoding encoding, params (string Name, int Location, FileFlags Flags)[] entries)
{
int offset = sector * 2048;
- offset = WriteRecord(image, offset, Constants.CurrentDirectory, sector);
- offset = WriteRecord(image, offset, Constants.ParentDirectory, sector);
- if (childName is not null)
- WriteRecord(image, offset, System.Text.Encoding.ASCII.GetBytes(childName), childSector);
+ offset = WriteRecord(image, offset, Constants.CurrentDirectory, sector, FileFlags.DIRECTORY);
+ offset = WriteRecord(image, offset, Constants.ParentDirectory, sector, FileFlags.DIRECTORY);
+ foreach (var (name, location, flags) in entries)
+ {
+ offset = WriteRecord(image, offset, encoding.GetBytes(name), location, flags);
+ }
}
///
- /// Write a directory record for a one-sector directory extent, returns the offset after the record
+ /// Write a directory record for a one-sector extent, returns the offset after the record
///
- private static int WriteRecord(byte[] image, int offset, byte[] identifier, int location)
+ private static int WriteRecord(byte[] image, int offset, byte[] identifier, int location, FileFlags flags)
{
int length = 33 + identifier.Length + (identifier.Length % 2 == 0 ? 1 : 0);
image[offset] = (byte)length;
WriteBothEndian(image, offset + 2, location);
WriteBothEndian(image, offset + 10, 2048);
- image[offset + 25] = (byte)FileFlags.DIRECTORY;
+ image[offset + 25] = (byte)flags;
image[offset + 28] = 1;
image[offset + 31] = 1;
image[offset + 32] = (byte)identifier.Length;
@@ -160,12 +269,17 @@ private static void WriteBothEndian(byte[] image, int offset, int value)
}
///
- /// Memory stream that counts absolute seeks per offset
+ /// Memory stream that records absolute seeks
///
private sealed class SeekCountingStream : MemoryStream
{
private readonly Dictionary _seeks = [];
+ ///
+ /// Offsets of all absolute seeks, in order
+ ///
+ public List SeekOrder { get; } = [];
+
public SeekCountingStream(byte[] buffer) : base(buffer) { }
public int SeeksTo(long offset)
@@ -174,7 +288,10 @@ public int SeeksTo(long offset)
public override long Seek(long offset, SeekOrigin loc)
{
if (loc == SeekOrigin.Begin)
+ {
_seeks[offset] = SeeksTo(offset) + 1;
+ SeekOrder.Add(offset);
+ }
return base.Seek(offset, loc);
}
diff --git a/SabreTools.Serialization.Readers/ISO9660.cs b/SabreTools.Serialization.Readers/ISO9660.cs
index 7ddd2358..083ff9a3 100644
--- a/SabreTools.Serialization.Readers/ISO9660.cs
+++ b/SabreTools.Serialization.Readers/ISO9660.cs
@@ -1,5 +1,7 @@
+using System;
using System.Collections.Generic;
using System.IO;
+using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.ISO9660;
using SabreTools.IO.Extensions;
@@ -589,6 +591,10 @@ public static List ParsePathTableGroup(Stream data, short sector
///
/// Parse a Stream into a map of sector numbers to Directory
///
+ ///
+ /// The directory trees of all volume descriptors are read together, in the order of their extents on the volume:
+ /// always the next known extent at or after the last one read, wrapping around to the lowest one at the end
+ ///
/// Stream to parse
/// Number of bytes in a logical sector (usually 2048)
/// Set of volume descriptors for a volume
@@ -596,6 +602,8 @@ public static List ParsePathTableGroup(Stream data, short sector
public static Dictionary? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet)
{
var directories = new Dictionary();
+ var parsed = new Dictionary();
+ var pending = new List();
foreach (VolumeDescriptor vd in vdSet)
{
DirectoryRecord rootDirectoryRecord;
@@ -609,17 +617,25 @@ public static List ParsePathTableGroup(Stream data, short sector
// Determine logical block size
short blockLength = vd.GetLogicalBlockSize(sectorLength);
- // Parse the root directory pointed to from the base volume descriptor
- var descriptors = ParseDirectory(data, sectorLength, blockLength, rootDirectoryRecord, false);
- if (descriptors is null || descriptors.Count == 0)
+ // Queue the root directory pointed to from the base volume descriptor
+ QueueExtent(data, blockLength, rootDirectoryRecord, pending);
+ }
+
+ // Read the queued extents in volume order
+ long position = 0;
+ while (pending.Count > 0)
+ {
+ int index = FindNextExtent(pending, position);
+ var extent = pending[index];
+ pending.RemoveAt(index);
+ position = extent.Offset + extent.Length;
+
+ // Each extent is read once
+ if (parsed.ContainsKey(extent.Offset))
continue;
- // Merge dictionaries
- foreach (var kvp in descriptors)
- {
- if (!directories.ContainsKey(kvp.Key))
- directories.Add(kvp.Key, kvp.Value);
- }
+ parsed[extent.Offset] = true;
+ ParseExtent(data, sectorLength, extent, directories, pending);
}
// Return error (null) if no valid directory descriptors were found
@@ -629,6 +645,333 @@ public static List ParsePathTableGroup(Stream data, short sector
return directories;
}
+ ///
+ /// Queue the extent a directory record points to, if it needs to be read
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical block (usually 2048)
+ /// Directory record pointing to the extent
+ /// Queued extents, sorted by offset
+ private static void QueueExtent(Stream data, short blockLength, DirectoryRecord dr, List pending)
+ {
+ // Only directories and files with an extended attribute record are read
+ if ((dr.FileFlags & FileFlags.DIRECTORY) != FileFlags.DIRECTORY && dr.ExtendedAttributeRecordLength == 0)
+ return;
+
+ QueueExtent(data, blockLength, dr, false, pending);
+
+ // If the extent location field is ambiguous, also queue the big-endian extent
+ if (!dr.ExtentLocation.IsValid)
+ QueueExtent(data, blockLength, dr, true, pending);
+ }
+
+ ///
+ /// Queue the extent a directory record points to, using one endianness
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical block (usually 2048)
+ /// Directory record pointing to the extent
+ /// True if the Big Endian extent location/length should be used
+ /// Queued extents, sorted by offset
+ private static void QueueExtent(Stream data, short blockLength, DirectoryRecord dr, bool bigEndian, List pending)
+ {
+ if (!TryGetExtent(data, blockLength, dr, bigEndian, out int extentLocation, out long extentOffset, out uint extentLength))
+ return;
+
+ var extent = new PendingExtent(extentLocation, extentOffset, extentLength, blockLength, dr);
+ int index = pending.BinarySearch(extent, PendingExtentComparer.Instance);
+ pending.Insert(index < 0 ? ~index : index, extent);
+ }
+
+ ///
+ /// Find the first queued extent at or after a position, or the first one if there is none
+ ///
+ /// Queued extents, sorted by offset
+ /// Offset just after the last extent read
+ /// Index of the next extent to read
+ private static int FindNextExtent(List pending, long position)
+ {
+ var probe = new PendingExtent(0, position, 0, 0, null);
+ int index = pending.BinarySearch(probe, PendingExtentComparer.Instance);
+ if (index < 0)
+ {
+ index = ~index;
+ }
+ else
+ {
+ while (index > 0 && pending[index - 1].Offset == position)
+ {
+ index--;
+ }
+ }
+
+ return index < pending.Count ? index : 0;
+ }
+
+ ///
+ /// Parse one queued extent and queue the extents its directory records point to
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical sector (usually 2048)
+ /// Extent to parse
+ /// Map of sector numbers to parsed extents
+ /// Queued extents, sorted by offset
+ private static void ParseExtent(Stream data, short sectorLength, PendingExtent extent, Dictionary directories, List pending)
+ {
+ var dr = extent.Record!;
+
+ // Move stream to extent location
+ data.SeekIfPossible(extent.Offset, SeekOrigin.Begin);
+
+ // Check if the current extent is a directory
+ if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY)
+ {
+ // Read all directory records in this directory
+ var records = ParseDirectoryRecords(data, sectorLength, extent.Length);
+ if (records is null)
+ return;
+
+ // Add current directory to dictionary
+ var currentDirectory = new DirectoryExtent();
+ currentDirectory.DirectoryRecords = [.. records];
+ if (!directories.ContainsKey(extent.Location))
+ directories.Add(extent.Location, currentDirectory);
+
+ // Queue all child extents
+ foreach (var record in records)
+ {
+ // Don't traverse to parent or self
+ if (record.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || record.FileIdentifier.EqualsExactly(Constants.ParentDirectory))
+ continue;
+
+ QueueExtent(data, extent.BlockLength, record, pending);
+ }
+ }
+ else if (dr.ExtendedAttributeRecordLength > 0)
+ {
+ // Extent is a file, parse the Extended Attribute Record
+ var fileExtent = new FileExtent();
+ var ear = ParseExtendedAttributeRecord(data);
+ if (ear is not null)
+ fileExtent.ExtendedAttributeRecord = ear;
+
+ // Put the file extent is the dictionary
+ if (!directories.ContainsKey(extent.Location))
+ directories.Add(extent.Location, fileExtent);
+ }
+ }
+
+ ///
+ /// Get the extent a directory record points to, using one endianness
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical block (usually 2048)
+ /// Directory record pointing to the extent
+ /// True if the Big Endian extent location/length should be used
+ /// Logical block number of the extent
+ /// Offset of the extent in the stream
+ /// Number of bytes in the extent
+ /// True if the extent is valid, false otherwise
+ private static bool TryGetExtent(Stream data, short blockLength, DirectoryRecord dr, bool bigEndian, out int extentLocation, out long extentOffset, out uint extentLength)
+ {
+ // Use provided extent endinanness
+ extentLocation = bigEndian ? dr.ExtentLocation.BigEndian : dr.ExtentLocation.LittleEndian;
+ extentLength = bigEndian ? dr.ExtentLength.BigEndian : dr.ExtentLength.LittleEndian;
+ extentOffset = extentLocation * (long)blockLength;
+ long extentFinal = extentOffset + extentLength;
+
+ // Deal with extent length ambiguity
+ if (!dr.ExtentLength.IsValid)
+ {
+ // If provided extent length is invalid, use the other value
+ if (extentLength <= 0 || extentFinal > data.Length)
+ extentLength = bigEndian ? dr.ExtentLength.LittleEndian : dr.ExtentLength.BigEndian;
+ }
+
+ // Validate extent length
+ return extentLength > 0 && extentFinal <= data.Length;
+ }
+
+ ///
+ /// Parse the directory records of one directory extent
+ ///
+ /// Stream to parse, positioned at the start of the directory extent
+ /// Number of bytes in a logical sector (usually 2048)
+ /// Number of bytes in the directory extent
+ /// Filled list of DirectoryRecord on success, null if the directory is invalid
+ private static List? ParseDirectoryRecords(Stream data, short sectorLength, uint extentLength)
+ {
+ // Start of directory should not be 0
+ int firstRecordLength = data.PeekByteValue();
+ if (firstRecordLength == 0)
+ return null;
+
+ // Read all directory records in this directory
+ var records = new List();
+ uint pos = 0;
+ while (pos < extentLength)
+ {
+ // Peek next byte to check whether the next record length is not greater than the end of the dir extent
+ var recordLength = data.PeekByteValue();
+
+ // If record length of 0x00, next record begins in next sector
+ if (recordLength == 0)
+ {
+ int paddingLength = (int)((uint)sectorLength - (pos % (uint)sectorLength));
+ pos += (uint)paddingLength;
+ _ = data.ReadBytes(paddingLength);
+
+ // Finish parsing records if end reached
+ if (pos >= extentLength)
+ break;
+
+ // Start of sector should not be 0, ignore entire directory
+ int nextRecordLength = data.PeekByteValue();
+ if (nextRecordLength <= 0)
+ return null;
+
+ continue;
+ }
+
+ // Ensure record will end in this extent
+ // TODO: Smartly detect record length for invalid record lengths
+ pos += recordLength;
+ if (pos > extentLength)
+ break;
+
+ // Get the next directory record
+ var directoryRecord = ParseDirectoryRecord(data, false);
+
+ // Compare recordLength with number of bytes in directoryRecord and ignore the directory if mismatch
+ var readLength = 33 + directoryRecord.FileIdentifier.Length + (directoryRecord.PaddingField is null ? 0 : 1) + directoryRecord.SystemUse.Length;
+ if (readLength != recordLength)
+ return null;
+
+ records.Add(directoryRecord);
+ }
+
+ return records;
+ }
+
+ ///
+ /// Find the directory records of one path, reading only the directories along the path
+ ///
+ ///
+ /// Names are compared case-insensitively and without a version suffix (";1") or a trailing period.
+ /// A file recorded in several extents returns one directory record per extent, in recorded order.
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical sector (usually 2048)
+ /// Primary or supplementary volume descriptor whose directory hierarchy is searched
+ /// Path from the root directory, separated by '/' or '\'
+ /// Filled array of DirectoryRecord on success, null if the path was not found
+ public static DirectoryRecord[]? FindDirectoryRecords(Stream data, short sectorLength, VolumeDescriptor vd, string path)
+ {
+ DirectoryRecord current;
+ if (vd is PrimaryVolumeDescriptor pvd)
+ current = pvd.RootDirectoryRecord;
+ else if (vd is SupplementaryVolumeDescriptor svd)
+ current = svd.RootDirectoryRecord;
+ else
+ return null;
+
+ // Determine logical block size
+ short blockLength = vd.GetLogicalBlockSize(sectorLength);
+
+ // TODO: Better encoding detection (EscapeSequences)
+ var encoding = vd is SupplementaryVolumeDescriptor ? Encoding.BigEndianUnicode : Encoding.UTF8;
+
+ string[] names = path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
+ if (names.Length == 0)
+ return [current];
+
+ for (int i = 0; i < names.Length; i++)
+ {
+ // Only a directory can contain the next name
+ if ((current.FileFlags & FileFlags.DIRECTORY) != FileFlags.DIRECTORY)
+ return null;
+
+ // If the extent location field is ambiguous, also search the big-endian directory extent
+ var found = FindInDirectory(data, sectorLength, blockLength, current, false, encoding, names[i]);
+ if (found is null && !current.ExtentLocation.IsValid)
+ found = FindInDirectory(data, sectorLength, blockLength, current, true, encoding, names[i]);
+
+ if (found is null)
+ return null;
+
+ if (i == names.Length - 1)
+ return found;
+
+ current = found[0];
+ }
+
+ return null;
+ }
+
+ ///
+ /// Find the directory records of one name in one directory
+ ///
+ /// Stream to parse
+ /// Number of bytes in a logical sector (usually 2048)
+ /// Number of bytes in a logical block (usually 2048)
+ /// Directory record pointing to the directory extent
+ /// True if the Big Endian extent location/length should be used
+ /// Encoding of the file identifiers
+ /// Name to find
+ /// Filled array of DirectoryRecord on success, null if the name was not found
+ private static DirectoryRecord[]? FindInDirectory(Stream data, short sectorLength, short blockLength, DirectoryRecord dr, bool bigEndian, Encoding encoding, string name)
+ {
+ if (!TryGetExtent(data, blockLength, dr, bigEndian, out _, out long extentOffset, out uint extentLength))
+ return null;
+
+ // Move stream to directory location
+ data.SeekIfPossible(extentOffset, SeekOrigin.Begin);
+
+ var records = ParseDirectoryRecords(data, sectorLength, extentLength);
+ if (records is null)
+ return null;
+
+ string wanted = TrimIdentifier(name);
+ for (int i = 0; i < records.Count; i++)
+ {
+ // Don't match parent or self
+ if (records[i].FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || records[i].FileIdentifier.EqualsExactly(Constants.ParentDirectory))
+ continue;
+
+ if (!string.Equals(TrimIdentifier(encoding.GetString(records[i].FileIdentifier)), wanted, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ // A multi-extent file continues in the following records, up to the final one
+ var matches = new List { records[i] };
+ while ((records[i].FileFlags & FileFlags.MULTI_EXTENT) == FileFlags.MULTI_EXTENT && i + 1 < records.Count)
+ {
+ i++;
+ matches.Add(records[i]);
+ }
+
+ return [.. matches];
+ }
+
+ return null;
+ }
+
+ ///
+ /// Remove the version suffix and a trailing period from a file identifier
+ ///
+ /// File identifier as recorded or requested
+ /// File identifier without version suffix and trailing period
+ private static string TrimIdentifier(string identifier)
+ {
+ int versionSeparator = identifier.LastIndexOf(';');
+ if (versionSeparator >= 0)
+ identifier = identifier.Substring(0, versionSeparator);
+
+ if (identifier.EndsWith("."))
+ identifier = identifier.Substring(0, identifier.Length - 1);
+
+ return identifier;
+ }
+
///
/// Parse a Stream into a map of sector numbers to Directory
///
@@ -671,54 +1014,10 @@ public static List ParsePathTableGroup(Stream data, short sector
if (dr.FileFlags.HasFlag(FileFlags.DIRECTORY))
#endif
{
- // Start of directory should not be 0
- int firstRecordLength = data.PeekByteValue();
- if (firstRecordLength == 0)
- return null;
-
// Read all directory records in this directory
- var records = new List();
- uint pos = 0;
- while (pos < extentLength)
- {
- // Peek next byte to check whether the next record length is not greater than the end of the dir extent
- var recordLength = data.PeekByteValue();
-
- // If record length of 0x00, next record begins in next sector
- if (recordLength == 0)
- {
- int paddingLength = (int)((uint)sectorLength - (pos % (uint)sectorLength));
- pos += (uint)paddingLength;
- _ = data.ReadBytes(paddingLength);
-
- // Finish parsing records if end reached
- if (pos >= extentLength)
- break;
-
- // Start of sector should not be 0, ignore entire directory
- int nextRecordLength = data.PeekByteValue();
- if (nextRecordLength <= 0)
- return null;
-
- continue;
- }
-
- // Ensure record will end in this extent
- // TODO: Smartly detect record length for invalid record lengths
- pos += recordLength;
- if (pos > extentLength)
- break;
-
- // Get the next directory record
- var directoryRecord = ParseDirectoryRecord(data, false);
-
- // Compare recordLength with number of bytes in directoryRecord and return null if mismatch
- var readLength = 33 + directoryRecord.FileIdentifier.Length + (directoryRecord.PaddingField is null ? 0 : 1) + directoryRecord.SystemUse.Length;
- if (readLength != recordLength)
- return null;
-
- records.Add(directoryRecord);
- }
+ var records = ParseDirectoryRecords(data, sectorLength, extentLength);
+ if (records is null)
+ return null;
// Add current directory to dictionary
var currentDirectory = new DirectoryExtent();
@@ -936,5 +1235,60 @@ public static DecDateTime ParseDecDateTime(Stream data)
return obj;
}
+
+ ///
+ /// Extent queued for reading
+ ///
+ private sealed class PendingExtent
+ {
+ ///
+ /// Logical block number of the extent
+ ///
+ public int Location { get; }
+
+ ///
+ /// Byte offset of the extent
+ ///
+ public long Offset { get; }
+
+ ///
+ /// Length of the extent in bytes
+ ///
+ public uint Length { get; }
+
+ ///
+ /// Number of bytes in a logical block of the volume
+ ///
+ public short BlockLength { get; }
+
+ ///
+ /// Directory record pointing to the extent
+ ///
+ public DirectoryRecord? Record { get; }
+
+ public PendingExtent(int location, long offset, uint length, short blockLength, DirectoryRecord? record)
+ {
+ Location = location;
+ Offset = offset;
+ Length = length;
+ BlockLength = blockLength;
+ Record = record;
+ }
+ }
+
+ ///
+ /// Orders queued extents by offset
+ ///
+ private sealed class PendingExtentComparer : IComparer
+ {
+ ///
+ /// Shared instance
+ ///
+ public static readonly PendingExtentComparer Instance = new();
+
+ ///
+ public int Compare(PendingExtent? x, PendingExtent? y)
+ => (x?.Offset ?? 0).CompareTo(y?.Offset ?? 0);
+ }
}
}