Skip to content

Commit 7668955

Browse files
Merge fix/linux-filestream-read: Linux compatibility fixes
- Replace MemoryMappedFile with FileStream to avoid mmap corruption on Linux - Handle Base64URL characters in encrypted data - Fix Xor/Aes flag check ordering (0xFF is superset of 0xEE) - Add bounds checking in NIF block parser - Add error recovery in NifParser for decrypt failures - Fix cross-platform paths in TestUtils Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 40aa4e1 + 323c76f commit 7668955

6 files changed

Lines changed: 69 additions & 34 deletions

File tree

Maple2.File.IO/Crypto/CryptoManager.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.IO.Compression;
2-
using System.IO.MemoryMappedFiles;
32
using System.Text;
43
using Maple2.File.IO.Crypto.Common;
54
using Maple2.File.IO.Crypto.Keys;
@@ -32,12 +31,24 @@ public static byte[] DecryptFileTable(IPackStream stream, System.IO.Stream buffe
3231
throw new Exception("ERROR decrypting file table: the size of the table is invalid.");
3332
}
3433

35-
public static byte[] DecryptData(IPackFileHeader pHeader, MemoryMappedFile data) {
34+
public static byte[] DecryptData(IPackFileHeader pHeader, FileStream data, object readLock) {
3635
if (pHeader.CompressedFileSize > 0 && pHeader.EncodedFileSize > 0 && pHeader.FileSize > 0) {
37-
using MemoryMappedViewStream buffer = data.CreateViewStream((long) pHeader.Offset, pHeader.EncodedFileSize);
3836
byte[] src = new byte[pHeader.EncodedFileSize];
37+
int bytesRead;
38+
lock (readLock) {
39+
data.Seek((long) pHeader.Offset, SeekOrigin.Begin);
40+
int totalRead = 0;
41+
int remaining = (int) pHeader.EncodedFileSize;
42+
while (remaining > 0) {
43+
int read = data.Read(src, totalRead, remaining);
44+
if (read == 0) break;
45+
totalRead += read;
46+
remaining -= read;
47+
}
48+
bytesRead = totalRead;
49+
}
3950

40-
if (buffer.Read(src, 0, (int) pHeader.EncodedFileSize) == pHeader.EncodedFileSize) {
51+
if (bytesRead == pHeader.EncodedFileSize) {
4152
return Decrypt(pHeader.Version, pHeader.EncodedFileSize, (uint) pHeader.CompressedFileSize, pHeader.BufferFlag, src);
4253
}
4354
}
@@ -47,19 +58,20 @@ public static byte[] DecryptData(IPackFileHeader pHeader, MemoryMappedFile data)
4758

4859
// Decryption Routine: Base64 -> AES -> Zlib
4960
private static byte[] Decrypt(PackVersion version, uint size, uint sizeCompressed, Encryption flag, byte[] src) {
50-
if (flag.HasFlag(Encryption.Aes)) {
61+
if (flag.HasFlag(Encryption.Xor)) {
62+
// Decrypt the XOR encrypted block (check Xor before Aes because 0xFF is a superset of 0xEE in bits)
63+
src = EncryptXor(version, src, size, sizeCompressed);
64+
} else if (flag.HasFlag(Encryption.Aes)) {
5165
// Get the AES Key/IV for transformation
5266
CipherKeys.GetKeyAndIV(version, sizeCompressed, out byte[] key, out byte[] iv);
5367

54-
// Decode the base64 encoded string
55-
src = Convert.FromBase64String(Encoding.UTF8.GetString(src));
68+
// Decode the base64 encoded string (handle both standard and URL-safe Base64)
69+
string b64 = Encoding.UTF8.GetString(src).Replace('-', '+').Replace('_', '/');
70+
src = Convert.FromBase64String(b64);
5671

5772
// Decrypt the AES encrypted block
5873
var pCipher = new AESCipher(key, iv);
5974
pCipher.TransformBlock(src, 0, size, src, 0);
60-
} else if (flag.HasFlag(Encryption.Xor)) {
61-
// Decrypt the XOR encrypted block
62-
src = EncryptXor(version, src, size, sizeCompressed);
6375
}
6476

6577
return flag.HasFlag(Encryption.Zlib) ? UncompressBuffer(src) : src;

Maple2.File.IO/M2dReader.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Globalization;
2-
using System.IO.MemoryMappedFiles;
1+
using System.Globalization;
32
using System.Text;
43
using System.Xml;
54
using Maple2.File.IO.Crypto;
@@ -8,14 +7,15 @@
87

98
namespace Maple2.File.IO {
109
public class M2dReader : IDisposable {
11-
private readonly MemoryMappedFile m2dFile;
10+
private readonly FileStream m2dFile;
11+
private readonly object m2dLock = new();
1212
public readonly IReadOnlyList<PackFileEntry> Files;
1313

1414
public M2dReader(string path) {
1515
// Force Globalization to en-US because we use periods instead of commas for decimals
1616
CultureInfo.CurrentCulture = new("en-US");
1717

18-
m2dFile = MemoryMappedFile.CreateFromFile(path);
18+
m2dFile = System.IO.File.OpenRead(path);
1919

2020
// Create an index from the header file
2121
using var headerReader = new BinaryReader(System.IO.File.OpenRead(path.Replace(".m2d", ".m2h")));
@@ -41,12 +41,12 @@ public PackFileEntry GetEntry(string filename) {
4141
}
4242

4343
public XmlReader GetXmlReader(PackFileEntry entry) {
44-
return XmlReader.Create(new MemoryStream(CryptoManager.DecryptData(entry.FileHeader, m2dFile)));
44+
return XmlReader.Create(new MemoryStream(CryptoManager.DecryptData(entry.FileHeader, m2dFile, m2dLock)));
4545
}
4646

4747
public XmlDocument GetXmlDocument(PackFileEntry entry) {
4848
var document = new XmlDocument();
49-
byte[] data = CryptoManager.DecryptData(entry.FileHeader, m2dFile);
49+
byte[] data = CryptoManager.DecryptData(entry.FileHeader, m2dFile, m2dLock);
5050
try {
5151
document.Load(new MemoryStream(data));
5252
} catch {
@@ -58,11 +58,11 @@ public XmlDocument GetXmlDocument(PackFileEntry entry) {
5858
}
5959

6060
public byte[] GetBytes(PackFileEntry entry) {
61-
return CryptoManager.DecryptData(entry.FileHeader, m2dFile);
61+
return CryptoManager.DecryptData(entry.FileHeader, m2dFile, m2dLock);
6262
}
6363

6464
public string GetString(PackFileEntry entry) {
65-
byte[] data = CryptoManager.DecryptData(entry.FileHeader, m2dFile);
65+
byte[] data = CryptoManager.DecryptData(entry.FileHeader, m2dFile, m2dLock);
6666
string result = Encoding.Default.GetString(data);
6767
// Remove UTF-8 BOM if present
6868
if (result.Length > 0 && result[0] == '\uFEFF') {

Maple2.File.IO/Nif/NifDocument.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ private int ReadHeaderString() {
3838
}
3939

4040
public NifBlock GetBlock(int index) {
41+
if (index < 0 || index >= Blocks.Length) {
42+
throw new IndexOutOfRangeException(
43+
$"Block index {index} out of range [0..{Blocks.Length}). " +
44+
$"Stream position: {Reader.BaseStream.Position}, " +
45+
$"Reading block: [{ReadingBlock?.BlockIndex}] {ReadingBlock?.BlockType} \"{ReadingBlock?.Name}\"");
46+
}
47+
4148
if (Blocks[index] is not null) {
4249
return Blocks[index];
4350
}
@@ -81,8 +88,16 @@ public T GetBlock<T>(int index) {
8188
public List<T> ReadBlockRefList<T>() {
8289
List<T> blocks = new List<T>();
8390

91+
long countPos = Reader.BaseStream.Position;
8492
int count = Reader.ReadAdjustedInt32();
8593

94+
if (count < 0 || count > Blocks.Length) {
95+
throw new InvalidDataException(
96+
$"Unreasonable block ref list count {count} at stream position {countPos}. " +
97+
$"Total blocks: {Blocks.Length}. " +
98+
$"Reading block: [{ReadingBlock?.BlockIndex}] {ReadingBlock?.BlockType} \"{ReadingBlock?.Name}\"");
99+
}
100+
86101
#if NETSTANDARD2_1
87102
blocks.EnsureCapacityCompat(count);
88103
#else

Maple2.File.Parser/Maple2.File.Parser.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<PackageTags>MapleStory2, File, Parser, m2d, xml</PackageTags>
1414
<!-- Use following lines to write the generated files to disk. -->
1515
<EmitCompilerGeneratedFiles Condition=" '$(Configuration)' == 'Debug' ">true</EmitCompilerGeneratedFiles>
16-
<PackageVersion>2.3.10</PackageVersion>
16+
<PackageVersion>2.4.0</PackageVersion>
1717
<TargetFramework>net8.0</TargetFramework>
1818
<PackageReadmeFile>README.md</PackageReadmeFile>
1919
<ImplicitUsings>enable</ImplicitUsings>

Maple2.File.Parser/NifParser.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ public NifParser(List<PrefixedM2dReader> modelM2dReaders) {
1717
string path = nifReader.PathPrefix + entry.Name;
1818
uint llid = LlidHash.Hash(path);
1919

20-
NifDocument nifDocument = new NifDocument(path, nifReader.GetBytes(entry));
20+
byte[] data;
21+
try {
22+
data = nifReader.GetBytes(entry);
23+
} catch (Exception ex) {
24+
Console.Error.WriteLine($"[NifParser] Failed to decrypt: {path} from {nifReader.PathPrefix} (FileIndex={entry.FileHeader?.FileIndex}): {ex.Message}");
25+
continue;
26+
}
27+
28+
NifDocument nifDocument = new NifDocument(path, data);
2129

2230
yield return (llid, path, nifDocument);
2331
}

Maple2.File.Tests/TestUtils.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@ static TestUtils() {
2323
throw new Exception("MS2_DATA_FOLDER is not set");
2424
}
2525

26-
XmlReader = new M2dReader(@$"{m2dPath}\Xml.m2d");
27-
ExportedReader = new M2dReader(@$"{m2dPath}\Resource\Exported.m2d");
28-
ServerReader = new M2dReader(@$"{m2dPath}\Server.m2d");
29-
AssetMetadataReader = new M2dReader(@$"{m2dPath}\Resource\asset-web-metadata.m2d");
26+
XmlReader = new M2dReader(Path.Combine(m2dPath, "Xml.m2d"));
27+
ExportedReader = new M2dReader(Path.Combine(m2dPath, "Resource", "Exported.m2d"));
28+
ServerReader = new M2dReader(Path.Combine(m2dPath, "Server.m2d"));
29+
AssetMetadataReader = new M2dReader(Path.Combine(m2dPath, "Resource", "asset-web-metadata.m2d"));
3030
AssetIndex = new AssetIndex(AssetMetadataReader);
3131
ModelM2dReaders = new List<PrefixedM2dReader>() {
32-
new PrefixedM2dReader("/library/", $@"{m2dPath}\Resource\Library.m2d"),
33-
new PrefixedM2dReader("/model/map/", $@"{m2dPath}\Resource\Model\Map.m2d"),
34-
new PrefixedM2dReader("/model/effect/", $@"{m2dPath}\Resource\Model\Effect.m2d"),
35-
new PrefixedM2dReader("/model/camera/", $@"{m2dPath}\Resource\Model\Camera.m2d"),
36-
new PrefixedM2dReader("/model/tool/", $@"{m2dPath}\Resource\Model\Tool.m2d"),
37-
new PrefixedM2dReader("/model/item/", $@"{m2dPath}\Resource\Model\Item.m2d"),
38-
new PrefixedM2dReader("/model/npc/", $@"{m2dPath}\Resource\Model\Npc.m2d"),
39-
new PrefixedM2dReader("/model/path/", $@"{m2dPath}\Resource\Model\Path.m2d"),
40-
new PrefixedM2dReader("/model/character/", $@"{m2dPath}\Resource\Model\Character.m2d"),
41-
new PrefixedM2dReader("/model/textures/", $@"{m2dPath}\Resource\Model\Textures.m2d"),
32+
new PrefixedM2dReader("/library/", Path.Combine(m2dPath, "Resource", "Library.m2d")),
33+
new PrefixedM2dReader("/model/map/", Path.Combine(m2dPath, "Resource", "Model", "Map.m2d")),
34+
new PrefixedM2dReader("/model/effect/", Path.Combine(m2dPath, "Resource", "Model", "Effect.m2d")),
35+
new PrefixedM2dReader("/model/camera/", Path.Combine(m2dPath, "Resource", "Model", "Camera.m2d")),
36+
new PrefixedM2dReader("/model/tool/", Path.Combine(m2dPath, "Resource", "Model", "Tool.m2d")),
37+
new PrefixedM2dReader("/model/item/", Path.Combine(m2dPath, "Resource", "Model", "Item.m2d")),
38+
new PrefixedM2dReader("/model/npc/", Path.Combine(m2dPath, "Resource", "Model", "Npc.m2d")),
39+
new PrefixedM2dReader("/model/path/", Path.Combine(m2dPath, "Resource", "Model", "Path.m2d")),
40+
new PrefixedM2dReader("/model/character/", Path.Combine(m2dPath, "Resource", "Model", "Character.m2d")),
41+
new PrefixedM2dReader("/model/textures/", Path.Combine(m2dPath, "Resource", "Model", "Textures.m2d")),
4242
};
4343
}
4444

0 commit comments

Comments
 (0)