Read ISO 9660 directories in volume order and add a path lookup (#99)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gmipf
2026-10-26 16:00:54 +01:00
committed by GitHub
parent f639f7ac24
commit 33caf06bc1
2 changed files with 542 additions and 71 deletions

View File

@@ -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
{
/// <summary>
/// File flags of a plain file record
/// </summary>
private const FileFlags NoFlags = 0;
[Fact]
public void NullArray_Null()
{
@@ -98,8 +104,8 @@ namespace SabreTools.Serialization.Readers.Test
{
// 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 @@ namespace SabreTools.Serialization.Readers.Test
Assert.Equal(1, stream.SeeksTo(3 * 2048));
}
/// <summary>
/// Write a one-sector directory extent with self and parent records and an optional child directory
/// </summary>
private static void WriteDirectory(byte[] image, int sector, string? childName = null, int childSector = 0)
[Fact]
public void ParseDirectoryDescriptors_ReadsExtentsInVolumeOrder()
{
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);
// 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);
}
/// <summary>
/// Write a directory record for a one-sector directory extent, returns the offset after the record
/// Create a primary volume descriptor with 2048-byte blocks and a one-sector root directory
/// </summary>
private static int WriteRecord(byte[] image, int offset, byte[] identifier, int location)
private static PrimaryVolumeDescriptor CreatePrimaryVolumeDescriptor(int rootSector)
{
return new PrimaryVolumeDescriptor
{
LogicalBlockSize = 2048,
RootDirectoryRecord = new DirectoryRecord
{
ExtentLocation = rootSector,
ExtentLength = 2048,
FileFlags = FileFlags.DIRECTORY,
},
};
}
/// <summary>
/// Write a one-sector directory extent with self and parent records and the given entries
/// </summary>
private static void WriteDirectory(byte[] image, int sector, params (string Name, int Location, FileFlags Flags)[] entries)
=> WriteDirectory(image, sector, Encoding.ASCII, entries);
/// <summary>
/// Write a one-sector directory extent with self and parent records and the given entries
/// </summary>
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, 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);
}
}
/// <summary>
/// Write a directory record for a one-sector extent, returns the offset after the record
/// </summary>
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 @@ namespace SabreTools.Serialization.Readers.Test
}
/// <summary>
/// Memory stream that counts absolute seeks per offset
/// Memory stream that records absolute seeks
/// </summary>
private sealed class SeekCountingStream : MemoryStream
{
private readonly Dictionary<long, int> _seeks = [];
/// <summary>
/// Offsets of all absolute seeks, in order
/// </summary>
public List<long> SeekOrder { get; } = [];
public SeekCountingStream(byte[] buffer) : base(buffer) { }
public int SeeksTo(long offset)
@@ -174,7 +288,10 @@ namespace SabreTools.Serialization.Readers.Test
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);
}

View File

@@ -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 @@ namespace SabreTools.Serialization.Readers
/// <summary>
/// Parse a Stream into a map of sector numbers to Directory
/// </summary>
/// <remarks>
/// 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
/// </remarks>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="vd">Set of volume descriptors for a volume</param>
@@ -596,6 +602,8 @@ namespace SabreTools.Serialization.Readers
public static Dictionary<int, FileExtent>? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet)
{
var directories = new Dictionary<int, FileExtent>();
var parsed = new Dictionary<long, bool>();
var pending = new List<PendingExtent>();
foreach (VolumeDescriptor vd in vdSet)
{
DirectoryRecord rootDirectoryRecord;
@@ -609,17 +617,25 @@ namespace SabreTools.Serialization.Readers
// 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 @@ namespace SabreTools.Serialization.Readers
return directories;
}
/// <summary>
/// Queue the extent a directory record points to, if it needs to be read
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="blockLength">Number of bytes in a logical block (usually 2048)</param>
/// <param name="dr">Directory record pointing to the extent</param>
/// <param name="pending">Queued extents, sorted by offset</param>
private static void QueueExtent(Stream data, short blockLength, DirectoryRecord dr, List<PendingExtent> 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);
}
/// <summary>
/// Queue the extent a directory record points to, using one endianness
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="blockLength">Number of bytes in a logical block (usually 2048)</param>
/// <param name="dr">Directory record pointing to the extent</param>
/// <param name="bigEndian">True if the Big Endian extent location/length should be used</param>
/// <param name="pending">Queued extents, sorted by offset</param>
private static void QueueExtent(Stream data, short blockLength, DirectoryRecord dr, bool bigEndian, List<PendingExtent> 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);
}
/// <summary>
/// Find the first queued extent at or after a position, or the first one if there is none
/// </summary>
/// <param name="pending">Queued extents, sorted by offset</param>
/// <param name="position">Offset just after the last extent read</param>
/// <returns>Index of the next extent to read</returns>
private static int FindNextExtent(List<PendingExtent> 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;
}
/// <summary>
/// Parse one queued extent and queue the extents its directory records point to
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="extent">Extent to parse</param>
/// <param name="directories">Map of sector numbers to parsed extents</param>
/// <param name="pending">Queued extents, sorted by offset</param>
private static void ParseExtent(Stream data, short sectorLength, PendingExtent extent, Dictionary<int, FileExtent> directories, List<PendingExtent> 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);
}
}
/// <summary>
/// Get the extent a directory record points to, using one endianness
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="blockLength">Number of bytes in a logical block (usually 2048)</param>
/// <param name="dr">Directory record pointing to the extent</param>
/// <param name="bigEndian">True if the Big Endian extent location/length should be used</param>
/// <param name="extentLocation">Logical block number of the extent</param>
/// <param name="extentOffset">Offset of the extent in the stream</param>
/// <param name="extentLength">Number of bytes in the extent</param>
/// <returns>True if the extent is valid, false otherwise</returns>
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;
}
/// <summary>
/// Parse the directory records of one directory extent
/// </summary>
/// <param name="data">Stream to parse, positioned at the start of the directory extent</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="extentLength">Number of bytes in the directory extent</param>
/// <returns>Filled list of DirectoryRecord on success, null if the directory is invalid</returns>
private static List<DirectoryRecord>? 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<DirectoryRecord>();
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;
}
/// <summary>
/// Find the directory records of one path, reading only the directories along the path
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="vd">Primary or supplementary volume descriptor whose directory hierarchy is searched</param>
/// <param name="path">Path from the root directory, separated by '/' or '\'</param>
/// <returns>Filled array of DirectoryRecord on success, null if the path was not found</returns>
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;
}
/// <summary>
/// Find the directory records of one name in one directory
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="blockLength">Number of bytes in a logical block (usually 2048)</param>
/// <param name="dr">Directory record pointing to the directory extent</param>
/// <param name="bigEndian">True if the Big Endian extent location/length should be used</param>
/// <param name="encoding">Encoding of the file identifiers</param>
/// <param name="name">Name to find</param>
/// <returns>Filled array of DirectoryRecord on success, null if the name was not found</returns>
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<DirectoryRecord> { 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;
}
/// <summary>
/// Remove the version suffix and a trailing period from a file identifier
/// </summary>
/// <param name="identifier">File identifier as recorded or requested</param>
/// <returns>File identifier without version suffix and trailing period</returns>
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;
}
/// <summary>
/// Parse a Stream into a map of sector numbers to Directory
/// </summary>
@@ -671,54 +1014,10 @@ namespace SabreTools.Serialization.Readers
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<DirectoryRecord>();
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 @@ namespace SabreTools.Serialization.Readers
return obj;
}
/// <summary>
/// Extent queued for reading
/// </summary>
private sealed class PendingExtent
{
/// <summary>
/// Logical block number of the extent
/// </summary>
public int Location { get; }
/// <summary>
/// Byte offset of the extent
/// </summary>
public long Offset { get; }
/// <summary>
/// Length of the extent in bytes
/// </summary>
public uint Length { get; }
/// <summary>
/// Number of bytes in a logical block of the volume
/// </summary>
public short BlockLength { get; }
/// <summary>
/// Directory record pointing to the extent
/// </summary>
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;
}
}
/// <summary>
/// Orders queued extents by offset
/// </summary>
private sealed class PendingExtentComparer : IComparer<PendingExtent>
{
/// <summary>
/// Shared instance
/// </summary>
public static readonly PendingExtentComparer Instance = new();
/// <inheritdoc/>
public int Compare(PendingExtent? x, PendingExtent? y)
=> (x?.Offset ?? 0).CompareTo(y?.Offset ?? 0);
}
}
}