From 85248b0135f4d624893428a3936f93dd81da422b Mon Sep 17 00:00:00 2001 From: Deterous <138427222+Deterous@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:00:10 +0900 Subject: [PATCH] ISO 9660 Extraction (#31) * ISO9660 Extraction * Fix build * Decode strings in Printer * Double comment --- .../Models/ISO9660/Volume.cs | 3 +- SabreTools.Serialization/Readers/ISO9660.cs | 151 +++++++++-------- .../Wrappers/ISO9660.Extraction.cs | 154 +++++++++++++++++- .../Wrappers/ISO9660.Printing.cs | 94 +++++++---- SabreTools.Serialization/Wrappers/ISO9660.cs | 5 +- .../Wrappers/WrapperBase.cs | 2 +- 6 files changed, 301 insertions(+), 108 deletions(-) diff --git a/SabreTools.Serialization/Models/ISO9660/Volume.cs b/SabreTools.Serialization/Models/ISO9660/Volume.cs index a3094a18..666456c7 100644 --- a/SabreTools.Serialization/Models/ISO9660/Volume.cs +++ b/SabreTools.Serialization/Models/ISO9660/Volume.cs @@ -42,8 +42,9 @@ namespace SabreTools.Data.Models.ISO9660 /// /// Map of sector numbers and the directory at that sector number /// Each Directory contains child directory and file descriptors + /// Note: FileExtent is the base class for DirectoryExtent /// - public Dictionary DirectoryDescriptors { get; set; } + public Dictionary DirectoryDescriptors { get; set; } #endregion } diff --git a/SabreTools.Serialization/Readers/ISO9660.cs b/SabreTools.Serialization/Readers/ISO9660.cs index 36281bee..2af9e6fc 100644 --- a/SabreTools.Serialization/Readers/ISO9660.cs +++ b/SabreTools.Serialization/Readers/ISO9660.cs @@ -577,10 +577,10 @@ namespace SabreTools.Serialization.Readers /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Set of volume descriptors for a volume - /// Filled Dictionary of int to Directory on success, null on error - public static Dictionary? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet) + /// Filled Dictionary of int to FileExtent on success, null on error + public static Dictionary? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet) { - var directories = new Dictionary(); + var directories = new Dictionary(); foreach (VolumeDescriptor vd in vdSet) { if (vd is not BaseVolumeDescriptor bvd) @@ -617,85 +617,104 @@ namespace SabreTools.Serialization.Readers /// Number of bytes in a logical block (usually 2048) /// Directory record pointing to the directory extent /// True if the Big Endian extent location/length should be parsed - /// Filled Dictionary of int to Directory on success, null on error - public static Dictionary? ParseDirectory(Stream data, short sectorLength, short blockLength, DirectoryRecord dr, bool bigEndian) + /// Filled Dictionary of int to FileExtent on success, null on error + public static Dictionary? ParseDirectory(Stream data, short sectorLength, short blockLength, DirectoryRecord dr, bool bigEndian) { - // Do not parse file extents -#if NET20 || NET35 - if ((dr.FileFlags & FileFlags.DIRECTORY) == 0) - return null; -#else - if (!dr.FileFlags.HasFlag(FileFlags.DIRECTORY)) - return null; -#endif - + var directories = new Dictionary(); int blocksPerSector = sectorLength / blockLength; - // Validate both-endian extent location - // TODO: Validate both-endian extent length (use the longest / non-zero one) - int extentLocation = bigEndian ? dr.ExtentLocation.LittleEndian : dr.ExtentLocation.BigEndian; - int extentLength = bigEndian ? dr.ExtentLength.LittleEndian : dr.ExtentLength.BigEndian; + // Use provided extent endinanness + int extentLocation = bigEndian ? dr.ExtentLocation.BigEndian : dr.ExtentLocation.LittleEndian; + int extentLength = bigEndian ? dr.ExtentLength.BigEndian : dr.ExtentLength.LittleEndian; - // Validate extent within data stream - if ((extentLocation * blockLength) + extentLength > data.Length) + // Deal with extent length ambiguity + if (!dr.ExtentLength.IsValid) + { + // If provided extent length is invalid, use the other value + if (extentLength <= 0 || (extentLocation * blockLength) + extentLength > data.Length) + extentLength = bigEndian ? dr.ExtentLength.LittleEndian : dr.ExtentLength.BigEndian; + } + + // Validate extent length + if (extentLength <= 0 || (extentLocation * blockLength) + extentLength > data.Length) return null; // Move stream to directory location data.SeekIfPossible(extentLocation * blockLength, SeekOrigin.Begin); - // Read all directory records in this directory - var records = new List(); - int pos = 0; - while (pos < extentLength) + // Check if the current extent is a directory +#if NET20 || NET35 + if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY) +#else + if (dr.FileFlags.HasFlag(FileFlags.DIRECTORY)) +#endif { - // Peek next byte to check whether the next record length is not greater than the end of the dir extent - var recordLength = data.PeekByteValue(); - - // If record length of 0x00, next record begins in next sector - if (recordLength == 0) + // Read all directory records in this directory + var records = new List(); + int pos = 0; + while (pos < extentLength) { - int paddingLength = sectorLength - (pos % sectorLength); - pos += paddingLength; - _ = data.ReadBytes(paddingLength); - continue; + // Peek next byte to check whether the next record length is not greater than the end of the dir extent + var recordLength = data.PeekByteValue(); + + // If record length of 0x00, next record begins in next sector + if (recordLength == 0) + { + int paddingLength = sectorLength - (pos % sectorLength); + pos += paddingLength; + _ = data.ReadBytes(paddingLength); + continue; + } + + // Ensure record will end in this extent + // TODO: Smartly detect record length for invalid record lengths + pos += recordLength; + if (pos > extentLength) + break; + + // Get the next directory record + var directoryRecord = ParseDirectoryRecord(data, false); + records.Add(directoryRecord); } - // Ensure record will end in this extent - // TODO: Smartly detect record length for invalid record lengths - pos += recordLength; - if (pos > extentLength) - break; + // Add current directory to dictionary + var currentDirectory = new DirectoryExtent(); + currentDirectory.DirectoryRecords = [.. records]; + directories.Add(extentLocation * blocksPerSector, currentDirectory); - // Get the next directory record - var directoryRecord = ParseDirectoryRecord(data, false); - records.Add(directoryRecord); + // Add all child directories to dictionary recursively + foreach (var record in records) + { + // Don't traverse to parent or self + if (record.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || record.FileIdentifier.EqualsExactly(Constants.ParentDirectory)) + continue; + + // Recursively parse child directory + int sectorNum = record.ExtentLocation * blocksPerSector; + var dir = ParseDirectory(data, sectorLength, blockLength, record, false); + if (dir == null) + continue; + + // Add new directories to dictionary + foreach (var kvp in dir) + { + if (!directories.ContainsKey(kvp.Key)) + directories.Add(kvp.Key, kvp.Value); + } + } } - - // Add current directory to dictionary - var directories = new Dictionary(); - var currentDirectory = new DirectoryExtent(); - currentDirectory.DirectoryRecords = [.. records]; - directories.Add(extentLocation * blocksPerSector, currentDirectory); - - // Add all child directories to dictionary recursively - foreach (var record in records) + else { - // Don't traverse to parent or self - if (record.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || record.FileIdentifier.EqualsExactly(Constants.ParentDirectory)) - continue; - - // Recursively parse child directory - int sectorNum = record.ExtentLocation * blocksPerSector; - var dir = ParseDirectory(data, sectorLength, blockLength, record, false); - if (dir == null) - continue; - - // Add new directories to dictionary - foreach (var kvp in dir) - { - if (!directories.ContainsKey(kvp.Key)) - directories.Add(kvp.Key, kvp.Value); - } + // TODO: Create ParseExtendedAttributeRecord() + // Extent is a file, parse the Extended Attribute Record + // var ear = ParseExtendedAttributeRecord(); + // if (ear != null) + // { + // var fileExtent = new FileExtent(); + // fileExtent.ExtendedAttributeRecord = ear; + // if (!directories.ContainsKey(extentLocation)) + // directories.Add(extentLocation, fileExtent); + // } } // If the extent location field is ambiguous, also parse the big-endian directory extent diff --git a/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs b/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs index 1df18731..7c2f1697 100644 --- a/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs @@ -1,6 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Text; using SabreTools.Data.Models.ISO9660; +using SabreTools.Data.Extensions; +using SabreTools.IO; +using SabreTools.IO.Extensions; namespace SabreTools.Serialization.Wrappers { @@ -9,15 +14,156 @@ namespace SabreTools.Serialization.Wrappers /// public bool Extract(string outputDirectory, bool includeDebug) { - // If we have no path tables or directory descriptors, there is nothing to extract - if (PathTableGroups.Length == 0 && DirectoryDescriptors.Count == 0) + // If we have no volume or directory descriptors, there is nothing to extract + if (VolumeDescriptorSet.Length == 0 || DirectoryDescriptors.Count == 0) return true; - bool allExtracted = false; + bool allExtracted = true; - // TODO: Extract all directories and file extents + // Determine and validate sector length, default to 2048 + short sectorLength = (short)(SystemArea.Length / 16); + if (sectorLength < 2048 || (sectorLength & (sectorLength - 1)) != 0) + sectorLength = 2048; + + // Keep track of extracted files according to their byte location + // Note: Using Dictionary instead of HashSet because .NET Framework doesn't support HashSet + var extractedFiles = new Dictionary(); + + // Loop through all Base Volume Descriptors to extract files from each directory hierarchy + // Note: This will prioritize the last volume descriptor directory hierarchies first (prioritises those filenames) + for (int i = VolumeDescriptorSet.Length - 1; i >= 0; i--) + { + if (VolumeDescriptorSet[i] is BaseVolumeDescriptor bvd) + { + var rootDir = bvd.RootDirectoryRecord; + + var blockLength = bvd.GetLogicalBlockSize(sectorLength); + + // TODO: Better encoding detection (EscapeSequences) + var encoding = Encoding.UTF8; + if (bvd is SupplementaryVolumeDescriptor svd) + encoding = Encoding.BigEndianUnicode; + + // Extract all files within root directory hierarchy + allExtracted &= ExtractExtent(rootDir.ExtentLocation.LittleEndian, extractedFiles, encoding, blockLength, outputDirectory, includeDebug); + // If Big Endian extent location differs from Little Endian extent location, also extract that directory hierarchy + if (!rootDir.ExtentLocation.IsValid) + { + if (includeDebug) Console.WriteLine($"Extracting from volume descriptor (big endian root dir location)"); + allExtracted &= ExtractExtent(rootDir.ExtentLocation.BigEndian, extractedFiles, encoding, blockLength, outputDirectory, includeDebug); + } + } + } return allExtracted; } + + /// + /// Extract all files from within a directory/file extent + /// + private bool ExtractExtent(int extentLocation, Dictionary extractedFiles, Encoding encoding, int blockLength, string outputDirectory, bool includeDebug) + { + // Check that directory exists in model + if (!DirectoryDescriptors.ContainsKey(extentLocation)) + return false; + + bool succeeded = true; + if (DirectoryDescriptors[extentLocation] is DirectoryExtent dir) + { + foreach (var dr in dir.DirectoryRecords) + { + // Recurse if record is directory + if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY) + { + // Don't recurse up or self + if (dr.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || dr.FileIdentifier.EqualsExactly(Constants.ParentDirectory)) + continue; + + // Append directory name + string outDirTemp = Path.Combine(outputDirectory, encoding.GetString(dr.FileIdentifier)); + if (includeDebug) Console.WriteLine($"Extracting to directory: {outDirTemp}"); + ExtractExtent(dr.ExtentLocation.LittleEndian, extractedFiles, encoding, blockLength, outDirTemp, includeDebug); + + // Also extract from BigEndian values if ambiguous + if (!dr.ExtentLocation.IsValid!) + { + ExtractExtent(dr.ExtentLocation.BigEndian, extractedFiles, encoding, blockLength, outDirTemp, includeDebug); + } + } + else + { + // Record is a file extent, extract file + succeeded &= ExtractFile(dr, extractedFiles, encoding, blockLength, false, outputDirectory, includeDebug); + // Also extract from BigEndian values if ambiguous + if (!dr.ExtentLocation.IsValid!) + { + succeeded &= ExtractFile(dr, extractedFiles, encoding, blockLength, true, outputDirectory, includeDebug); + } + } + + } + } + + return true; + } + + /// + /// Extract file pointed to by a directory record + /// + private bool ExtractFile(DirectoryRecord dr, Dictionary extractedFiles, Encoding encoding, int blockLength, bool bigEndian, string outputDirectory, bool includeDebug) + { + // Cannot extract file if it is a directory + if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY) + return false; + + int extentLocation = bigEndian ? dr.ExtentLocation.BigEndian : dr.ExtentLocation.LittleEndian; + int fileOffset = dr.ExtentLocation * blockLength; + + // Check that the file hasn't been extracted already + if (extractedFiles.ContainsKey(fileOffset)) + return true; + + const int chunkSize = 2048 * 1024; + lock (_dataSourceLock) + { + _dataSource.SeekIfPossible(fileOffset, SeekOrigin.Begin); + + // Get the length, and make sure it won't EOF + int length = dr.ExtentLength; + if (length > _dataSource.Length - _dataSource.Position) + return false; + + // TODO: Decode properly (Use VD's separator characters and encoding) + string filename = encoding.GetString(dr.FileIdentifier); + int index = filename.IndexOf(';'); + if (index > 0) + filename = filename.Substring(0, index); + + // Ensure the full output directory exists + filename = Path.Combine(outputDirectory, filename); + var directoryName = Path.GetDirectoryName(filename); + if (directoryName != null && !Directory.Exists(directoryName)) + Directory.CreateDirectory(directoryName); + + // Write the output file + if (includeDebug) Console.WriteLine($"Extracting: {filename}"); + using var fs = File.Open(filename, 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 -= bytesToRead; + } + + // Mark the file as extracted + extractedFiles.Add(fileOffset, dr.ExtentLength); + } + + return true; + } } } diff --git a/SabreTools.Serialization/Wrappers/ISO9660.Printing.cs b/SabreTools.Serialization/Wrappers/ISO9660.Printing.cs index 59864bd2..34487470 100644 --- a/SabreTools.Serialization/Wrappers/ISO9660.Printing.cs +++ b/SabreTools.Serialization/Wrappers/ISO9660.Printing.cs @@ -29,8 +29,11 @@ namespace SabreTools.Serialization.Wrappers builder.AppendLine(); Print(builder, Model.VolumeDescriptorSet); - Print(builder, Model.PathTableGroups); - Print(builder, Model.DirectoryDescriptors); + + // TODO: Parse the volume descriptors to print the Path Table Groups and Directory Descriptors with proper encoding + Encoding encoding = Encoding.UTF8; + Print(builder, Model.PathTableGroups, encoding); + Print(builder, Model.DirectoryDescriptors, encoding); } #region Volume Descriptors @@ -96,12 +99,16 @@ namespace SabreTools.Serialization.Wrappers builder.AppendLine(" Unidentified Base Volume Descriptor:"); builder.AppendLine(" -------------------------"); + // Default to UTF-8 deocding (note: Spec says PVD uses subset of ASCII, but UTF-8 is used here as it is a strict superset) + Encoding encoding = Encoding.UTF8; if (vd is PrimaryVolumeDescriptor pvd) { builder.AppendLine(pvd.UnusedByte, " Unused Byte"); } else if (vd is SupplementaryVolumeDescriptor svd) { + // Decode strings using UTF-16 BigEndian (note: Spec says SVD uses UCS-2, but UTF-16 is used here as it is a strict superset) + encoding = Encoding.BigEndianUnicode; builder.AppendLine(" Volume Flags:"); #if NET20 || NET35 builder.AppendLine((svd.VolumeFlags & VolumeFlags.UNREGISTERED_ESCAPE_SEQUENCES) != 0, " Unregistered Escape Sequences"); @@ -114,10 +121,10 @@ namespace SabreTools.Serialization.Wrappers builder.AppendLine("Zeroed", " Reserved Flags"); } - // TODO: Decode all byte arrays into strings (based on encoding above) + // TODO: Better string decoding (based on spec, EscapeSequences, and detection) - builder.AppendLine(vd.SystemIdentifier, " System Identifier"); - builder.AppendLine(vd.VolumeIdentifier, " Volume Identifier"); + builder.AppendLine(encoding.GetString(vd.SystemIdentifier), " System Identifier"); + builder.AppendLine(encoding.GetString(vd.VolumeIdentifier), " Volume Identifier"); if (vd.Unused8Bytes != null && Array.TrueForAll(vd.Unused8Bytes, b => b == 0)) @@ -150,15 +157,15 @@ namespace SabreTools.Serialization.Wrappers builder.AppendLine(vd.OptionalPathTableLocationM, " Optional Type-M Path Table Location"); builder.AppendLine(" Root Directory Record:"); - Print(builder, vd.RootDirectoryRecord); + Print(builder, vd.RootDirectoryRecord, encoding); - builder.AppendLine(vd.VolumeSetIdentifier, " Volume Set Identifier"); - builder.AppendLine(vd.PublisherIdentifier, " Publisher Identifier"); - builder.AppendLine(vd.DataPreparerIdentifier, " Data Preparer Identifier"); - builder.AppendLine(vd.ApplicationIdentifier, " Application Identifier"); - builder.AppendLine(vd.CopyrightFileIdentifier, " Copyright Identifier"); - builder.AppendLine(vd.AbstractFileIdentifier, " Abstract Identifier"); - builder.AppendLine(vd.BibliographicFileIdentifier, " Bibliographic Identifier"); + builder.AppendLine(encoding.GetString(vd.VolumeSetIdentifier), " Volume Set Identifier"); + builder.AppendLine(encoding.GetString(vd.PublisherIdentifier), " Publisher Identifier"); + builder.AppendLine(encoding.GetString(vd.DataPreparerIdentifier), " Data Preparer Identifier"); + builder.AppendLine(encoding.GetString(vd.ApplicationIdentifier), " Application Identifier"); + builder.AppendLine(encoding.GetString(vd.CopyrightFileIdentifier), " Copyright Identifier"); + builder.AppendLine(encoding.GetString(vd.AbstractFileIdentifier), " Abstract Identifier"); + builder.AppendLine(encoding.GetString(vd.BibliographicFileIdentifier), " Bibliographic Identifier"); builder.AppendLine(Format(vd.VolumeCreationDateTime), " Volume Creation Date Time:"); builder.AppendLine(Format(vd.VolumeModificationDateTime), " Volume Modification Date Time:"); @@ -240,7 +247,7 @@ namespace SabreTools.Serialization.Wrappers #region Path Tables - private static void Print(StringBuilder builder, PathTableGroup[]? ptgs) + private static void Print(StringBuilder builder, PathTableGroup[]? ptgs, Encoding encoding) { builder.AppendLine(" Path Table Group(s):"); builder.AppendLine(" -------------------------"); @@ -257,7 +264,7 @@ namespace SabreTools.Serialization.Wrappers { builder.AppendLine($" Type-L Path Table {tableNum}:"); builder.AppendLine(" -------------------------"); - Print(builder, ptgs[tableNum].PathTableL); + Print(builder, ptgs[tableNum].PathTableL, encoding); } else { @@ -268,7 +275,7 @@ namespace SabreTools.Serialization.Wrappers { builder.AppendLine($" Optional Type-L Path Table {tableNum}:"); builder.AppendLine(" -------------------------"); - Print(builder, ptgs[tableNum].OptionalPathTableL); + Print(builder, ptgs[tableNum].OptionalPathTableL, encoding); } else { @@ -279,7 +286,7 @@ namespace SabreTools.Serialization.Wrappers { builder.AppendLine($" Type-M Path Table {tableNum}:"); builder.AppendLine(" -------------------------"); - Print(builder, ptgs[tableNum].PathTableM); + Print(builder, ptgs[tableNum].PathTableM, encoding); } else { @@ -290,7 +297,7 @@ namespace SabreTools.Serialization.Wrappers { builder.AppendLine($" Optional Type-M Path Table {tableNum}:"); builder.AppendLine(" -------------------------"); - Print(builder, ptgs[tableNum].OptionalPathTableM); + Print(builder, ptgs[tableNum].OptionalPathTableM, encoding); } else { @@ -302,7 +309,7 @@ namespace SabreTools.Serialization.Wrappers builder.AppendLine(); } - private static void Print(StringBuilder builder, PathTableRecord[] records) + private static void Print(StringBuilder builder, PathTableRecord[] records, Encoding encoding) { if (records.Length == 0) { @@ -329,7 +336,7 @@ namespace SabreTools.Serialization.Wrappers #region Directories - private static void Print(StringBuilder builder, Dictionary? dirs) + private static void Print(StringBuilder builder, Dictionary? dirs, Encoding encoding) { builder.AppendLine(" Directory Descriptors Information:"); builder.AppendLine(" -------------------------"); @@ -344,39 +351,56 @@ namespace SabreTools.Serialization.Wrappers { builder.AppendLine($" Directory at Sector {kvp.Key}"); builder.AppendLine(" -------------------------"); - Print(builder, kvp.Value); + Print(builder, kvp.Value, encoding); } builder.AppendLine(); } - private static void Print(StringBuilder builder, DirectoryExtent? dir) + private static void Print(StringBuilder builder, FileExtent? extent, Encoding encoding) { - if (dir == null) + if (extent == null) { builder.AppendLine(" No directory descriptor"); builder.AppendLine(); return; } - if (dir.DirectoryRecords == null) - { - builder.AppendLine(" No directory records"); - builder.AppendLine(); - return; - } - for (int recordNum = 0; recordNum < dir.DirectoryRecords.Length; recordNum++) + if (extent is DirectoryExtent dir) { - builder.AppendLine($" Directory Record {recordNum}:"); - builder.AppendLine(" -------------------------"); - Print(builder, dir.DirectoryRecords[recordNum]); - builder.AppendLine(); + if (dir.DirectoryRecords == null) + { + builder.AppendLine(" No directory records"); + builder.AppendLine(); + return; + } + + // File extent is a directory, print all directory records + for (int recordNum = 0; recordNum < dir.DirectoryRecords.Length; recordNum++) + { + builder.AppendLine($" Directory Record {recordNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, dir.DirectoryRecords[recordNum], encoding); + builder.AppendLine(); + } + } + else + { + // File extent is a file, print the file's Extended Attribute Record + Print(builder, extent.ExtendedAttributeRecord); } builder.AppendLine(); } - private static void Print(StringBuilder builder, DirectoryRecord? dr) + private static void Print(StringBuilder builder, ExtendedAttributeRecord? ear) + { + // TODO: Implement ExtendedAttributeRecord printing + + return; + } + + private static void Print(StringBuilder builder, DirectoryRecord? dr, Encoding encoding) { if (dr == null) { diff --git a/SabreTools.Serialization/Wrappers/ISO9660.cs b/SabreTools.Serialization/Wrappers/ISO9660.cs index 169324e3..19e2c593 100644 --- a/SabreTools.Serialization/Wrappers/ISO9660.cs +++ b/SabreTools.Serialization/Wrappers/ISO9660.cs @@ -15,6 +15,9 @@ namespace SabreTools.Serialization.Wrappers #region Extension Properties + /// + public byte[] SystemArea => Model.SystemArea ?? []; + /// public VolumeDescriptor[] VolumeDescriptorSet => Model.VolumeDescriptorSet ?? []; @@ -22,7 +25,7 @@ namespace SabreTools.Serialization.Wrappers public PathTableGroup[] PathTableGroups => Model.PathTableGroups ?? []; /// - public Dictionary DirectoryDescriptors => Model.DirectoryDescriptors ?? []; + public Dictionary DirectoryDescriptors => Model.DirectoryDescriptors ?? []; #endregion diff --git a/SabreTools.Serialization/Wrappers/WrapperBase.cs b/SabreTools.Serialization/Wrappers/WrapperBase.cs index 1f94753d..deffd9cd 100644 --- a/SabreTools.Serialization/Wrappers/WrapperBase.cs +++ b/SabreTools.Serialization/Wrappers/WrapperBase.cs @@ -148,7 +148,7 @@ namespace SabreTools.Serialization.Wrappers #region Data /// - /// Read a number of bytes from an offset fomr the data source, if possible + /// Read a number of bytes from an offset from the data source, if possible /// /// Offset within the data source to start reading /// Number of bytes to read from the offset