mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-11-15 17:00:38 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user