Files
SabreTools.Serialization/SabreTools.Wrappers/XDVDFS.Extraction.cs
2026-03-29 21:54:16 -04:00

145 lines
6.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Models.XDVDFS;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
public partial class XDVDFS : IExtractable
{
#region Extraction State
/// <summary>
/// List of extracted files by their sector offset
/// </summary>
private readonly Dictionary<uint, uint> extractedFiles = [];
#endregion
/// <inheritdoc/>
public virtual bool Extract(string outputDirectory, bool includeDebug)
{
// Clear the extraction state
extractedFiles.Clear();
// Cache the current offset
long initialOffset = _dataSource.Position;
// Extract files from all directories from root directory
return ExtractDescriptor(outputDirectory, includeDebug, initialOffset, VolumeDescriptor.RootOffset);
}
/// <summary>
/// Extracts all directory records recursively from directory descriptor
/// </summary>
public bool ExtractDescriptor(string outputDirectory, bool includeDebug, long initialOffset, uint sectorNumber)
{
// If no descriptor exists at that sector, we cannot extract from it
if (!DirectoryDescriptors.ContainsKey(sectorNumber))
return false;
bool allExtracted = true;
// Extract directory records within directory descriptor
foreach (var dr in DirectoryDescriptors[sectorNumber].DirectoryRecords)
{
// Skip invalid records
if (dr.FilenameLength == 0 || dr.Filename is null)
{
if (includeDebug) Console.WriteLine($"Empty filename in directory record at sector {sectorNumber}");
continue;
}
string outputPath = Path.Combine(outputDirectory, Encoding.UTF8.GetString(dr.Filename));
// If record is a directory, create it and extract child records
if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY)
{
if (!string.IsNullOrEmpty(outputPath) && !Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
allExtracted |= ExtractDescriptor(outputPath, includeDebug, initialOffset, dr.ExtentOffset);
}
else
{
// Skip invalid file size
if (dr.ExtentSize == 0)
{
if (includeDebug) Console.WriteLine($"Zero file size for file {dr.Filename} at sector {dr.ExtentOffset}");
continue;
}
// Skip invalid file location
if ((((long)dr.ExtentOffset) * Constants.SectorSize) + dr.ExtentSize > _dataSource.Length)
{
if (includeDebug) Console.WriteLine($"Invalid file location for file {dr.Filename} at sector {dr.ExtentOffset}");
continue;
}
// Check that the file hasn't been extracted already
if (extractedFiles.ContainsKey(dr.ExtentOffset))
{
if (includeDebug) Console.WriteLine($"File {dr.Filename} at sector {dr.ExtentOffset} already extracted");
continue;
}
// Read and extract the file extent
const uint chunkSize = 2048 * 1024;
lock (_dataSourceLock)
{
long fileOffset = ((long)dr.ExtentOffset) * Constants.SectorSize;
_dataSource.SeekIfPossible(initialOffset + fileOffset, SeekOrigin.Begin);
// Get the length, and make sure it won't EOF
uint length = dr.ExtentSize;
if (length > _dataSource.Length - _dataSource.Position)
return false;
// Write the output file
if (includeDebug) Console.WriteLine($"Extracting: {outputPath}");
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
while (length > 0)
{
int bytesToRead = (int)Math.Min(length, chunkSize);
byte[] buffer = _dataSource.ReadBytes(bytesToRead);
fs.Write(buffer, 0, bytesToRead);
fs.Flush();
length -= (uint)bytesToRead;
}
}
// Mark the file as extracted
extractedFiles.Add(dr.ExtentOffset, dr.ExtentSize);
// Don't set any file attributes if file is normal
if ((dr.FileFlags & FileFlags.NORMAL) == FileFlags.NORMAL)
continue;
// Copy over hidden flag
if ((dr.FileFlags & FileFlags.HIDDEN) == FileFlags.HIDDEN)
File.SetAttributes(outputPath, FileAttributes.Hidden);
// Copy over read-only flag
if ((dr.FileFlags & FileFlags.READ_ONLY) == FileFlags.READ_ONLY)
File.SetAttributes(outputPath, FileAttributes.ReadOnly);
// Copy over system flag
if ((dr.FileFlags & FileFlags.SYSTEM) == FileFlags.SYSTEM)
File.SetAttributes(outputPath, FileAttributes.System);
// Copy over archive flag
if ((dr.FileFlags & FileFlags.ARCHIVE) == FileFlags.ARCHIVE)
File.SetAttributes(outputPath, FileAttributes.Archive);
}
}
return allExtracted;
}
}
}