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);
}
}
}