|
| 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