Skip to content

Commit 87eb854

Browse files
authored
Merge pull request #260 from TeamWheelWizard/dev
to 2.4.5
2 parents 33a59db + 4a20819 commit 87eb854

63 files changed

Lines changed: 4363 additions & 1389 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Flatpak/io.github.TeamWheelWizard.WheelWizard.metainfo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
</provides>
5656

5757
<releases>
58+
<release version="2.4.5" date="2026-05-17"/>
5859
<release version="2.4.4" date="2026-04-15"/>
5960
<release version="2.4.3" date="2026-04-01"/>
6061
<release version="2.4.2" date="2026-03-25"/>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace WheelWizard.Features.Archives;
2+
3+
public static class ArchivesExtensions
4+
{
5+
public static IServiceCollection AddArchives(this IServiceCollection services)
6+
{
7+
services.AddSingleton<ISzsArchiveDecoder, SzsArchiveDecoder>();
8+
return services;
9+
}
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace WheelWizard.Features.Archives;
2+
3+
public sealed record DecodedArchive(IReadOnlyDictionary<string, byte[]> Files);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace WheelWizard.Features.Archives;
2+
3+
public sealed record U8Node(int Type, int NameOffset, int DataOffset, int Size);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace WheelWizard.Features.Archives;
2+
3+
public interface ISzsArchiveDecoder
4+
{
5+
OperationResult<DecodedArchive> TryDecodeU8Archive(byte[] bytes);
6+
7+
OperationResult<byte[]> DecompressYaz0IfNeeded(byte[] bytes);
8+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using WheelWizard.Helpers;
2+
3+
namespace WheelWizard.Features.Archives;
4+
5+
public sealed class SzsArchiveDecoder : ISzsArchiveDecoder
6+
{
7+
private const uint U8Magic = 0x55aa382d;
8+
9+
public OperationResult<DecodedArchive> TryDecodeU8Archive(byte[] bytes)
10+
{
11+
try
12+
{
13+
var decompressResult = DecompressYaz0IfNeeded(bytes);
14+
if (decompressResult.IsFailure)
15+
return decompressResult.Error;
16+
17+
var raw = decompressResult.Value;
18+
19+
if (raw.Length < 8)
20+
return new OperationError { Message = "The provided file is too small to contain a valid U8 archive header." };
21+
if (BigEndianBinaryHelper.BufferToUint32(raw, 0) != U8Magic)
22+
return new OperationError { Message = "The provided file is not a valid Yaz0/U8 archive." };
23+
24+
return ParseU8Archive(raw);
25+
}
26+
catch (Exception ex)
27+
{
28+
return new OperationError { Message = $"Failed to decode U8 archive: {ex.Message}", Exception = ex };
29+
}
30+
}
31+
32+
public OperationResult<byte[]> DecompressYaz0IfNeeded(byte[] bytes)
33+
{
34+
try
35+
{
36+
if (bytes.Length < 4)
37+
return bytes;
38+
39+
if (BinaryStringHelper.ReadAscii(bytes, 0, 4) != "Yaz0")
40+
return bytes;
41+
42+
if (bytes.Length < 8)
43+
return new OperationError { Message = "Yaz0 header is truncated." };
44+
45+
var outputSize = checked((int)BigEndianBinaryHelper.BufferToUint32(bytes, 4));
46+
var output = new byte[outputSize];
47+
var src = 0x10;
48+
var dst = 0;
49+
var groupHeader = 0;
50+
var bitsRemaining = 0;
51+
52+
while (dst < output.Length)
53+
{
54+
if (bitsRemaining == 0)
55+
{
56+
if (src >= bytes.Length)
57+
return new OperationError { Message = "Yaz0 group header is truncated." };
58+
groupHeader = bytes[src++];
59+
bitsRemaining = 8;
60+
}
61+
62+
if ((groupHeader & 0x80) != 0)
63+
{
64+
if (src >= bytes.Length)
65+
return new OperationError { Message = "Yaz0 literal chunk is truncated." };
66+
output[dst++] = bytes[src++];
67+
}
68+
else
69+
{
70+
if (src + 1 >= bytes.Length)
71+
return new OperationError { Message = "Yaz0 backreference is truncated." };
72+
73+
var b1 = bytes[src++];
74+
var b2 = bytes[src++];
75+
var backOffset = (((b1 & 0x0f) << 8) | b2) + 1;
76+
var length = b1 >> 4;
77+
if (length == 0)
78+
{
79+
if (src >= bytes.Length)
80+
return new OperationError { Message = "Yaz0 extended length byte is truncated." };
81+
length = bytes[src++] + 0x12;
82+
}
83+
else
84+
{
85+
length += 2;
86+
}
87+
88+
if (backOffset > dst)
89+
return new OperationError { Message = "Yaz0 backreference offset is out of bounds." };
90+
91+
var copySrc = dst - backOffset;
92+
for (var index = 0; index < length && dst < output.Length; index++)
93+
output[dst++] = output[copySrc++];
94+
}
95+
96+
groupHeader <<= 1;
97+
bitsRemaining--;
98+
}
99+
100+
return output;
101+
}
102+
catch (Exception ex)
103+
{
104+
return new OperationError { Message = $"Failed to decompress Yaz0 data: {ex.Message}", Exception = ex };
105+
}
106+
}
107+
108+
private static DecodedArchive ParseU8Archive(byte[] bytes)
109+
{
110+
var rootOffset = (int)BigEndianBinaryHelper.BufferToUint32(bytes, 4);
111+
var rootNode = ReadU8Node(bytes, rootOffset);
112+
var nodeCount = rootNode.Size;
113+
var stringTableOffset = rootOffset + nodeCount * 12;
114+
var files = new Dictionary<string, byte[]>(StringComparer.Ordinal);
115+
116+
if (rootNode.Type != 1)
117+
throw new InvalidDataException("U8 root node is not a directory.");
118+
if (nodeCount == 0 || stringTableOffset > bytes.Length)
119+
throw new InvalidDataException("U8 node table is invalid or truncated.");
120+
121+
void Walk(int dirIndex, string prefix, int parentEndIndex)
122+
{
123+
var nodeIndex = dirIndex + 1;
124+
var directoryNode = ReadU8Node(bytes, rootOffset + dirIndex * 12);
125+
var endIndex = Math.Min(directoryNode.Size, parentEndIndex);
126+
127+
if (endIndex <= dirIndex)
128+
return;
129+
130+
while (nodeIndex < endIndex)
131+
{
132+
var nodeOffset = rootOffset + nodeIndex * 12;
133+
var node = ReadU8Node(bytes, nodeOffset);
134+
var nameOffset = stringTableOffset + node.NameOffset;
135+
136+
if (nameOffset < stringTableOffset || nameOffset >= bytes.Length)
137+
{
138+
nodeIndex++;
139+
continue;
140+
}
141+
142+
var name = BinaryStringHelper.ReadNullTerminatedAscii(bytes, nameOffset);
143+
var logicalPath = string.IsNullOrEmpty(prefix) ? name : $"{prefix}/{name}";
144+
145+
if (node.Type == 1)
146+
{
147+
Walk(nodeIndex, logicalPath, endIndex);
148+
nodeIndex = Math.Min(Math.Max(node.Size, nodeIndex + 1), endIndex);
149+
}
150+
else
151+
{
152+
var endOffset = node.DataOffset + node.Size;
153+
if (endOffset <= bytes.Length && endOffset >= node.DataOffset)
154+
files[logicalPath] = bytes[node.DataOffset..endOffset];
155+
nodeIndex++;
156+
}
157+
}
158+
}
159+
160+
Walk(0, string.Empty, nodeCount);
161+
return new(files);
162+
}
163+
164+
private static U8Node ReadU8Node(byte[] bytes, int offset)
165+
{
166+
if (offset + 12 > bytes.Length)
167+
throw new InvalidDataException("U8 node table is truncated.");
168+
169+
return new(
170+
bytes[offset] == 0 ? 0 : 1,
171+
(bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3],
172+
(int)BigEndianBinaryHelper.BufferToUint32(bytes, offset + 4),
173+
(int)BigEndianBinaryHelper.BufferToUint32(bytes, offset + 8)
174+
);
175+
}
176+
}

0 commit comments

Comments
 (0)