diff --git a/src/NosCore.ParserInputGenerator/Extractor/NosArchive.cs b/src/NosCore.ParserInputGenerator/Extractor/NosArchive.cs new file mode 100644 index 0000000..0ed48b7 --- /dev/null +++ b/src/NosCore.ParserInputGenerator/Extractor/NosArchive.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace NosCore.ParserInputGenerator.Extractor; + +/// +/// Read/write support for the legacy NosTale .NOS archive format used +/// by NScliData_*.NOS and friends. The newer "NT Data" DEFLATE variant +/// is not supported — not needed for the NosMall URL patch since conststring +/// archives use the legacy layout. +/// +/// Layout: int32 fileCount, then per entry +/// int32 id; int32 nameLen; byte[nameLen] name; int32 unknown; int32 encLen; byte[encLen] enc. +/// The inner content is XOR-obfuscated — see and +/// (our encrypter uses only the simple 0x33-XOR mode, +/// which the decrypter handles along with the packed-nibble mode from the +/// real client). +/// +public static class NosArchive +{ + /// + /// A single file entry inside a legacy .NOS archive. + /// + /// Per-entry id stored in the archive header (usually the sequential index). + /// Inner file name (ASCII), e.g. conststring.dat. + /// The 4-byte header field between the name and the payload size whose purpose we don't yet characterise — preserve verbatim on round-trip. + /// Decrypted payload bytes. + public sealed record Entry(int Id, string Name, int Unknown, byte[] Content); + + private static readonly byte[] CryptoArray = + { + 0x00, 0x20, 0x2D, 0x2E, 0x30, 0x31, 0x32, 0x33, 0x34, + 0x35, 0x36, 0x37, 0x38, 0x39, 0x0A, 0x00, + }; + + /// + /// Parse a legacy .NOS archive and return its entries with + /// decrypted . + /// + public static List Read(byte[] bytes) + { + var result = new List(); + var i = 0; + var fileCount = BitConverter.ToInt32(bytes, i); i += 4; + for (var f = 0; f < fileCount; f++) + { + var id = BitConverter.ToInt32(bytes, i); i += 4; + var nameLen = BitConverter.ToInt32(bytes, i); i += 4; + var name = Encoding.ASCII.GetString(bytes, i, nameLen); + i += nameLen; + var unknown = BitConverter.ToInt32(bytes, i); i += 4; + var encLen = BitConverter.ToInt32(bytes, i); i += 4; + var enc = new byte[encLen]; + Buffer.BlockCopy(bytes, i, enc, 0, encLen); + i += encLen; + var content = Decrypt(enc); + result.Add(new Entry(id, name, unknown, content)); + } + return result; + } + + /// + /// Serialise a list of entries back to the legacy .NOS byte layout, + /// re-applying the simple 0x33-XOR encryption. The decrypter in the real + /// client accepts this output. + /// + public static byte[] Write(IReadOnlyList entries) + { + using var ms = new MemoryStream(); + using var w = new BinaryWriter(ms); + w.Write(entries.Count); + foreach (var e in entries) + { + var nameBytes = Encoding.ASCII.GetBytes(e.Name); + var enc = Encrypt(e.Content); + w.Write(e.Id); + w.Write(nameBytes.Length); + w.Write(nameBytes); + w.Write(e.Unknown); + w.Write(enc.Length); + w.Write(enc); + } + return ms.ToArray(); + } + + private static byte[] Decrypt(byte[] enc) + { + var output = new List(enc.Length * 2); + var i = 0; + while (i < enc.Length) + { + var b = enc[i++]; + if (b == 0xFF) + { + output.Add(0x0D); + continue; + } + var len = b & 0x7F; + if ((b & 0x80) != 0) + { + for (; len > 0; len -= 2) + { + if (i >= enc.Length) break; + var c = enc[i++]; + output.Add(CryptoArray[(c & 0xF0) >> 4]); + if (len <= 1) break; + var lo = CryptoArray[c & 0x0F]; + if (lo == 0) break; + output.Add(lo); + } + } + else + { + for (; len > 0; len--) + { + if (i >= enc.Length) break; + output.Add((byte)(enc[i++] ^ 0x33)); + } + } + } + return output.ToArray(); + } + + private static byte[] Encrypt(byte[] plain) + { + using var ms = new MemoryStream(plain.Length * 2); + var i = 0; + while (i < plain.Length) + { + if (plain[i] == 0x0D) + { + ms.WriteByte(0xFF); + i++; + continue; + } + var start = i; + while (i < plain.Length && plain[i] != 0x0D && (i - start) < 0x7F) + { + i++; + } + var chunkLen = i - start; + ms.WriteByte((byte)chunkLen); + for (var j = start; j < i; j++) + { + ms.WriteByte((byte)(plain[j] ^ 0x33)); + } + } + return ms.ToArray(); + } +} diff --git a/src/NosCore.ParserInputGenerator/NosCore.ParserInputGenerator.csproj b/src/NosCore.ParserInputGenerator/NosCore.ParserInputGenerator.csproj index 0314e49..6125ad7 100644 --- a/src/NosCore.ParserInputGenerator/NosCore.ParserInputGenerator.csproj +++ b/src/NosCore.ParserInputGenerator/NosCore.ParserInputGenerator.csproj @@ -12,7 +12,7 @@ https://github.com/NosCoreIO/NosCore.ParserInputGenerator.git nostale, noscore, nostale private server source, nostale emulator - 4.0.0 + 4.1.0 false NosCore's Parser InputGenerator