From 756b7c68dabffc00592deb9814328e2e21394c13 Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 15 Oct 2026 02:39:42 +0200 Subject: [PATCH 1/2] Read directory extents in volume order ParseDirectoryDescriptors now keeps one worklist of directory extents for all volume descriptors, sorted by offset, and always reads the next extent at or after the last one, wrapping around at the end. Each extent is read once. The record loop moves to a shared helper. Co-Authored-By: Claude Opus 5.5 --- .../ISO9660Tests.cs | 76 +++- SabreTools.Serialization.Readers/ISO9660.cs | 345 +++++++++++++++--- 2 files changed, 353 insertions(+), 68 deletions(-) diff --git a/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs b/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs index 5367a104..ef7fe2d4 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; @@ -98,8 +99,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 +119,71 @@ 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); + } + /// - /// 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 +204,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 +223,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..4d772161 100644 --- a/SabreTools.Serialization.Readers/ISO9660.cs +++ b/SabreTools.Serialization.Readers/ISO9660.cs @@ -589,6 +589,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 +600,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 +615,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 +643,214 @@ 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; + } + /// /// Parse a Stream into a map of sector numbers to Directory /// @@ -671,54 +893,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 +1114,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); + } } } From fd88b76c9cf9ea6e480d0d3e46ae33a9ab64ccb8 Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 15 Oct 2026 02:42:39 +0200 Subject: [PATCH 2/2] Add a path lookup for ISO 9660 directory records FindDirectoryRecords follows a path from the root directory of one volume descriptor and reads only the directories on that path. Names are compared case-insensitively, without the version suffix or a trailing period. A file recorded in several extents returns all of its records. Co-Authored-By: Claude Opus 5.5 --- .../ISO9660Tests.cs | 65 ++++++++++ SabreTools.Serialization.Readers/ISO9660.cs | 121 ++++++++++++++++++ 2 files changed, 186 insertions(+) diff --git a/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs b/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs index ef7fe2d4..8047930e 100644 --- a/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs +++ b/SabreTools.Serialization.Readers.Test/ISO9660Tests.cs @@ -10,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() { @@ -137,6 +142,66 @@ public void ParseDirectoryDescriptors_ReadsExtentsInVolumeOrder() 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); + } + /// /// Create a primary volume descriptor with 2048-byte blocks and a one-sector root directory /// diff --git a/SabreTools.Serialization.Readers/ISO9660.cs b/SabreTools.Serialization.Readers/ISO9660.cs index 4d772161..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; @@ -851,6 +853,125 @@ private static bool TryGetExtent(Stream data, short blockLength, 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 ///