mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-11-15 17:00:38 +00:00
* 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>
173 lines
6.4 KiB
C#
173 lines
6.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|