Skip to content

Commit 75c0d67

Browse files
erwan-jolyclaude
andcommitted
Add NosArchive read/write for legacy .NOS containers
Exposes a public static NosArchive type with Read(byte[]) / Write(IReadOnlyList<Entry>) handling the legacy file-list + XOR-obfuscated payload layout (NScliData_*.NOS, NSlangData_*.NOS, etc). Preserves the per-entry Id and the unknown 4-byte field verbatim so round-trips are byte-faithful after decryption. Useful for tooling that needs to rewrite strings inside conststring.dat (e.g. the NosMall URL) and repack without shelling out to external tools. Bump version to 4.1.0 (new public API). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 37785f4 commit 75c0d67

2 files changed

Lines changed: 153 additions & 1 deletion

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
6+
namespace NosCore.ParserInputGenerator.Extractor;
7+
8+
/// <summary>
9+
/// Read/write support for the legacy NosTale <c>.NOS</c> archive format used
10+
/// by <c>NScliData_*.NOS</c> and friends. The newer "NT Data" DEFLATE variant
11+
/// is not supported — not needed for the NosMall URL patch since conststring
12+
/// archives use the legacy layout.
13+
///
14+
/// Layout: <c>int32 fileCount</c>, then per entry
15+
/// <c>int32 id; int32 nameLen; byte[nameLen] name; int32 unknown; int32 encLen; byte[encLen] enc</c>.
16+
/// The inner content is XOR-obfuscated — see <see cref="Decrypt"/> and
17+
/// <see cref="Encrypt"/> (our encrypter uses only the simple 0x33-XOR mode,
18+
/// which the decrypter handles along with the packed-nibble mode from the
19+
/// real client).
20+
/// </summary>
21+
public static class NosArchive
22+
{
23+
/// <summary>
24+
/// A single file entry inside a legacy <c>.NOS</c> archive.
25+
/// </summary>
26+
/// <param name="Id">Per-entry id stored in the archive header (usually the sequential index).</param>
27+
/// <param name="Name">Inner file name (ASCII), e.g. <c>conststring.dat</c>.</param>
28+
/// <param name="Unknown">The 4-byte header field between the name and the payload size whose purpose we don't yet characterise — preserve verbatim on round-trip.</param>
29+
/// <param name="Content">Decrypted payload bytes.</param>
30+
public sealed record Entry(int Id, string Name, int Unknown, byte[] Content);
31+
32+
private static readonly byte[] CryptoArray =
33+
{
34+
0x00, 0x20, 0x2D, 0x2E, 0x30, 0x31, 0x32, 0x33, 0x34,
35+
0x35, 0x36, 0x37, 0x38, 0x39, 0x0A, 0x00,
36+
};
37+
38+
/// <summary>
39+
/// Parse a legacy <c>.NOS</c> archive and return its entries with
40+
/// decrypted <see cref="Entry.Content"/>.
41+
/// </summary>
42+
public static List<Entry> Read(byte[] bytes)
43+
{
44+
var result = new List<Entry>();
45+
var i = 0;
46+
var fileCount = BitConverter.ToInt32(bytes, i); i += 4;
47+
for (var f = 0; f < fileCount; f++)
48+
{
49+
var id = BitConverter.ToInt32(bytes, i); i += 4;
50+
var nameLen = BitConverter.ToInt32(bytes, i); i += 4;
51+
var name = Encoding.ASCII.GetString(bytes, i, nameLen);
52+
i += nameLen;
53+
var unknown = BitConverter.ToInt32(bytes, i); i += 4;
54+
var encLen = BitConverter.ToInt32(bytes, i); i += 4;
55+
var enc = new byte[encLen];
56+
Buffer.BlockCopy(bytes, i, enc, 0, encLen);
57+
i += encLen;
58+
var content = Decrypt(enc);
59+
result.Add(new Entry(id, name, unknown, content));
60+
}
61+
return result;
62+
}
63+
64+
/// <summary>
65+
/// Serialise a list of entries back to the legacy <c>.NOS</c> byte layout,
66+
/// re-applying the simple 0x33-XOR encryption. The decrypter in the real
67+
/// client accepts this output.
68+
/// </summary>
69+
public static byte[] Write(IReadOnlyList<Entry> entries)
70+
{
71+
using var ms = new MemoryStream();
72+
using var w = new BinaryWriter(ms);
73+
w.Write(entries.Count);
74+
foreach (var e in entries)
75+
{
76+
var nameBytes = Encoding.ASCII.GetBytes(e.Name);
77+
var enc = Encrypt(e.Content);
78+
w.Write(e.Id);
79+
w.Write(nameBytes.Length);
80+
w.Write(nameBytes);
81+
w.Write(e.Unknown);
82+
w.Write(enc.Length);
83+
w.Write(enc);
84+
}
85+
return ms.ToArray();
86+
}
87+
88+
private static byte[] Decrypt(byte[] enc)
89+
{
90+
var output = new List<byte>(enc.Length * 2);
91+
var i = 0;
92+
while (i < enc.Length)
93+
{
94+
var b = enc[i++];
95+
if (b == 0xFF)
96+
{
97+
output.Add(0x0D);
98+
continue;
99+
}
100+
var len = b & 0x7F;
101+
if ((b & 0x80) != 0)
102+
{
103+
for (; len > 0; len -= 2)
104+
{
105+
if (i >= enc.Length) break;
106+
var c = enc[i++];
107+
output.Add(CryptoArray[(c & 0xF0) >> 4]);
108+
if (len <= 1) break;
109+
var lo = CryptoArray[c & 0x0F];
110+
if (lo == 0) break;
111+
output.Add(lo);
112+
}
113+
}
114+
else
115+
{
116+
for (; len > 0; len--)
117+
{
118+
if (i >= enc.Length) break;
119+
output.Add((byte)(enc[i++] ^ 0x33));
120+
}
121+
}
122+
}
123+
return output.ToArray();
124+
}
125+
126+
private static byte[] Encrypt(byte[] plain)
127+
{
128+
using var ms = new MemoryStream(plain.Length * 2);
129+
var i = 0;
130+
while (i < plain.Length)
131+
{
132+
if (plain[i] == 0x0D)
133+
{
134+
ms.WriteByte(0xFF);
135+
i++;
136+
continue;
137+
}
138+
var start = i;
139+
while (i < plain.Length && plain[i] != 0x0D && (i - start) < 0x7F)
140+
{
141+
i++;
142+
}
143+
var chunkLen = i - start;
144+
ms.WriteByte((byte)chunkLen);
145+
for (var j = start; j < i; j++)
146+
{
147+
ms.WriteByte((byte)(plain[j] ^ 0x33));
148+
}
149+
}
150+
return ms.ToArray();
151+
}
152+
}

src/NosCore.ParserInputGenerator/NosCore.ParserInputGenerator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<RepositoryUrl>https://github.com/NosCoreIO/NosCore.ParserInputGenerator.git</RepositoryUrl>
1313
<PackageIconUrl></PackageIconUrl>
1414
<PackageTags>nostale, noscore, nostale private server source, nostale emulator</PackageTags>
15-
<Version>4.0.0</Version>
15+
<Version>4.1.0</Version>
1616
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
1717
<Description>NosCore's Parser InputGenerator</Description>
1818
<PackageLicenseExpression></PackageLicenseExpression>

0 commit comments

Comments
 (0)