Files

275 lines
9.2 KiB
C#
Raw Permalink Normal View History

Add a UDF reader and wrapper (#100) * Add a UDF reader Reads the Volume Recognition Sequence, the Anchor Volume Descriptor Pointer (sector 256, then the end of the volume), the Main or Reserve Volume Descriptor Sequence, the File Set Descriptor and the directory hierarchy. Type 1 and Metadata Partitions are mapped, Virtual and Sparable partitions are not. File Entries, Allocation Extent Descriptors and directories are read in ascending sector order with wrap-around, adjacent sectors merged into one request. FindFileEntry follows one path and reads only the directories on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a UDF wrapper Prints the volume and file set structures and the directory hierarchy, and extracts the files. CreateDiscImageWrapper tries UDF after ISO 9660 for a stream at offset 0, so discs with both file systems still open as ISO 9660. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Treat the stream position as the start of the UDF volume The UDF reader addresses logical sectors from the start of the stream, so the wrapper hands it a view that starts at the current position and keeps that offset, like the other wrappers. CreateDiscImageWrapper no longer skips UDF for a stream that is not at offset 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review comments on the UDF code - Move the descriptor tag check to UDFExtensions, with tests - Parse Volume Structure Descriptors in their own method - Use the existing extensions for numbers, the partition type identifier and printing entity identifiers - Use Array.Exists in IsSafeName - Make Extract non-virtual and remove the printing test - Use collection expressions and string interpolation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Parse File Identifier Descriptors in their own method Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a TODO to use the CRC from Hashing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:18:23 +01:00
using SabreTools.Data.Models.UDF;
using Xunit;
namespace SabreTools.Data.Extensions.Test
{
public class UDFExtensionsTests
{
#region DecodeDCharacters
[Fact]
public void DecodeDCharacters_Null_Empty()
{
byte[]? data = null;
Assert.Equal(string.Empty, data.DecodeDCharacters());
}
[Fact]
public void DecodeDCharacters_8Bit_Decoded()
{
byte[] data = [8, 0x41, 0x62, 0xE4];
Assert.Equal("Abä", data.DecodeDCharacters());
}
[Fact]
public void DecodeDCharacters_16Bit_Decoded()
{
byte[] data = [16, 0x00, 0x41, 0x30, 0x42];
Assert.Equal("Aあ", data.DecodeDCharacters());
}
[Fact]
public void DecodeDCharacters_UnknownCompression_Null()
{
byte[] data = [1, 0x41];
Assert.Null(data.DecodeDCharacters());
}
[Fact]
public void DecodeDString_UsesLengthByte()
{
byte[] data = [8, 0x41, 0x42, 0x43, 0x00, 3];
Assert.Equal("AB", data.DecodeDString());
}
[Fact]
public void DecodeDString_ZeroLength_Empty()
{
byte[] data = [8, 0x41, 0x00, 0];
Assert.Equal(string.Empty, data.DecodeDString());
}
#endregion
#region TryGetSector
[Fact]
public void TryGetSector_Type1_StartPlusBlock()
{
var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 });
bool actual = volume.TryGetSector(UDFExtensions.GetKey(0, 10), out long sector);
Assert.True(actual);
Assert.Equal(1010, sector);
}
[Fact]
public void TryGetSector_Type1_OutsidePartition_False()
{
var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 });
bool actual = volume.TryGetSector(UDFExtensions.GetKey(0, 100), out _);
Assert.False(actual);
}
[Fact]
public void TryGetSector_UnknownPartitionReference_False()
{
var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 });
bool actual = volume.TryGetSector(UDFExtensions.GetKey(1, 0), out _);
Assert.False(actual);
}
[Fact]
public void TryGetSector_Metadata_ThroughMetadataFile()
{
// Metadata File at block 5 of partition 0: blocks 0-1 of the metadata partition are blocks 20-21,
// blocks 2-3 are blocks 40-41
var volume = CreateVolume(
new Type1PartitionMap { PartitionNumber = 0 },
new MetadataPartitionMap { PartitionNumber = 0, MetadataFileLocation = 5, MetadataMirrorFileLocation = 6 });
volume.FileEntries[UDFExtensions.GetKey(0, 5)] = CreateShortAdEntry((4096, 20), (4096, 40));
Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 1), out long first));
Assert.Equal(1021, first);
Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 2), out long second));
Assert.Equal(1040, second);
Assert.False(volume.TryGetSector(UDFExtensions.GetKey(1, 4), out _));
}
[Fact]
public void TryGetSector_Metadata_FallsBackToMirror()
{
var volume = CreateVolume(
new Type1PartitionMap { PartitionNumber = 0 },
new MetadataPartitionMap { PartitionNumber = 0, MetadataFileLocation = 5, MetadataMirrorFileLocation = 6 });
volume.FileEntries[UDFExtensions.GetKey(0, 6)] = CreateShortAdEntry((2048, 30));
Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 0), out long sector));
Assert.Equal(1030, sector);
}
#endregion
#region GetAllocationDescriptors
[Fact]
public void GetAllocationDescriptors_Embedded_Empty()
{
var entry = new FileEntry();
entry.ICBTag.Flags = (ushort)AllocationDescriptorType.EMBEDDED;
entry.AllocationDescriptors = [1, 2, 3];
var actual = new Volume().GetAllocationDescriptors(0, entry);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void GetAllocationDescriptors_ZeroLengthTerminates()
{
var entry = CreateShortAdEntry((2048, 20), (0, 99), (2048, 40));
var actual = new Volume().GetAllocationDescriptors(UDFExtensions.GetKey(2, 7), entry);
Assert.NotNull(actual);
var ad = Assert.Single(actual);
Assert.Equal(20u, ad.ExtentLocation.LogicalBlockNumber);
Assert.Equal((ushort)2, ad.ExtentLocation.PartitionReferenceNumber);
}
[Fact]
public void GetAllocationDescriptors_MissingAllocationExtent_NullWithKey()
{
var entry = CreateShortAdEntry((2048, 20), ((3u << 30) | 2048, 50));
var actual = new Volume().GetAllocationDescriptors(UDFExtensions.GetKey(0, 7), entry, out long missingKey);
Assert.Null(actual);
Assert.Equal(UDFExtensions.GetKey(0, 50), missingKey);
}
[Fact]
public void GetAllocationDescriptors_AllocationExtentLoop_Null()
{
var volume = new Volume();
var entry = CreateShortAdEntry(((3u << 30) | 2048, 50));
volume.AllocationExtentDescriptors[UDFExtensions.GetKey(0, 50)] = new AllocationExtentDescriptor
{
AllocationDescriptors = CreateShortAdEntry(((3u << 30) | 2048, 50)).AllocationDescriptors,
};
var actual = volume.GetAllocationDescriptors(UDFExtensions.GetKey(0, 7), entry);
Assert.Null(actual);
}
#endregion
#region IsValidDescriptor
[Theory]
[InlineData(2)]
[InlineData(3)]
public void IsValidDescriptor_Valid_True(byte version)
{
byte[] data = CreateDescriptor(version);
Assert.True(data.IsValidDescriptor(0, data.Length, 20));
}
[Fact]
public void IsValidDescriptor_WrongChecksum_False()
{
byte[] data = CreateDescriptor(2);
data[4]++;
Assert.False(data.IsValidDescriptor(0, data.Length, 20));
}
[Fact]
public void IsValidDescriptor_UnknownVersion_False()
{
byte[] data = CreateDescriptor(4);
Assert.False(data.IsValidDescriptor(0, data.Length, 20));
}
[Fact]
public void IsValidDescriptor_WrongCrc_False()
{
byte[] data = CreateDescriptor(2);
data[16] ^= 0x01;
Assert.False(data.IsValidDescriptor(0, data.Length, 20));
}
[Fact]
public void IsValidDescriptor_CrcPastAvailable_NotChecked()
{
byte[] data = CreateDescriptor(2);
data[16] ^= 0x01;
Assert.True(data.IsValidDescriptor(0, 16, 20));
}
[Fact]
public void IsValidDescriptor_WrongLocation_False()
{
byte[] data = CreateDescriptor(2);
Assert.False(data.IsValidDescriptor(0, data.Length, 21));
}
#endregion
/// <summary>
/// Descriptor with tag location 20 whose 3 bytes after the tag have the CRC #3299 (ECMA-167 3/7.2.6)
/// </summary>
private static byte[] CreateDescriptor(byte version)
{
byte[] data = new byte[19];
data[0] = 1;
data[2] = version;
data[8] = 0x99;
data[9] = 0x32;
data[10] = 3;
data[12] = 20;
data[16] = 0x70;
data[17] = 0x6A;
data[18] = 0x77;
for (int i = 0; i < 16; i++)
{
if (i != 4)
data[4] += data[i];
}
return data;
}
/// <summary>
/// Volume with partition 0 at sector 1000 (length 50) and the given partition maps
/// </summary>
private static Volume CreateVolume(params PartitionMap[] maps)
{
return new Volume
{
VolumeDescriptorSequence =
[
new PartitionDescriptor { PartitionNumber = 0, PartitionStartingLocation = 1000, PartitionLength = 50 },
new LogicalVolumeDescriptor { LogicalBlockSize = 2048, PartitionMaps = maps },
],
};
}
/// <summary>
/// File Entry with the given short allocation descriptors (extent length, position)
/// </summary>
private static FileEntry CreateShortAdEntry(params (uint Length, uint Position)[] extents)
{
var data = new byte[extents.Length * 8];
for (int i = 0; i < extents.Length; i++)
{
for (int b = 0; b < 4; b++)
{
data[(i * 8) + b] = (byte)(extents[i].Length >> (8 * b));
data[(i * 8) + 4 + b] = (byte)(extents[i].Position >> (8 * b));
}
}
var entry = new FileEntry { AllocationDescriptors = data };
entry.ICBTag.Flags = (ushort)AllocationDescriptorType.SHORT;
return entry;
}
}
}