using System; using System.IO; using System.Text; using SabreTools.Data.Models.CFB; using SabreTools.Numerics.Extensions; namespace SabreTools.Wrappers { public partial class CFB : IExtractable { /// public bool Extract(string outputDirectory, bool includeDebug) { // If we have no files if (DirectoryEntries is null || DirectoryEntries.Length == 0) return false; // Loop through and extract all directory entries to the output bool allExtracted = true; for (int i = 0; i < DirectoryEntries.Length; i++) { allExtracted &= ExtractEntry(i, outputDirectory, includeDebug); } return allExtracted; } /// /// Extract a file from the CFB to an output directory by index /// /// Entry index to extract /// Output directory to write to /// True to include debug data, false otherwise /// True if the file extracted, false otherwise public bool ExtractEntry(int index, string outputDirectory, bool includeDebug) { // If we have no entries if (DirectoryEntries is null || DirectoryEntries.Length == 0) return false; // If we have an invalid index if (index < 0 || index >= DirectoryEntries.Length) return false; // Get the entry information var entry = DirectoryEntries[index]; if (entry is null) return false; // Only try to extract stream objects if (entry.ObjectType != ObjectType.StreamObject) return true; // Get the entry data byte[]? data = GetDirectoryEntryData(entry); if (data is null) return false; // If we have an invalid output directory if (string.IsNullOrEmpty(outputDirectory)) return false; // Ensure the output filename is trimmed string filename = entry.Name.Length == 0 ? $"entry{index}" : entry.Name; byte[] nameBytes = Encoding.UTF8.GetBytes(filename); if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80) filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3); foreach (char c in Path.GetInvalidFileNameChars()) { filename = filename.Replace(c, '_'); } // Ensure directory separators are consistent if (Path.DirectorySeparatorChar == '\\') filename = filename.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') filename = filename.Replace('\\', '/'); // Ensure the full output directory exists filename = Path.Combine(outputDirectory, filename); var directoryName = Path.GetDirectoryName(filename); if (directoryName is not null && !Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); // Try to write the data try { // Open the output file for writing using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); fs.Write(data); fs.Flush(); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); return false; } return true; } /// /// Read the entry data for a single directory entry, if possible /// /// Entry to try to retrieve data for /// Byte array representing the entry data on success, null otherwise private byte[]? GetDirectoryEntryData(DirectoryEntry entry) { // If the CFB is invalid if (Header is null) return null; // Only try to extract stream objects if (entry.ObjectType != ObjectType.StreamObject) return null; // Determine which FAT is being used bool miniFat = entry.StreamSize < Header.MiniStreamCutoffSize; // Get the chain data var chain = miniFat ? GetMiniFATSectorChainData((SectorNumber)entry.StartingSectorLocation) : GetFATSectorChainData((SectorNumber)entry.StartingSectorLocation); if (chain is null) return null; // Return only the proper amount of data byte[] data = new byte[entry.StreamSize]; Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize)); return data; } } }