mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-11-15 17:00:38 +00:00
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>
This commit is contained in:
274
SabreTools.Data.Extensions.Test/UDFExtensionsTests.cs
Normal file
274
SabreTools.Data.Extensions.Test/UDFExtensionsTests.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
417
SabreTools.Data.Extensions/UDFExtensions.cs
Normal file
417
SabreTools.Data.Extensions/UDFExtensions.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.UDF;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Data.Extensions
|
||||
{
|
||||
public static class UDFExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of Allocation Extent Descriptors followed for one File Entry
|
||||
/// </summary>
|
||||
private const int MaximumAllocationExtents = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// CRC-ITU-T (CCITT, polynomial 0x1021) table for descriptor CRCs
|
||||
/// </summary>
|
||||
private static readonly ushort[] CrcTable = CreateCrcTable();
|
||||
|
||||
/// <summary>
|
||||
/// Get the dictionary key of a logical block address
|
||||
/// </summary>
|
||||
/// <param name="address">Logical block address</param>
|
||||
/// <returns>((long)PartitionReferenceNumber << 32) | LogicalBlockNumber</returns>
|
||||
public static long GetKey(this LogicalBlockAddress address)
|
||||
=> GetKey(address.PartitionReferenceNumber, address.LogicalBlockNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Get the dictionary key of a logical block address
|
||||
/// </summary>
|
||||
/// <param name="partitionReferenceNumber">Index of the partition map</param>
|
||||
/// <param name="logicalBlockNumber">Logical block number within the partition</param>
|
||||
/// <returns>((long)partitionReferenceNumber << 32) | logicalBlockNumber</returns>
|
||||
public static long GetKey(ushort partitionReferenceNumber, uint logicalBlockNumber)
|
||||
=> ((long)partitionReferenceNumber << 32) | logicalBlockNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Get the extent type from an allocation descriptor's extent length
|
||||
/// </summary>
|
||||
public static ExtentType GetExtentType(uint extentLength)
|
||||
=> (ExtentType)(extentLength >> Constants.ExtentTypeShift);
|
||||
|
||||
/// <summary>
|
||||
/// Get the extent length in bytes from an allocation descriptor's extent length
|
||||
/// </summary>
|
||||
public static uint GetExtentLength(uint extentLength)
|
||||
=> extentLength & Constants.ExtentLengthMask;
|
||||
|
||||
/// <summary>
|
||||
/// Get the type of the allocation descriptors of an ICB
|
||||
/// </summary>
|
||||
public static AllocationDescriptorType GetAllocationDescriptorType(this ICBTag tag)
|
||||
=> (AllocationDescriptorType)(tag.Flags & Constants.AllocationDescriptorTypeMask);
|
||||
|
||||
/// <summary>
|
||||
/// Get the prevailing Logical Volume Descriptor (highest Volume Descriptor Sequence Number)
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume to search</param>
|
||||
/// <returns>Logical Volume Descriptor on success, null if none was recorded</returns>
|
||||
public static LogicalVolumeDescriptor? GetLogicalVolumeDescriptor(this Volume volume)
|
||||
{
|
||||
LogicalVolumeDescriptor? prevailing = null;
|
||||
foreach (var descriptor in volume.VolumeDescriptorSequence)
|
||||
{
|
||||
if (descriptor is LogicalVolumeDescriptor lvd
|
||||
&& (prevailing is null || lvd.VolumeDescriptorSequenceNumber > prevailing.VolumeDescriptorSequenceNumber))
|
||||
{
|
||||
prevailing = lvd;
|
||||
}
|
||||
}
|
||||
|
||||
return prevailing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the prevailing Partition Descriptor of a partition (highest Volume Descriptor Sequence Number)
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume to search</param>
|
||||
/// <param name="partitionNumber">Partition number</param>
|
||||
/// <returns>Partition Descriptor on success, null if none was recorded</returns>
|
||||
public static PartitionDescriptor? GetPartitionDescriptor(this Volume volume, ushort partitionNumber)
|
||||
{
|
||||
PartitionDescriptor? prevailing = null;
|
||||
foreach (var descriptor in volume.VolumeDescriptorSequence)
|
||||
{
|
||||
if (descriptor is PartitionDescriptor pd
|
||||
&& pd.PartitionNumber == partitionNumber
|
||||
&& (prevailing is null || pd.VolumeDescriptorSequenceNumber > prevailing.VolumeDescriptorSequenceNumber))
|
||||
{
|
||||
prevailing = pd;
|
||||
}
|
||||
}
|
||||
|
||||
return prevailing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the partition reference number of the Type 1 partition map for a partition number
|
||||
/// </summary>
|
||||
/// <param name="lvd">Logical Volume Descriptor holding the partition maps</param>
|
||||
/// <param name="partitionNumber">Partition number</param>
|
||||
/// <returns>Partition reference number on success, -1 if there is no such map</returns>
|
||||
public static int GetType1PartitionReference(this LogicalVolumeDescriptor lvd, ushort partitionNumber)
|
||||
{
|
||||
for (int i = 0; i < lvd.PartitionMaps.Length; i++)
|
||||
{
|
||||
if (lvd.PartitionMaps[i] is Type1PartitionMap map && map.PartitionNumber == partitionNumber)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a logical block of a partition to a logical sector of the volume
|
||||
/// Supports Type 1 partitions and Metadata Partitions (the Metadata File's File Entry must be in FileEntries)
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume containing the Volume Descriptor Sequence</param>
|
||||
/// <param name="key">Logical block address key, see GetKey</param>
|
||||
/// <param name="sector">Logical sector on success</param>
|
||||
/// <returns>True if the block could be mapped, false otherwise</returns>
|
||||
public static bool TryGetSector(this Volume volume, long key, out long sector)
|
||||
{
|
||||
sector = -1;
|
||||
|
||||
var lvd = volume.GetLogicalVolumeDescriptor();
|
||||
if (lvd is null)
|
||||
return false;
|
||||
|
||||
int partitionReference = (int)(key >> 32);
|
||||
uint block = (uint)key;
|
||||
if (partitionReference < 0 || partitionReference >= lvd.PartitionMaps.Length)
|
||||
return false;
|
||||
|
||||
var map = lvd.PartitionMaps[partitionReference];
|
||||
if (map is Type1PartitionMap type1)
|
||||
{
|
||||
var pd = volume.GetPartitionDescriptor(type1.PartitionNumber);
|
||||
if (pd is null || block >= pd.PartitionLength)
|
||||
return false;
|
||||
|
||||
sector = (long)pd.PartitionStartingLocation + block;
|
||||
return true;
|
||||
}
|
||||
else if (map is MetadataPartitionMap metadata)
|
||||
{
|
||||
// The Metadata File (or its mirror) is recorded in the Type 1 partition of the same partition number
|
||||
int physical = lvd.GetType1PartitionReference(metadata.PartitionNumber);
|
||||
if (physical < 0)
|
||||
return false;
|
||||
|
||||
if (!TryGetMetadataFileBlock(volume, lvd, GetKey((ushort)physical, metadata.MetadataFileLocation), block, out long physicalKey)
|
||||
&& !TryGetMetadataFileBlock(volume, lvd, GetKey((ushort)physical, metadata.MetadataMirrorFileLocation), block, out physicalKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return volume.TryGetSector(physicalKey, out sector);
|
||||
}
|
||||
|
||||
// Virtual and Sparable partitions are not supported
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a logical block of a Metadata Partition through the Metadata File's allocation descriptors
|
||||
/// </summary>
|
||||
private static bool TryGetMetadataFileBlock(Volume volume, LogicalVolumeDescriptor lvd, long fileKey, uint block, out long physicalKey)
|
||||
{
|
||||
physicalKey = -1;
|
||||
if (!volume.FileEntries.TryGetValue(fileKey, out var fileEntry))
|
||||
return false;
|
||||
|
||||
var descriptors = volume.GetAllocationDescriptors(fileKey, fileEntry);
|
||||
if (descriptors is null)
|
||||
return false;
|
||||
|
||||
uint blockSize = lvd.LogicalBlockSize;
|
||||
long first = 0;
|
||||
foreach (var ad in descriptors)
|
||||
{
|
||||
long blocks = (GetExtentLength(ad.ExtentLength) + blockSize - 1) / blockSize;
|
||||
if (block < first + blocks)
|
||||
{
|
||||
if (GetExtentType(ad.ExtentLength) != ExtentType.RECORDED_AND_ALLOCATED)
|
||||
return false;
|
||||
|
||||
physicalKey = GetKey(ad.ExtentLocation.PartitionReferenceNumber, (uint)(ad.ExtentLocation.LogicalBlockNumber + (block - first)));
|
||||
return true;
|
||||
}
|
||||
|
||||
first += blocks;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the allocation descriptors of a File Entry, including those continued in Allocation Extent Descriptors,
|
||||
/// as long allocation descriptors (short allocation descriptors get the partition of the File Entry)
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume containing the Allocation Extent Descriptors</param>
|
||||
/// <param name="key">Logical block address key of the File Entry</param>
|
||||
/// <param name="fileEntry">File Entry</param>
|
||||
/// <returns>Allocation descriptors (empty for embedded data), null if they are not supported or incomplete</returns>
|
||||
public static List<LongAllocationDescriptor>? GetAllocationDescriptors(this Volume volume, long key, FileEntry fileEntry)
|
||||
=> volume.GetAllocationDescriptors(key, fileEntry, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Get the allocation descriptors of a File Entry, including those continued in Allocation Extent Descriptors,
|
||||
/// as long allocation descriptors (short allocation descriptors get the partition of the File Entry)
|
||||
/// </summary>
|
||||
/// <param name="volume">Volume containing the Allocation Extent Descriptors</param>
|
||||
/// <param name="key">Logical block address key of the File Entry</param>
|
||||
/// <param name="fileEntry">File Entry</param>
|
||||
/// <param name="missingKey">Key of the first Allocation Extent Descriptor that is not in the volume, -1 if none</param>
|
||||
/// <returns>Allocation descriptors (empty for embedded data), null if they are not supported or incomplete</returns>
|
||||
public static List<LongAllocationDescriptor>? GetAllocationDescriptors(this Volume volume, long key, FileEntry fileEntry, out long missingKey)
|
||||
{
|
||||
missingKey = -1;
|
||||
|
||||
var type = fileEntry.ICBTag.GetAllocationDescriptorType();
|
||||
if (type == AllocationDescriptorType.EMBEDDED)
|
||||
return [];
|
||||
|
||||
// Extended allocation descriptors are not allowed by UDF
|
||||
if (type != AllocationDescriptorType.SHORT && type != AllocationDescriptorType.LONG)
|
||||
return null;
|
||||
|
||||
List<LongAllocationDescriptor> result = [];
|
||||
ushort partition = (ushort)(key >> 32);
|
||||
byte[] descriptors = fileEntry.AllocationDescriptors;
|
||||
int length = descriptors.Length;
|
||||
|
||||
for (int extents = 0; extents <= MaximumAllocationExtents; extents++)
|
||||
{
|
||||
long nextKey = -1;
|
||||
int size = type == AllocationDescriptorType.SHORT ? 8 : 16;
|
||||
for (int offset = 0; offset + size <= length; offset += size)
|
||||
{
|
||||
var ad = new LongAllocationDescriptor();
|
||||
ad.ExtentLength = descriptors.ToUInt32LittleEndian(offset);
|
||||
if (type == AllocationDescriptorType.SHORT)
|
||||
{
|
||||
ad.ExtentLocation.LogicalBlockNumber = descriptors.ToUInt32LittleEndian(offset + 4);
|
||||
ad.ExtentLocation.PartitionReferenceNumber = partition;
|
||||
}
|
||||
else
|
||||
{
|
||||
ad.ExtentLocation.LogicalBlockNumber = descriptors.ToUInt32LittleEndian(offset + 4);
|
||||
ad.ExtentLocation.PartitionReferenceNumber = descriptors.ToUInt16LittleEndian(offset + 8);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
ad.ImplementationUse[i] = descriptors[offset + 10 + i];
|
||||
}
|
||||
}
|
||||
|
||||
// A zero length terminates the allocation descriptors
|
||||
if (GetExtentLength(ad.ExtentLength) == 0)
|
||||
return result;
|
||||
|
||||
if (GetExtentType(ad.ExtentLength) == ExtentType.NEXT_EXTENT)
|
||||
{
|
||||
nextKey = ad.ExtentLocation.GetKey();
|
||||
break;
|
||||
}
|
||||
|
||||
result.Add(ad);
|
||||
}
|
||||
|
||||
if (nextKey < 0)
|
||||
return result;
|
||||
|
||||
if (!volume.AllocationExtentDescriptors.TryGetValue(nextKey, out var aed))
|
||||
{
|
||||
missingKey = nextKey;
|
||||
return null;
|
||||
}
|
||||
|
||||
partition = (ushort)(nextKey >> 32);
|
||||
descriptors = aed.AllocationDescriptors;
|
||||
length = descriptors.Length;
|
||||
}
|
||||
|
||||
// Too many continuation extents, assume a loop
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check a descriptor tag: checksum, version, CRC (when it fits into the data) and location
|
||||
/// </summary>
|
||||
/// <remarks>ECMA-167 3/7.2; receiving systems allow descriptor version 2 or 3 (3/7.2.2)</remarks>
|
||||
/// <param name="data">Data holding the descriptor</param>
|
||||
/// <param name="offset">Offset of the descriptor</param>
|
||||
/// <param name="available">Number of bytes of the descriptor available from the offset</param>
|
||||
/// <param name="location">Expected tag location (logical sector or logical block number)</param>
|
||||
/// <returns>True if the tag is valid, false otherwise</returns>
|
||||
public static bool IsValidDescriptor(this byte[] data, int offset, int available, uint location)
|
||||
{
|
||||
byte checksum = 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
if (i != 4)
|
||||
checksum += data[offset + i];
|
||||
}
|
||||
|
||||
if (checksum != data[offset + 4])
|
||||
return false;
|
||||
|
||||
ushort version = data.ToUInt16LittleEndian(offset + 2);
|
||||
if (version != 2 && version != 3)
|
||||
return false;
|
||||
|
||||
ushort crcLength = data.ToUInt16LittleEndian(offset + 10);
|
||||
if (16 + crcLength <= available)
|
||||
{
|
||||
if (ComputeCrc(data, offset + 16, crcLength) != data.ToUInt16LittleEndian(offset + 8))
|
||||
return false;
|
||||
}
|
||||
|
||||
return data.ToUInt32LittleEndian(offset + 12) == location;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode OSTA Compressed Unicode d-characters (first byte = compression ID)
|
||||
/// </summary>
|
||||
/// <param name="data">Encoded characters</param>
|
||||
/// <returns>Decoded string, empty for no characters, null for an unknown compression ID</returns>
|
||||
public static string? DecodeDCharacters(this byte[]? data)
|
||||
=> DecodeDCharacters(data, data?.Length ?? 0);
|
||||
|
||||
/// <summary>
|
||||
/// Decode an OSTA Compressed Unicode dstring (last byte = number of used bytes)
|
||||
/// </summary>
|
||||
/// <param name="data">Encoded dstring field</param>
|
||||
/// <returns>Decoded string, empty for no characters, null for an unknown compression ID</returns>
|
||||
public static string? DecodeDString(this byte[]? data)
|
||||
{
|
||||
if (data is null || data.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
int length = data[data.Length - 1];
|
||||
if (length > data.Length - 1)
|
||||
length = data.Length - 1;
|
||||
|
||||
return DecodeDCharacters(data, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode the first length bytes of OSTA Compressed Unicode d-characters
|
||||
/// </summary>
|
||||
private static string? DecodeDCharacters(byte[]? data, int length)
|
||||
{
|
||||
if (data is null || length == 0)
|
||||
return string.Empty;
|
||||
|
||||
// 254 and 255 are the same encodings, used by UDF for deleted entries
|
||||
byte compressionId = data[0];
|
||||
var builder = new StringBuilder(length);
|
||||
if (compressionId == Constants.CompressionId8 || compressionId == 254)
|
||||
{
|
||||
for (int i = 1; i < length; i++)
|
||||
{
|
||||
builder.Append((char)data[i]);
|
||||
}
|
||||
}
|
||||
else if (compressionId == Constants.CompressionId16 || compressionId == 255)
|
||||
{
|
||||
for (int i = 1; i + 1 < length; i += 2)
|
||||
{
|
||||
builder.Append((char)data.ToUInt16BigEndian(i));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
// TODO: Replace with CRC-16/XMODEM from Hashing once its CRC code is faster
|
||||
/// <summary>
|
||||
/// Compute the CRC-ITU-T of a range of bytes
|
||||
/// </summary>
|
||||
private static ushort ComputeCrc(byte[] data, int offset, int length)
|
||||
{
|
||||
ushort crc = 0;
|
||||
for (int i = offset; i < offset + length; i++)
|
||||
{
|
||||
crc = (ushort)(CrcTable[((crc >> 8) ^ data[i]) & 0xFF] ^ (crc << 8));
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the CRC-ITU-T table
|
||||
/// </summary>
|
||||
private static ushort[] CreateCrcTable()
|
||||
{
|
||||
var table = new ushort[256];
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
ushort crc = (ushort)(i << 8);
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc & 0x8000) != 0 ? (ushort)((crc << 1) ^ 0x1021) : (ushort)(crc << 1);
|
||||
}
|
||||
|
||||
table[i] = crc;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
294
SabreTools.Serialization.Readers.Test/UDFImageBuilder.cs
Normal file
294
SabreTools.Serialization.Readers.Test/UDFImageBuilder.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.Serialization.Readers.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a minimal UDF 1.02 image in memory (2048-byte sectors, one Type 1 partition at sector 260)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Partition blocks:
|
||||
/// 0 File Set Descriptor
|
||||
/// 2 root directory File Entry, contents in block 3:
|
||||
/// parent, "README.TXT" (embedded data), "deleted.txt" (deleted), "Sub" (directory),
|
||||
/// "Media" (last entry, no padding)
|
||||
/// 4 "README.TXT" File Entry, data "hello world" embedded
|
||||
/// 5 "Sub" File Entry, contents in block 6: parent, "data.bin" (16-bit name)
|
||||
/// 7 "data.bin" File Entry, long_ad to block 8 (2048 bytes), then an Allocation Extent Descriptor
|
||||
/// in block 10 with a long_ad to block 9 (952 bytes)
|
||||
/// 8-9 "data.bin" data (byte i = i % 251)
|
||||
/// 11 "Media" File Entry, one short_ad of 100 bytes allocated but not recorded
|
||||
/// </remarks>
|
||||
internal static class UDFImageBuilder
|
||||
{
|
||||
public const int SectorSize = 2048;
|
||||
public const int PartitionStart = 260;
|
||||
public const int PartitionLength = 40;
|
||||
public const int Sectors = 300;
|
||||
public const int DataBinLength = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Build the image
|
||||
/// </summary>
|
||||
/// <param name="corruptRootEntry">True to break the checksum of the root directory File Entry</param>
|
||||
/// <param name="corruptRootEntryCrc">True to change a byte of the root directory File Entry that only the CRC covers</param>
|
||||
/// <param name="emptyPartitionMap">True to record the partition map with length 0 and a map count of 3</param>
|
||||
/// <param name="pointerLoop">True to replace the Terminating Descriptor by a Volume Descriptor Pointer back to the start of the sequence</param>
|
||||
/// <param name="subName">Name of the "Sub" directory in the root directory</param>
|
||||
/// <param name="subLoop">True to add a directory "loop" to "Sub" that points back to the root directory</param>
|
||||
/// <param name="mediaPastEnd">True to record "Media" in a partition block past the end of the image</param>
|
||||
public static byte[] Build(bool corruptRootEntry = false,
|
||||
bool corruptRootEntryCrc = false,
|
||||
bool emptyPartitionMap = false,
|
||||
bool pointerLoop = false,
|
||||
string subName = "Sub",
|
||||
bool subLoop = false,
|
||||
bool mediaPastEnd = false)
|
||||
{
|
||||
var image = new byte[Sectors * SectorSize];
|
||||
|
||||
// Volume Recognition Sequence
|
||||
WriteStructure(image, 16, "BEA01");
|
||||
WriteStructure(image, 17, "NSR02");
|
||||
WriteStructure(image, 18, "TEA01");
|
||||
|
||||
// Anchor Volume Descriptor Pointer: main and reserve sequence at sector 32, 4 sectors
|
||||
int avdp = 256 * SectorSize;
|
||||
WriteUInt32(image, avdp + 16, 4 * SectorSize);
|
||||
WriteUInt32(image, avdp + 20, 32);
|
||||
WriteUInt32(image, avdp + 24, 4 * SectorSize);
|
||||
WriteUInt32(image, avdp + 28, 32);
|
||||
WriteTag(image, avdp, 2, 256, 512);
|
||||
|
||||
// Primary Volume Descriptor
|
||||
int pvd = 32 * SectorSize;
|
||||
WriteDString(image, pvd + 24, 32, "TESTVOLUME");
|
||||
WriteTag(image, pvd, 1, 32, 512);
|
||||
|
||||
// Partition Descriptor: partition 0 at sector 260
|
||||
int pd = 33 * SectorSize;
|
||||
WriteUInt32(image, pd + 16, 1);
|
||||
image[pd + 22] = 0;
|
||||
WriteUInt32(image, pd + 188, PartitionStart);
|
||||
WriteUInt32(image, pd + 192, mediaPastEnd ? PartitionLength + 20 : PartitionLength);
|
||||
WriteTag(image, pd, 5, 33, 512);
|
||||
|
||||
// Logical Volume Descriptor: block size, domain, File Set Descriptor at block 0, one Type 1 map
|
||||
int lvd = 34 * SectorSize;
|
||||
WriteUInt32(image, lvd + 16, 2);
|
||||
WriteDString(image, lvd + 84, 128, "TESTVOLUME");
|
||||
WriteUInt32(image, lvd + 212, SectorSize);
|
||||
WriteAscii(image, lvd + 217, "*OSTA UDF Compliant");
|
||||
image[lvd + 240] = 0x02;
|
||||
image[lvd + 241] = 0x01;
|
||||
WriteLongAd(image, lvd + 248, SectorSize, 0);
|
||||
WriteUInt32(image, lvd + 264, 6);
|
||||
WriteUInt32(image, lvd + 268, emptyPartitionMap ? 3 : 1);
|
||||
image[lvd + 440] = 1;
|
||||
image[lvd + 441] = (byte)(emptyPartitionMap ? 0 : 6);
|
||||
image[lvd + 442] = 1;
|
||||
WriteTag(image, lvd, 6, 34, 446);
|
||||
|
||||
// Terminating Descriptor, or a Volume Descriptor Pointer back to the start of the sequence
|
||||
int td = 35 * SectorSize;
|
||||
if (pointerLoop)
|
||||
{
|
||||
WriteUInt32(image, td + 20, 4 * SectorSize);
|
||||
WriteUInt32(image, td + 24, 32);
|
||||
WriteTag(image, td, 3, 35, 512);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteTag(image, td, 8, 35, 512);
|
||||
}
|
||||
|
||||
// File Set Descriptor: root directory ICB at block 2
|
||||
int fsd = Block(0);
|
||||
WriteDString(image, fsd + 304, 32, "TESTSET");
|
||||
WriteLongAd(image, fsd + 400, SectorSize, 2);
|
||||
WriteTag(image, fsd, 256, 0, 512);
|
||||
|
||||
// Root directory contents
|
||||
int root = Block(3);
|
||||
int length = 0;
|
||||
length += WriteFid(image, root + length, 3, 0x0A, 2, []);
|
||||
length += WriteFid(image, root + length, 3, 0x00, 4, Name8("README.TXT"));
|
||||
length += WriteFid(image, root + length, 3, 0x04, 4, Name8("deleted.txt"));
|
||||
length += WriteFid(image, root + length, 3, 0x02, 5, Name8(subName));
|
||||
length += WriteFid(image, root + length, 3, 0x00, 11, Name8("Media"));
|
||||
WriteFileEntry(image, 2, 4, 0, length, ShortAd(length, 3));
|
||||
if (corruptRootEntry)
|
||||
image[Block(2) + 4] ^= 0xFF;
|
||||
if (corruptRootEntryCrc)
|
||||
image[Block(2) + 36] ^= 0xFF;
|
||||
|
||||
// "README.TXT", embedded
|
||||
WriteFileEntry(image, 4, 5, 3, 11, Encoding.ASCII.GetBytes("hello world"));
|
||||
|
||||
// "Sub" contents
|
||||
int sub = Block(6);
|
||||
length = 0;
|
||||
length += WriteFid(image, sub + length, 6, 0x0A, 2, []);
|
||||
length += WriteFid(image, sub + length, 6, 0x00, 7, Name16("data.bin"));
|
||||
if (subLoop)
|
||||
length += WriteFid(image, sub + length, 6, 0x02, 2, Name8("loop"));
|
||||
|
||||
WriteFileEntry(image, 5, 4, 0, length, ShortAd(length, 6));
|
||||
|
||||
// "data.bin", long allocation descriptors continued in an Allocation Extent Descriptor
|
||||
var ads = new byte[32];
|
||||
WriteLongAd(ads, 0, SectorSize, 8);
|
||||
WriteLongAd(ads, 16, (3u << 30) | SectorSize, 10);
|
||||
WriteFileEntry(image, 7, 5, 1, DataBinLength, ads);
|
||||
for (int i = 0; i < DataBinLength; i++)
|
||||
{
|
||||
image[Block(8) + i] = (byte)(i % 251);
|
||||
}
|
||||
|
||||
int aed = Block(10);
|
||||
WriteUInt32(image, aed + 20, 16);
|
||||
WriteLongAd(image, aed + 24, DataBinLength - SectorSize, 9);
|
||||
WriteTag(image, aed, 258, 10, 40);
|
||||
|
||||
// "Media", allocated but not recorded, or recorded past the end of the image
|
||||
if (mediaPastEnd)
|
||||
WriteFileEntry(image, 11, 5, 0, 100, ShortAd(100, PartitionLength + 10));
|
||||
else
|
||||
WriteFileEntry(image, 11, 5, 0, 100, ShortAd((1u << 30) | 100, 0));
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offset of a partition block in the image
|
||||
/// </summary>
|
||||
public static int Block(int block) => (PartitionStart + block) * SectorSize;
|
||||
|
||||
private static void WriteStructure(byte[] image, int sector, string identifier)
|
||||
{
|
||||
int offset = sector * SectorSize;
|
||||
image[offset] = 0;
|
||||
WriteAscii(image, offset + 1, identifier);
|
||||
image[offset + 6] = 1;
|
||||
}
|
||||
|
||||
private static int WriteFid(byte[] image, int offset, uint block, byte characteristics, uint icb, byte[] name)
|
||||
{
|
||||
image[offset + 16] = 1;
|
||||
image[offset + 18] = characteristics;
|
||||
image[offset + 19] = (byte)name.Length;
|
||||
WriteLongAd(image, offset + 20, SectorSize, icb);
|
||||
Array.Copy(name, 0, image, offset + 38, name.Length);
|
||||
int length = (38 + name.Length + 3) & ~3;
|
||||
WriteTag(image, offset, 257, block, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
private static void WriteFileEntry(byte[] image, int block, byte fileType, ushort adType, long informationLength, byte[] ads)
|
||||
{
|
||||
int offset = Block(block);
|
||||
image[offset + 16 + 4] = 4;
|
||||
image[offset + 16 + 11] = fileType;
|
||||
image[offset + 16 + 18] = (byte)adType;
|
||||
WriteUInt32(image, offset + 56, (uint)informationLength);
|
||||
WriteUInt32(image, offset + 172, (uint)ads.Length);
|
||||
Array.Copy(ads, 0, image, offset + 176, ads.Length);
|
||||
WriteTag(image, offset, 261, (uint)block, 176 + ads.Length);
|
||||
}
|
||||
|
||||
private static byte[] ShortAd(uint length, uint position)
|
||||
{
|
||||
var ad = new byte[8];
|
||||
WriteUInt32(ad, 0, length);
|
||||
WriteUInt32(ad, 4, position);
|
||||
return ad;
|
||||
}
|
||||
|
||||
private static byte[] ShortAd(int length, uint position) => ShortAd((uint)length, position);
|
||||
|
||||
private static void WriteLongAd(byte[] data, int offset, uint length, uint block)
|
||||
{
|
||||
WriteUInt32(data, offset, length);
|
||||
WriteUInt32(data, offset + 4, block);
|
||||
}
|
||||
|
||||
private static void WriteLongAd(byte[] data, int offset, int length, uint block)
|
||||
=> WriteLongAd(data, offset, (uint)length, block);
|
||||
|
||||
private static byte[] Name8(string name)
|
||||
{
|
||||
var bytes = new byte[name.Length + 1];
|
||||
bytes[0] = 8;
|
||||
Encoding.ASCII.GetBytes(name, 0, name.Length, bytes, 1);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] Name16(string name)
|
||||
{
|
||||
var bytes = new byte[(name.Length * 2) + 1];
|
||||
bytes[0] = 16;
|
||||
Encoding.BigEndianUnicode.GetBytes(name, 0, name.Length, bytes, 1);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void WriteDString(byte[] image, int offset, int fieldLength, string value)
|
||||
{
|
||||
image[offset] = 8;
|
||||
WriteAscii(image, offset + 1, value);
|
||||
image[offset + fieldLength - 1] = (byte)(value.Length + 1);
|
||||
}
|
||||
|
||||
private static void WriteAscii(byte[] image, int offset, string value)
|
||||
=> Encoding.ASCII.GetBytes(value, 0, value.Length, image, offset);
|
||||
|
||||
private static void WriteUInt32(byte[] data, int offset, uint value)
|
||||
{
|
||||
data[offset] = (byte)value;
|
||||
data[offset + 1] = (byte)(value >> 8);
|
||||
data[offset + 2] = (byte)(value >> 16);
|
||||
data[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(byte[] data, int offset, int value) => WriteUInt32(data, offset, (uint)value);
|
||||
|
||||
/// <summary>
|
||||
/// Write a descriptor tag covering the given descriptor length (version 2, CRC, checksum)
|
||||
/// </summary>
|
||||
private static void WriteTag(byte[] image, int offset, ushort identifier, uint location, int length)
|
||||
{
|
||||
image[offset] = (byte)identifier;
|
||||
image[offset + 1] = (byte)(identifier >> 8);
|
||||
image[offset + 2] = 2;
|
||||
ushort crc = Crc(image, offset + 16, length - 16);
|
||||
image[offset + 8] = (byte)crc;
|
||||
image[offset + 9] = (byte)(crc >> 8);
|
||||
image[offset + 10] = (byte)(length - 16);
|
||||
image[offset + 11] = (byte)((length - 16) >> 8);
|
||||
WriteUInt32(image, offset + 12, location);
|
||||
|
||||
byte checksum = 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
if (i != 4)
|
||||
checksum += image[offset + i];
|
||||
}
|
||||
|
||||
image[offset + 4] = checksum;
|
||||
}
|
||||
|
||||
private static ushort Crc(byte[] data, int offset, int length)
|
||||
{
|
||||
ushort crc = 0;
|
||||
for (int i = offset; i < offset + length; i++)
|
||||
{
|
||||
crc ^= (ushort)(data[i] << 8);
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc & 0x8000) != 0 ? (ushort)((crc << 1) ^ 0x1021) : (ushort)(crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
}
|
||||
289
SabreTools.Serialization.Readers.Test/UDFTests.cs
Normal file
289
SabreTools.Serialization.Readers.Test/UDFTests.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Extensions;
|
||||
using SabreTools.Data.Models.UDF;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Serialization.Readers.Test
|
||||
{
|
||||
public class UDFTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var deserializer = new UDF();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidImage_VolumeStructures()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build()));
|
||||
|
||||
Assert.NotNull(volume);
|
||||
Assert.Equal(3, volume.VolumeRecognitionSequence.Length);
|
||||
Assert.NotNull(volume.AnchorVolumeDescriptorPointer);
|
||||
Assert.Equal(4, volume.VolumeDescriptorSequence.Length);
|
||||
var terminator = Assert.IsType<GenericDescriptor>(volume.VolumeDescriptorSequence[3]);
|
||||
Assert.Equal(TagIdentifier.TERMINATING_DESCRIPTOR, terminator.DescriptorTag.TagIdentifier);
|
||||
|
||||
var lvd = volume.GetLogicalVolumeDescriptor();
|
||||
Assert.NotNull(lvd);
|
||||
Assert.Equal("TESTVOLUME", lvd.LogicalVolumeIdentifier.DecodeDString());
|
||||
Assert.IsType<Type1PartitionMap>(Assert.Single(lvd.PartitionMaps));
|
||||
|
||||
Assert.NotNull(volume.FileSetDescriptor);
|
||||
Assert.Equal("TESTSET", volume.FileSetDescriptor.FileSetIdentifier.DecodeDString());
|
||||
Assert.Equal(2u, volume.FileSetDescriptor.RootDirectoryICB.ExtentLocation.LogicalBlockNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidImage_DirectoryHierarchy()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build()));
|
||||
|
||||
Assert.NotNull(volume);
|
||||
|
||||
// Root, "README.TXT", "Sub", "data.bin", "Media"; the deleted entry is not followed
|
||||
Assert.Equal(5, volume.FileEntries.Count);
|
||||
Assert.Equal(2, volume.Directories.Count);
|
||||
Assert.Single(volume.AllocationExtentDescriptors);
|
||||
|
||||
// The last root entry has no padding and ends exactly at the end of the directory
|
||||
var root = volume.Directories[UDFExtensions.GetKey(0, 2)];
|
||||
Assert.Equal(5, root.Length);
|
||||
Assert.Equal("Media", root[4].FileIdentifier.DecodeDCharacters());
|
||||
Assert.Empty(root[4].Padding);
|
||||
|
||||
var sub = volume.Directories[UDFExtensions.GetKey(0, 5)];
|
||||
Assert.Equal("data.bin", sub[1].FileIdentifier.DecodeDCharacters());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindFileEntry_EmbeddedData()
|
||||
{
|
||||
var data = new MemoryStream(UDFImageBuilder.Build());
|
||||
var volume = new UDF().Deserialize(data);
|
||||
Assert.NotNull(volume);
|
||||
|
||||
var entry = UDF.FindFileEntry(data, volume, "README.TXT", out long key);
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(UDFExtensions.GetKey(0, 4), key);
|
||||
Assert.Equal(AllocationDescriptorType.EMBEDDED, entry.ICBTag.GetAllocationDescriptorType());
|
||||
Assert.Equal("hello world", Encoding.ASCII.GetString(entry.AllocationDescriptors, 0, (int)entry.InformationLength));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindFileEntry_CaseInsensitive()
|
||||
{
|
||||
var data = new MemoryStream(UDFImageBuilder.Build());
|
||||
var volume = new UDF().Deserialize(data);
|
||||
Assert.NotNull(volume);
|
||||
|
||||
var entry = UDF.FindFileEntry(data, volume, "/sub\\DATA.BIN", out long key);
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(UDFExtensions.GetKey(0, 7), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindFileEntry_DeletedOrMissing_Null()
|
||||
{
|
||||
var data = new MemoryStream(UDFImageBuilder.Build());
|
||||
var volume = new UDF().Deserialize(data);
|
||||
Assert.NotNull(volume);
|
||||
|
||||
Assert.Null(UDF.FindFileEntry(data, volume, "deleted.txt", out long deletedKey));
|
||||
Assert.Equal(-1, deletedKey);
|
||||
Assert.Null(UDF.FindFileEntry(data, volume, "Sub/missing.bin", out _));
|
||||
Assert.Null(UDF.FindFileEntry(data, volume, "README.TXT/data.bin", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindFileEntry_WithoutDirectories_ReadsOnlyThePath()
|
||||
{
|
||||
var data = new MemoryStream(UDFImageBuilder.Build());
|
||||
var volume = new UDF().Deserialize(data, includeDirectories: false);
|
||||
Assert.NotNull(volume);
|
||||
Assert.Empty(volume.FileEntries);
|
||||
Assert.Empty(volume.Directories);
|
||||
|
||||
var entry = UDF.FindFileEntry(data, volume, "Sub/data.bin", out long key);
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(UDFExtensions.GetKey(0, 7), key);
|
||||
|
||||
// Root, "Sub" and "data.bin" only
|
||||
Assert.Equal(3, volume.FileEntries.Count);
|
||||
Assert.Equal(2, volume.Directories.Count);
|
||||
Assert.False(volume.FileEntries.ContainsKey(UDFExtensions.GetKey(0, 4)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllocationExtent_Followed()
|
||||
{
|
||||
byte[] image = UDFImageBuilder.Build();
|
||||
var data = new MemoryStream(image);
|
||||
var volume = new UDF().Deserialize(data);
|
||||
Assert.NotNull(volume);
|
||||
|
||||
long key = UDFExtensions.GetKey(0, 7);
|
||||
var descriptors = volume.GetAllocationDescriptors(key, volume.FileEntries[key]);
|
||||
|
||||
Assert.NotNull(descriptors);
|
||||
Assert.Equal(2, descriptors.Count);
|
||||
Assert.Equal(8u, descriptors[0].ExtentLocation.LogicalBlockNumber);
|
||||
Assert.Equal(9u, descriptors[1].ExtentLocation.LogicalBlockNumber);
|
||||
Assert.Equal((uint)UDFImageBuilder.DataBinLength, UDFExtensions.GetExtentLength(descriptors[0].ExtentLength) + UDFExtensions.GetExtentLength(descriptors[1].ExtentLength));
|
||||
|
||||
Assert.True(volume.TryGetSector(descriptors[1].ExtentLocation.GetKey(), out long sector));
|
||||
Assert.Equal(UDFImageBuilder.PartitionStart + 9, sector);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotRecordedExtent_Kept()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build()));
|
||||
Assert.NotNull(volume);
|
||||
|
||||
long key = UDFExtensions.GetKey(0, 11);
|
||||
var descriptors = volume.GetAllocationDescriptors(key, volume.FileEntries[key]);
|
||||
|
||||
Assert.NotNull(descriptors);
|
||||
var ad = Assert.Single(descriptors);
|
||||
Assert.Equal(ExtentType.NOT_RECORDED_BUT_ALLOCATED, UDFExtensions.GetExtentType(ad.ExtentLength));
|
||||
Assert.Equal(100u, UDFExtensions.GetExtentLength(ad.ExtentLength));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorruptRootEntry_NoDirectories()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(corruptRootEntry: true)));
|
||||
|
||||
Assert.NotNull(volume);
|
||||
Assert.NotNull(volume.FileSetDescriptor);
|
||||
Assert.Empty(volume.FileEntries);
|
||||
Assert.Empty(volume.Directories);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorruptRootEntryCrc_NoDirectories()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(corruptRootEntryCrc: true)));
|
||||
|
||||
Assert.NotNull(volume);
|
||||
Assert.NotNull(volume.FileSetDescriptor);
|
||||
Assert.Empty(volume.FileEntries);
|
||||
Assert.Empty(volume.Directories);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyPartitionMap_EndsMapTable()
|
||||
{
|
||||
var data = new MemoryStream(UDFImageBuilder.Build(emptyPartitionMap: true));
|
||||
var anchor = UDF.ParseAnchorVolumeDescriptorPointer(data, Constants.AnchorVolumeDescriptorPointerSector);
|
||||
Assert.NotNull(anchor);
|
||||
|
||||
var sequence = UDF.ParseVolumeDescriptorSequence(data, anchor.MainVolumeDescriptorSequenceExtent);
|
||||
|
||||
var lvd = Assert.Single(sequence.OfType<LogicalVolumeDescriptor>());
|
||||
Assert.Empty(lvd.PartitionMaps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VolumeDescriptorPointerLoop_Stops()
|
||||
{
|
||||
var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(pointerLoop: true)));
|
||||
|
||||
Assert.NotNull(volume);
|
||||
Assert.Equal(5, volume.FileEntries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreadableSector_OtherEntriesRead()
|
||||
{
|
||||
// The "Sub" File Entry (block 5) cannot be read; "README.TXT" (block 4) is read in the same request
|
||||
byte[] image = UDFImageBuilder.Build();
|
||||
long bad = UDFImageBuilder.Block(5);
|
||||
var volume = new UDF().Deserialize(new ThrowingStream(image, bad, bad + UDFImageBuilder.SectorSize));
|
||||
|
||||
// Root, "README.TXT" and "Media"
|
||||
long[] expected = [UDFExtensions.GetKey(0, 2), UDFExtensions.GetKey(0, 4), UDFExtensions.GetKey(0, 11)];
|
||||
Assert.NotNull(volume);
|
||||
Assert.Equal(expected, volume.FileEntries.Keys.OrderBy(k => k));
|
||||
Assert.Single(volume.Directories);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stream that throws an IOException for reads touching a range, like a drive with an unreadable sector
|
||||
/// </summary>
|
||||
private sealed class ThrowingStream(byte[] data, long badStart, long badEnd) : MemoryStream(data)
|
||||
{
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (Position < badEnd && Position + count > badStart)
|
||||
throw new IOException("Unreadable sector");
|
||||
|
||||
return base.Read(buffer, offset, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1296
SabreTools.Serialization.Readers/UDF.cs
Normal file
1296
SabreTools.Serialization.Readers/UDF.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,10 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SabreTools.Serialization.Readers.Test\UDFImageBuilder.cs" Link="UDFImageBuilder.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SabreTools.Wrappers\SabreTools.Wrappers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
162
SabreTools.Wrappers.Test/UDFTests.cs
Normal file
162
SabreTools.Wrappers.Test/UDFTests.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Serialization.Readers.Test;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Wrappers.Test
|
||||
{
|
||||
public class UDFTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var actual = UDF.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var actual = UDF.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var actual = UDF.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var actual = UDF.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var actual = UDF.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var actual = UDF.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectoryLoop_Extract()
|
||||
{
|
||||
var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(subLoop: true)));
|
||||
Assert.NotNull(wrapper);
|
||||
|
||||
string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
Assert.True(wrapper.Extract(output, false));
|
||||
Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Sub", "data.bin")).Length);
|
||||
Assert.True(Directory.Exists(Path.Combine(output, "Sub", "loop")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(output))
|
||||
Directory.Delete(output, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentDirectoryName_NotExtracted()
|
||||
{
|
||||
// "Sub" is named "..", so "data.bin" would be written next to the output directory
|
||||
var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(subName: "..")));
|
||||
Assert.NotNull(wrapper);
|
||||
|
||||
string parent = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
string output = Path.Combine(parent, "output");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
Assert.False(wrapper.Extract(output, false));
|
||||
Assert.False(File.Exists(Path.Combine(parent, "data.bin")));
|
||||
Assert.True(File.Exists(Path.Combine(output, "README.TXT")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(parent))
|
||||
Directory.Delete(parent, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordedPastEnd_OtherFilesExtracted()
|
||||
{
|
||||
// "Media" is recorded in a partition block past the end of the image
|
||||
var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(mediaPastEnd: true)));
|
||||
Assert.NotNull(wrapper);
|
||||
|
||||
string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
Assert.False(wrapper.Extract(output, false));
|
||||
Assert.True(File.Exists(Path.Combine(output, "README.TXT")));
|
||||
Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Sub", "data.bin")).Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(output))
|
||||
Directory.Delete(output, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamAtOffset_VolumeAtOffsetExtracted()
|
||||
{
|
||||
// Another volume comes first, so reading from the start of the stream would find "Outer" instead of "Inner"
|
||||
var stream = new MemoryStream([.. UDFImageBuilder.Build(subName: "Outer"), .. UDFImageBuilder.Build(subName: "Inner")]);
|
||||
stream.Position = UDFImageBuilder.Sectors * UDFImageBuilder.SectorSize;
|
||||
|
||||
var wrapper = UDF.Create(stream);
|
||||
Assert.NotNull(wrapper);
|
||||
|
||||
string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
Assert.True(wrapper.Extract(output, false));
|
||||
Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Inner", "data.bin")).Length);
|
||||
Assert.False(Directory.Exists(Path.Combine(output, "Outer")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(output))
|
||||
Directory.Delete(output, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamAtOffset_WrapperFactoryUDF()
|
||||
{
|
||||
var stream = new MemoryStream([.. new byte[3 * UDFImageBuilder.SectorSize], .. UDFImageBuilder.Build()]);
|
||||
stream.Position = 3 * UDFImageBuilder.SectorSize;
|
||||
|
||||
var wrapper = WrapperFactory.CreateDiscImageWrapper(stream);
|
||||
Assert.IsType<UDF>(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
SabreTools.Wrappers/UDF.Extraction.cs
Normal file
172
SabreTools.Wrappers/UDF.Extraction.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Extensions;
|
||||
using SabreTools.Data.Models.UDF;
|
||||
using SabreTools.IO.Extensions;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class UDF : IExtractable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
// If we have no file set or directories, there is nothing to extract
|
||||
if (FileSetDescriptor is null || Directories.Count == 0)
|
||||
return true;
|
||||
|
||||
long root = FileSetDescriptor.RootDirectoryICB.ExtentLocation.GetKey();
|
||||
var visited = new HashSet<long> { root };
|
||||
return ExtractDirectory(root, Path.GetFullPath(outputDirectory), visited, includeDebug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract all files and directories within a directory
|
||||
/// </summary>
|
||||
private bool ExtractDirectory(long key, string outputPath, HashSet<long> visited, bool includeDebug)
|
||||
{
|
||||
if (!Directories.TryGetValue(key, out var fids))
|
||||
return false;
|
||||
|
||||
bool succeeded = true;
|
||||
foreach (var fid in fids)
|
||||
{
|
||||
// Skip deleted entries and the parent directory
|
||||
if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0)
|
||||
continue;
|
||||
|
||||
string? name = fid.FileIdentifier.DecodeDCharacters();
|
||||
if (!IsSafeName(name))
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"Skipping entry with unsafe name in {outputPath}");
|
||||
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
long childKey = fid.ICB.ExtentLocation.GetKey();
|
||||
if (!FileEntries.TryGetValue(childKey, out var entry))
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"File Entry missing, cannot extract: {name}");
|
||||
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
string childPath = Path.Combine(outputPath, name!);
|
||||
if (entry.ICBTag.FileType == FileType.DIRECTORY)
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"Extracting to directory: {childPath}");
|
||||
if (!Directory.Exists(childPath))
|
||||
Directory.CreateDirectory(childPath);
|
||||
|
||||
// Directories reachable twice (hard links, loops) are extracted once
|
||||
if (visited.Add(childKey))
|
||||
succeeded &= ExtractDirectory(childKey, childPath, visited, includeDebug);
|
||||
}
|
||||
else
|
||||
{
|
||||
succeeded &= ExtractFile(childKey, entry, childPath, includeDebug);
|
||||
}
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract a file's data
|
||||
/// </summary>
|
||||
private bool ExtractFile(long key, FileEntry entry, string filePath, bool includeDebug)
|
||||
{
|
||||
if (File.Exists(filePath) || Directory.Exists(filePath))
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"File/Folder already exists, cannot extract: {filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (includeDebug) Console.WriteLine($"Extracting: {filePath}");
|
||||
|
||||
long size = (long)entry.InformationLength;
|
||||
if (entry.ICBTag.GetAllocationDescriptorType() == AllocationDescriptorType.EMBEDDED)
|
||||
{
|
||||
using var embedded = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
embedded.Write(entry.AllocationDescriptors, 0, (int)size);
|
||||
return true;
|
||||
}
|
||||
|
||||
var descriptors = Model.GetAllocationDescriptors(key, entry);
|
||||
if (descriptors is null)
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"Allocation descriptors not supported or incomplete: {filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int chunkSize = 2048 * 1024;
|
||||
using var fs = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
long written = 0;
|
||||
foreach (var ad in descriptors)
|
||||
{
|
||||
if (written >= size)
|
||||
break;
|
||||
|
||||
long length = Math.Min(UDFExtensions.GetExtentLength(ad.ExtentLength), size - written);
|
||||
bool recorded = UDFExtensions.GetExtentType(ad.ExtentLength) == ExtentType.RECORDED_AND_ALLOCATED;
|
||||
|
||||
long sector = 0;
|
||||
if (recorded && !Model.TryGetSector(ad.ExtentLocation.GetKey(), out sector))
|
||||
return false;
|
||||
|
||||
// Make sure the extent won't EOF
|
||||
if (recorded && (sector * Constants.SectorSize) + length > _dataSource.Length)
|
||||
return false;
|
||||
|
||||
long done = 0;
|
||||
while (done < length)
|
||||
{
|
||||
int count = (int)Math.Min(chunkSize, length - done);
|
||||
byte[] buffer;
|
||||
if (recorded)
|
||||
{
|
||||
lock (_dataSourceLock)
|
||||
{
|
||||
_dataSource.SeekIfPossible((sector * Constants.SectorSize) + done, SeekOrigin.Begin);
|
||||
buffer = _dataSource.ReadBytes(count);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extents that are not recorded read as zeros
|
||||
buffer = new byte[count];
|
||||
}
|
||||
|
||||
fs.Write(buffer, 0, count);
|
||||
done += count;
|
||||
}
|
||||
|
||||
written += length;
|
||||
}
|
||||
|
||||
return written == size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that a file identifier can be used as a single path component
|
||||
/// </summary>
|
||||
private static bool IsSafeName(string? name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || name == "." || name == "..")
|
||||
return false;
|
||||
|
||||
char[] invalid = Path.GetInvalidFileNameChars();
|
||||
foreach (char ch in name!)
|
||||
{
|
||||
if (ch == '/' || ch == '\\' || ch == '\0' || Array.Exists(invalid, x => x == ch))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
296
SabreTools.Wrappers/UDF.Printing.cs
Normal file
296
SabreTools.Wrappers/UDF.Printing.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Extensions;
|
||||
using SabreTools.Data.Models.UDF;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class UDF : IPrintable
|
||||
{
|
||||
#if NETCOREAPP
|
||||
/// <inheritdoc/>
|
||||
public string ExportJSON(bool recursive) => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
|
||||
#else
|
||||
/// <inheritdoc/>
|
||||
public string ExportJSON(bool recursive) => Newtonsoft.Json.JsonConvert.SerializeObject(Model, _jsonSerializerOptions);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintInformation(StringBuilder builder, bool recursive)
|
||||
{
|
||||
builder.AppendLine("UDF Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
builder.AppendLine();
|
||||
|
||||
Print(builder, Model.VolumeRecognitionSequence);
|
||||
Print(builder, Model.AnchorVolumeDescriptorPointer);
|
||||
Print(builder, Model.VolumeDescriptorSequence);
|
||||
Print(builder, Model.FileSetDescriptor);
|
||||
|
||||
if (Model.FileSetDescriptor is not null)
|
||||
{
|
||||
builder.AppendLine(" Directory Hierarchy:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
long root = Model.FileSetDescriptor.RootDirectoryICB.ExtentLocation.GetKey();
|
||||
PrintDirectory(builder, root, "\\", new HashSet<long> { root });
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, VolumeStructureDescriptor[] sequence)
|
||||
{
|
||||
builder.AppendLine(" Volume Recognition Sequence:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (sequence.Length == 0)
|
||||
{
|
||||
builder.AppendLine(" No volume structure descriptors");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sequence.Length; i++)
|
||||
{
|
||||
var vsd = sequence[i];
|
||||
builder.AppendLine($" Volume Structure Descriptor {i}:");
|
||||
builder.AppendLine(vsd.StructureType, " Structure Type");
|
||||
builder.AppendLine(Encoding.ASCII.GetString(vsd.StandardIdentifier), " Standard Identifier");
|
||||
builder.AppendLine(vsd.StructureVersion, " Structure Version");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, AnchorVolumeDescriptorPointer? anchor)
|
||||
{
|
||||
builder.AppendLine(" Anchor Volume Descriptor Pointer:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (anchor is null)
|
||||
{
|
||||
builder.AppendLine(" No anchor volume descriptor pointer");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
Print(builder, anchor.DescriptorTag, " ");
|
||||
Print(builder, anchor.MainVolumeDescriptorSequenceExtent, " Main Volume Descriptor Sequence");
|
||||
Print(builder, anchor.ReserveVolumeDescriptorSequenceExtent, " Reserve Volume Descriptor Sequence");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, Descriptor[] sequence)
|
||||
{
|
||||
builder.AppendLine(" Volume Descriptor Sequence:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (sequence.Length == 0)
|
||||
{
|
||||
builder.AppendLine(" No volume descriptors");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var descriptor in sequence)
|
||||
{
|
||||
builder.AppendLine($" {descriptor.DescriptorTag.TagIdentifier} (Sector {descriptor.DescriptorTag.TagLocation}):");
|
||||
Print(builder, descriptor.DescriptorTag, " ");
|
||||
switch (descriptor)
|
||||
{
|
||||
case PrimaryVolumeDescriptor pvd:
|
||||
builder.AppendLine(pvd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number");
|
||||
builder.AppendLine(pvd.PrimaryVolumeDescriptorNumber, " Primary Volume Descriptor Number");
|
||||
builder.AppendLine(pvd.VolumeIdentifier.DecodeDString(), " Volume Identifier");
|
||||
builder.AppendLine(pvd.VolumeSequenceNumber, " Volume Sequence Number");
|
||||
builder.AppendLine(pvd.MaximumVolumeSequenceNumber, " Maximum Volume Sequence Number");
|
||||
builder.AppendLine(pvd.InterchangeLevel, " Interchange Level");
|
||||
builder.AppendLine(pvd.MaximumInterchangeLevel, " Maximum Interchange Level");
|
||||
builder.AppendLine(pvd.VolumeSetIdentifier.DecodeDString(), " Volume Set Identifier");
|
||||
Print(builder, pvd.ApplicationIdentifier, " Application Identifier");
|
||||
builder.AppendLine(Format(pvd.RecordingDateAndTime), " Recording Date and Time");
|
||||
Print(builder, pvd.ImplementationIdentifier, " Implementation Identifier");
|
||||
builder.AppendLine(pvd.Flags, " Flags");
|
||||
break;
|
||||
|
||||
case PartitionDescriptor pd:
|
||||
builder.AppendLine(pd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number");
|
||||
builder.AppendLine(pd.PartitionFlags, " Partition Flags");
|
||||
builder.AppendLine(pd.PartitionNumber, " Partition Number");
|
||||
Print(builder, pd.PartitionContents, " Partition Contents");
|
||||
builder.AppendLine(pd.AccessType, " Access Type");
|
||||
builder.AppendLine(pd.PartitionStartingLocation, " Partition Starting Location");
|
||||
builder.AppendLine(pd.PartitionLength, " Partition Length");
|
||||
Print(builder, pd.ImplementationIdentifier, " Implementation Identifier");
|
||||
break;
|
||||
|
||||
case LogicalVolumeDescriptor lvd:
|
||||
builder.AppendLine(lvd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number");
|
||||
builder.AppendLine(lvd.LogicalVolumeIdentifier.DecodeDString(), " Logical Volume Identifier");
|
||||
builder.AppendLine(lvd.LogicalBlockSize, " Logical Block Size");
|
||||
Print(builder, lvd.DomainIdentifier, " Domain Identifier");
|
||||
Print(builder, lvd.LogicalVolumeContentsUse, " File Set Descriptor Extent");
|
||||
builder.AppendLine(lvd.MapTableLength, " Map Table Length");
|
||||
builder.AppendLine(lvd.NumberOfPartitionMaps, " Number of Partition Maps");
|
||||
Print(builder, lvd.ImplementationIdentifier, " Implementation Identifier");
|
||||
Print(builder, lvd.IntegritySequenceExtent, " Integrity Sequence Extent");
|
||||
for (int i = 0; i < lvd.PartitionMaps.Length; i++)
|
||||
{
|
||||
Print(builder, lvd.PartitionMaps[i], i);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case VolumeDescriptorPointer vdp:
|
||||
builder.AppendLine(vdp.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number");
|
||||
Print(builder, vdp.NextVolumeDescriptorSequenceExtent, " Next Volume Descriptor Sequence");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Other descriptors are not parsed further
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, PartitionMap map, int index)
|
||||
{
|
||||
builder.AppendLine($" Partition Map {index} (Partition Reference Number {index}):");
|
||||
builder.AppendLine(map.PartitionMapType, " Partition Map Type");
|
||||
builder.AppendLine(map.PartitionMapLength, " Partition Map Length");
|
||||
switch (map)
|
||||
{
|
||||
case Type1PartitionMap type1:
|
||||
builder.AppendLine(type1.VolumeSequenceNumber, " Volume Sequence Number");
|
||||
builder.AppendLine(type1.PartitionNumber, " Partition Number");
|
||||
break;
|
||||
|
||||
case MetadataPartitionMap metadata:
|
||||
Print(builder, metadata.PartitionTypeIdentifier, " Partition Type Identifier");
|
||||
builder.AppendLine(metadata.VolumeSequenceNumber, " Volume Sequence Number");
|
||||
builder.AppendLine(metadata.PartitionNumber, " Partition Number");
|
||||
builder.AppendLine(metadata.MetadataFileLocation, " Metadata File Location");
|
||||
builder.AppendLine(metadata.MetadataMirrorFileLocation, " Metadata Mirror File Location");
|
||||
builder.AppendLine(metadata.MetadataBitmapFileLocation, " Metadata Bitmap File Location");
|
||||
builder.AppendLine(metadata.AllocationUnitSize, " Allocation Unit Size");
|
||||
builder.AppendLine(metadata.AlignmentUnitSize, " Alignment Unit Size");
|
||||
builder.AppendLine(metadata.Flags, " Flags");
|
||||
break;
|
||||
|
||||
case Type2PartitionMap type2:
|
||||
Print(builder, type2.PartitionTypeIdentifier, " Partition Type Identifier");
|
||||
builder.AppendLine(type2.VolumeSequenceNumber, " Volume Sequence Number");
|
||||
builder.AppendLine(type2.PartitionNumber, " Partition Number");
|
||||
builder.AppendLine(type2.PartitionTypeData, " Partition Type Data");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown partition map types are not parsed further
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, FileSetDescriptor? fsd)
|
||||
{
|
||||
builder.AppendLine(" File Set Descriptor:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (fsd is null)
|
||||
{
|
||||
builder.AppendLine(" No file set descriptor");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
Print(builder, fsd.DescriptorTag, " ");
|
||||
builder.AppendLine(Format(fsd.RecordingDateAndTime), " Recording Date and Time");
|
||||
builder.AppendLine(fsd.InterchangeLevel, " Interchange Level");
|
||||
builder.AppendLine(fsd.MaximumInterchangeLevel, " Maximum Interchange Level");
|
||||
builder.AppendLine(fsd.FileSetNumber, " File Set Number");
|
||||
builder.AppendLine(fsd.FileSetDescriptorNumber, " File Set Descriptor Number");
|
||||
builder.AppendLine(fsd.LogicalVolumeIdentifier.DecodeDString(), " Logical Volume Identifier");
|
||||
builder.AppendLine(fsd.FileSetIdentifier.DecodeDString(), " File Set Identifier");
|
||||
builder.AppendLine(fsd.CopyrightFileIdentifier.DecodeDString(), " Copyright File Identifier");
|
||||
builder.AppendLine(fsd.AbstractFileIdentifier.DecodeDString(), " Abstract File Identifier");
|
||||
Print(builder, fsd.RootDirectoryICB, " Root Directory ICB");
|
||||
Print(builder, fsd.DomainIdentifier, " Domain Identifier");
|
||||
Print(builder, fsd.NextExtent, " Next Extent");
|
||||
Print(builder, fsd.SystemStreamDirectoryICB, " System Stream Directory ICB");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private void PrintDirectory(StringBuilder builder, long key, string path, HashSet<long> visited)
|
||||
{
|
||||
if (!Model.Directories.TryGetValue(key, out var fids))
|
||||
{
|
||||
builder.AppendLine($" {path} (directory not read)");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fid in fids)
|
||||
{
|
||||
if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0)
|
||||
continue;
|
||||
|
||||
string name = fid.FileIdentifier.DecodeDCharacters() ?? "(unknown encoding)";
|
||||
string childPath = path + name;
|
||||
long childKey = fid.ICB.ExtentLocation.GetKey();
|
||||
string hidden = (fid.FileCharacteristics & FileCharacteristics.EXISTENCE) != 0 ? ", hidden" : string.Empty;
|
||||
if (!Model.FileEntries.TryGetValue(childKey, out var entry))
|
||||
{
|
||||
builder.AppendLine($" {childPath} (File Entry not read{hidden})");
|
||||
continue;
|
||||
}
|
||||
|
||||
string location = $"partition {fid.ICB.ExtentLocation.PartitionReferenceNumber}, block {fid.ICB.ExtentLocation.LogicalBlockNumber}";
|
||||
if (entry.ICBTag.FileType == FileType.DIRECTORY)
|
||||
{
|
||||
builder.AppendLine($" {childPath}\\ ({location}{hidden})");
|
||||
if (visited.Add(childKey))
|
||||
PrintDirectory(builder, childKey, $"{childPath}\\", visited);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendLine($" {childPath} ({entry.InformationLength} bytes, {entry.ICBTag.FileType}, {location}{hidden})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, DescriptorTag tag, string indent)
|
||||
{
|
||||
builder.AppendLine((ushort)tag.TagIdentifier, $"{indent}Tag Identifier");
|
||||
builder.AppendLine(tag.DescriptorVersion, $"{indent}Descriptor Version");
|
||||
builder.AppendLine(tag.TagSerialNumber, $"{indent}Tag Serial Number");
|
||||
builder.AppendLine(tag.DescriptorCRCLength, $"{indent}Descriptor CRC Length");
|
||||
builder.AppendLine(tag.TagLocation, $"{indent}Tag Location");
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, ExtentDescriptor extent, string label)
|
||||
=> builder.AppendLine($"sector {extent.ExtentLocation}, {extent.ExtentLength} bytes", label);
|
||||
|
||||
private static void Print(StringBuilder builder, LongAllocationDescriptor ad, string label)
|
||||
{
|
||||
uint length = UDFExtensions.GetExtentLength(ad.ExtentLength);
|
||||
builder.AppendLine($"partition {ad.ExtentLocation.PartitionReferenceNumber}, block {ad.ExtentLocation.LogicalBlockNumber}, {length} bytes", label);
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, EntityIdentifier identifier, string label)
|
||||
{
|
||||
builder.AppendLine(identifier.Identifier, label, Encoding.ASCII);
|
||||
builder.AppendLine(identifier.Flags, $"{label} Flags");
|
||||
builder.AppendLine(identifier.IdentifierSuffix, $"{label} Suffix");
|
||||
}
|
||||
|
||||
private static string Format(Timestamp timestamp)
|
||||
{
|
||||
if (timestamp.Year == 0 && timestamp.Month == 0 && timestamp.Day == 0)
|
||||
return "Not specified";
|
||||
|
||||
// Lower 12 bits: signed offset from UTC in minutes, -2047 = not specified
|
||||
int offset = timestamp.TypeAndTimezone & 0x0FFF;
|
||||
if ((offset & 0x0800) != 0)
|
||||
offset -= 0x1000;
|
||||
|
||||
string zone = offset == -2047 ? string.Empty : $" UTC{(offset < 0 ? "-" : "+")}{System.Math.Abs(offset) / 60:D2}:{System.Math.Abs(offset) % 60:D2}";
|
||||
return $"{timestamp.Year:D4}-{timestamp.Month:D2}-{timestamp.Day:D2} {timestamp.Hour:D2}:{timestamp.Minute:D2}:{timestamp.Second:D2}.{timestamp.Centiseconds:D2}{zone}";
|
||||
}
|
||||
}
|
||||
}
|
||||
112
SabreTools.Wrappers/UDF.cs
Normal file
112
SabreTools.Wrappers/UDF.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.UDF;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class UDF : WrapperBase<Volume>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "Universal Disk Format (UDF) Volume";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="Volume.VolumeRecognitionSequence"/>
|
||||
public VolumeStructureDescriptor[] VolumeRecognitionSequence => Model.VolumeRecognitionSequence;
|
||||
|
||||
/// <inheritdoc cref="Volume.VolumeDescriptorSequence"/>
|
||||
public Descriptor[] VolumeDescriptorSequence => Model.VolumeDescriptorSequence;
|
||||
|
||||
/// <inheritdoc cref="Volume.FileSetDescriptor"/>
|
||||
public FileSetDescriptor? FileSetDescriptor => Model.FileSetDescriptor;
|
||||
|
||||
/// <inheritdoc cref="Volume.FileEntries"/>
|
||||
public Dictionary<long, FileEntry> FileEntries => Model.FileEntries;
|
||||
|
||||
/// <inheritdoc cref="Volume.Directories"/>
|
||||
public Dictionary<long, FileIdentifierDescriptor[]> Directories => Model.Directories;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UDF(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create a UDF Volume from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the UDF Volume</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>A UDF Volume wrapper on success, null on failure</returns>
|
||||
public static UDF? Create(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || data.Length == 0)
|
||||
return null;
|
||||
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Create a memory stream and use that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return Create(dataStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a UDF Volume from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the UDF Volume</param>
|
||||
/// <returns>A UDF Volume wrapper on success, null on failure</returns>
|
||||
public static UDF? Create(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || !data.CanRead || !data.CanSeek)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
// The reader addresses logical sectors from the start of the stream, so it reads a view that starts here
|
||||
var model = new Serialization.Readers.UDF().Deserialize(new ReadOnlyViewStream(data, currentOffset));
|
||||
if (model is null)
|
||||
return null;
|
||||
|
||||
return new UDF(model, data, currentOffset);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,14 @@ namespace SabreTools.Wrappers
|
||||
// Reset position in stream
|
||||
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to get a UDF wrapper (discs without an ISO9660 file system)
|
||||
var udfWrapper = UDF.Create(stream);
|
||||
if (udfWrapper is not null)
|
||||
return udfWrapper;
|
||||
|
||||
// Reset position in stream
|
||||
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to get a 3DO / M2 disc image wrapper
|
||||
var operaFSWrapper = OperaFS.Create(stream);
|
||||
if (operaFSWrapper is not null)
|
||||
|
||||
Reference in New Issue
Block a user