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 { /// 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 { root }; return ExtractDirectory(root, Path.GetFullPath(outputDirectory), visited, includeDebug); } /// /// Extract all files and directories within a directory /// private bool ExtractDirectory(long key, string outputPath, HashSet 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; } /// /// Extract a file's data /// 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; } /// /// Check that a file identifier can be used as a single path component /// 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; } } }