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 /// /// List of extracted files by their sector offset /// private readonly Dictionary extractedFiles = []; #endregion /// 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); } /// /// Extracts all directory records recursively from directory descriptor /// 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; } } }