- Created project.nuget.cache to store NuGet package cache information. - Added project.packagespec.json to define project restore settings and dependencies. - Included rider.project.restore.info for Rider IDE integration.
164 lines
5.7 KiB
C#
164 lines
5.7 KiB
C#
using System.Text;
|
|
using GsaEditor.Core.Models;
|
|
|
|
namespace GsaEditor.Core.IO;
|
|
|
|
/// <summary>
|
|
/// Reads a .gsa binary archive from a stream or file and produces a <see cref="GsaArchive"/>.
|
|
/// Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
|
|
/// </summary>
|
|
public static class GsaReader
|
|
{
|
|
/// <summary>Magic word for the NARC (legacy) format.</summary>
|
|
private const uint MAGIC_NARC = 0x4E415243;
|
|
|
|
/// <summary>Magic word for the NARD (enhanced) format.</summary>
|
|
private const uint MAGIC_NARD = 0x4E415244;
|
|
|
|
/// <summary>Maximum allowed alias length in characters.</summary>
|
|
private const int MAX_ALIAS_LENGTH = 511;
|
|
|
|
/// <summary>
|
|
/// Reads a GSA archive from the specified file path.
|
|
/// </summary>
|
|
/// <param name="filePath">The path to the .gsa archive file.</param>
|
|
/// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
|
|
/// <exception cref="InvalidDataException">Thrown if the file has an invalid magic word.</exception>
|
|
public static GsaArchive Read(string filePath)
|
|
{
|
|
using var stream = File.OpenRead(filePath);
|
|
return Read(stream);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a GSA archive from a stream by performing a sequential scan of all entries.
|
|
/// </summary>
|
|
/// <param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
|
|
/// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
|
|
/// <exception cref="InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a single file entry from the current stream position.
|
|
/// </summary>
|
|
/// <param name="reader">The binary reader positioned at the start of an entry.</param>
|
|
/// <param name="archive">The parent archive (used for NARD padding alignment).</param>
|
|
/// <returns>A populated <see cref="GsaEntry"/>, or null if no more entries can be read.</returns>
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the stream position to the next aligned boundary.
|
|
/// </summary>
|
|
/// <param name="stream">The stream to align.</param>
|
|
/// <param name="alignment">The alignment boundary in bytes.</param>
|
|
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);
|
|
}
|
|
}
|
|
}
|