using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using SabreTools.Data.Models.ISO9660; using SabreTools.Numerics; using Xunit; 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() { byte[]? data = null; int offset = 0; var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data, offset); Assert.Null(actual); } [Fact] public void EmptyArray_Null() { byte[]? data = []; int offset = 0; var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data, offset); Assert.Null(actual); } [Fact] public void InvalidArray_Null() { byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; int offset = 0; var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data, offset); Assert.Null(actual); } [Fact] public void NullStream_Null() { Stream? data = null; var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data); Assert.Null(actual); } [Fact] public void EmptyStream_Null() { Stream? data = new MemoryStream([]); var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data); Assert.Null(actual); } [Fact] public void InvalidStream_Null() { Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); var deserializer = new ISO9660(); var actual = deserializer.Deserialize(data); Assert.Null(actual); } [Fact] public void ParseDirectory_AmbiguousExtentLocation_ParsesBothExtents() { // Little-endian location points to sector 1, big-endian location to sector 2 byte[] image = new byte[4 * 2048]; WriteDirectory(image, 1); WriteDirectory(image, 2); var dr = new DirectoryRecord { ExtentLocation = new BothInt32(1, 2), ExtentLength = 2048, FileFlags = FileFlags.DIRECTORY, }; var actual = ISO9660.ParseDirectory(new MemoryStream(image), 2048, 2048, dr, false); Assert.NotNull(actual); Assert.True(actual.ContainsKey(1)); Assert.True(actual.ContainsKey(2)); } [Fact] 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, FileFlags.DIRECTORY)); WriteDirectory(image, 2, ("B", 3, FileFlags.DIRECTORY)); WriteDirectory(image, 3); var dr = new DirectoryRecord { ExtentLocation = 1, ExtentLength = 2048, FileFlags = FileFlags.DIRECTORY, }; var stream = new SeekCountingStream(image); var actual = ISO9660.ParseDirectory(stream, 2048, 2048, dr, false); Assert.NotNull(actual); Assert.Equal(3, actual.Count); Assert.Equal(1, stream.SeeksTo(1 * 2048)); Assert.Equal(1, stream.SeeksTo(2 * 2048)); 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); } /// /// Create a primary volume descriptor with 2048-byte blocks and a one-sector root directory /// 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, 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 extent, returns the offset after the record /// 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)flags; image[offset + 28] = 1; image[offset + 31] = 1; image[offset + 32] = (byte)identifier.Length; identifier.CopyTo(image, offset + 33); return offset + length; } /// /// Write a 32-bit value in both-endian format /// private static void WriteBothEndian(byte[] image, int offset, int value) { for (int i = 0; i < 4; i++) { image[offset + i] = (byte)(value >> (8 * i)); image[offset + 7 - i] = (byte)(value >> (8 * i)); } } /// /// 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) => _seeks.TryGetValue(offset, out int count) ? count : 0; 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); } } } }