using System.Text; using GsaEditor.Core.Models; namespace GsaEditor.Core.IO; /// /// Reads a .gsa binary archive from a stream or file and produces a . /// Supports both NARC (legacy) and NARD (enhanced with padding) format variants. /// public static class GsaReader { /// Magic word for the NARC (legacy) format. private const uint MAGIC_NARC = 0x4E415243; /// Magic word for the NARD (enhanced) format. private const uint MAGIC_NARD = 0x4E415244; /// Maximum allowed alias length in characters. private const int MAX_ALIAS_LENGTH = 511; /// /// Reads a GSA archive from the specified file path. /// /// The path to the .gsa archive file. /// A fully populated . /// Thrown if the file has an invalid magic word. public static GsaArchive Read(string filePath) { using var stream = File.OpenRead(filePath); return Read(stream); } /// /// Reads a GSA archive from a stream by performing a sequential scan of all entries. /// /// A readable, seekable stream positioned at the start of the archive. /// A fully populated . /// Thrown if the stream has an invalid magic word. public static GsaArchive Read(Stream stream) { using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true); var archive = new GsaArchive(); // Read global header uint magic = reader.ReadUInt32(); if (magic == MAGIC_NARC) { archive.Format = GsaFormat.NARC; } else if (magic == MAGIC_NARD) { archive.Format = GsaFormat.NARD; archive.OffsetPadding = reader.ReadInt32(); archive.SizePadding = reader.ReadInt32(); } else { throw new InvalidDataException( $"Invalid GSA magic word: 0x{magic:X8}. Expected NARC (0x{MAGIC_NARC:X8}) or NARD (0x{MAGIC_NARD:X8})."); } // Sequentially read all entries until EOF while (stream.Position < stream.Length) { var entry = ReadEntry(reader, archive); if (entry == null) break; archive.Entries.Add(entry); } return archive; } /// /// Reads a single file entry from the current stream position. /// /// The binary reader positioned at the start of an entry. /// The parent archive (used for NARD padding alignment). /// A populated , or null if no more entries can be read. private static GsaEntry? ReadEntry(BinaryReader reader, GsaArchive archive) { var stream = reader.BaseStream; if (stream.Position >= stream.Length) return null; // NARD: align stream position before reading entry header if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0) AlignStream(stream, archive.OffsetPadding); // Check if enough bytes remain for the alias length field if (stream.Length - stream.Position < 4) return null; // Read alias uint aliasLength = reader.ReadUInt32(); if (aliasLength == 0 || aliasLength > MAX_ALIAS_LENGTH) return null; if (stream.Length - stream.Position < aliasLength) return null; byte[] aliasBytes = reader.ReadBytes((int)aliasLength); string alias = Encoding.ASCII.GetString(aliasBytes); // Read compression flag (bit 0: 1=compressed, 0=raw) byte compressionByte = reader.ReadByte(); bool isCompressed = (compressionByte & 0x01) != 0; // Read original (decompressed) length uint originalLength = reader.ReadUInt32(); // Read compressed length (only present if compressed) uint compressedLength; if (isCompressed) { compressedLength = reader.ReadUInt32(); } else { compressedLength = originalLength; } // NARD: align stream position before reading the data block if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0) AlignStream(stream, archive.OffsetPadding); // Read raw data long dataOffset = stream.Position; uint dataSize = isCompressed ? compressedLength : originalLength; if (stream.Length - stream.Position < dataSize) return null; byte[] rawData = reader.ReadBytes((int)dataSize); return new GsaEntry { Alias = alias, IsCompressed = isCompressed, OriginalLength = originalLength, CompressedLength = compressedLength, RawData = rawData, DataOffset = dataOffset }; } /// /// Advances the stream position to the next aligned boundary. /// /// The stream to align. /// The alignment boundary in bytes. private static void AlignStream(Stream stream, int alignment) { if (alignment <= 1) return; long pos = stream.Position; long remainder = pos % alignment; if (remainder != 0) { stream.Position = pos + (alignment - remainder); } } }