Skip to content

Commit fc45642

Browse files
committed
2026-04-15T1923Z
1 parent 213433d commit fc45642

5 files changed

Lines changed: 130 additions & 29 deletions

File tree

Source/assets/serialisers/csg/csgmdl5.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,11 @@ def convert_to_csgmdl2(csgmdl_buffer: bytes) -> bytes:
284284

285285
xor_encrypt(b''.join([
286286
# Hash
287-
create_hash(vertices=vertices_packed, indices=indices_packed),
287+
create_hash(
288+
vertices=vertices_packed,
289+
indices=indices_packed,
290+
salt=b'67'*8,
291+
),
288292

289293
# Vertex count
290294
len(positions).to_bytes(4, byteorder='little'),

Source/assets/serialisers/csg/util.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def lcm_rand() -> collections.abc.Generator[int, Any, Never]:
2929

3030

3131
@functools.cache
32-
def xor_encrypt(code: bytes, offset: int = 0, key=OBFUSCATION_NOISE_CYCLE_XOR) -> bytes:
32+
def xor_encrypt(code: bytes, offset: int = 0, key: bytes = OBFUSCATION_NOISE_CYCLE_XOR) -> bytes:
3333
l = len(key)
3434
return bytes(
3535
c ^ key[i % l]
@@ -71,6 +71,78 @@ def create_hash(vertices: bytes, indices: bytes, salt: bytes = b'\0'*16) -> byte
7171
return hash_buffer
7272

7373

74+
def recalculate_hash(data: bytes) -> bytes:
75+
data_xor = xor_encrypt(data)
76+
77+
hash_base = 0x0a
78+
hash_size = 0x20
79+
80+
hash_salt_base = hash_base+0x10
81+
hash_salt_size = 0x10
82+
hash_salt = data_xor[
83+
hash_salt_base:
84+
hash_salt_base+hash_salt_size
85+
]
86+
87+
vertex_count_base = hash_base + hash_size
88+
vertex_count_size = INT_SIZE
89+
vertex_count = int.from_bytes(
90+
bytes=data_xor[
91+
vertex_count_base:
92+
vertex_count_base + vertex_count_size
93+
],
94+
byteorder='little',
95+
)
96+
97+
vertex_stride_size_base = vertex_count_base + vertex_count_size
98+
vertex_stride_size_size = INT_SIZE
99+
vertex_stride_size = int.from_bytes(
100+
bytes=data_xor[
101+
vertex_stride_size_base:
102+
vertex_stride_size_base + vertex_stride_size_size
103+
],
104+
byteorder='little',
105+
)
106+
assert vertex_stride_size == 84
107+
108+
vertex_data_base = vertex_stride_size_base + vertex_stride_size_size
109+
vertex_data_size = vertex_count * vertex_stride_size
110+
vertex_data = data_xor[
111+
vertex_data_base:
112+
vertex_data_base + vertex_data_size
113+
]
114+
115+
index_count_base = vertex_data_base + vertex_data_size
116+
index_count_size = INT_SIZE
117+
index_count = int.from_bytes(
118+
bytes=data_xor[
119+
index_count_base:
120+
index_count_base + index_count_size
121+
],
122+
byteorder='little',
123+
)
124+
125+
index_data_base = index_count_base + index_count_size
126+
index_data_size = index_count * INT_SIZE
127+
index_data = data_xor[
128+
index_data_base:
129+
index_data_base + index_data_size
130+
]
131+
132+
new_hash = create_hash(
133+
vertices=vertex_data,
134+
indices=index_data,
135+
salt=hash_salt,
136+
)
137+
138+
result = b''.join([
139+
data[:hash_base],
140+
xor_encrypt(new_hash, offset=hash_base),
141+
data[hash_base+hash_size:],
142+
])
143+
return result
144+
145+
74146
class CSG_HEADER(enum.Enum):
75147
MDL2 = xor_encrypt(get_header(b'CSGMDL', 2))
76148
MDL4 = xor_encrypt(get_header(b'CSGMDL', 4))

Source/tester/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
test_asset,
55
test_logger,
66
test_server,
7+
test_serialise,
78
)
89

910
NAMED_MODULES = {
11+
'serialise': test_serialise,
1012
'asset': test_asset,
1113
'logger': test_logger,
1214
'server': test_server, # This goes last.

Source/tester/test_asset.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# Local application imports
77
from assets import serialisers, extractor
8-
from assets.serialisers.csg.util import create_hash, xor_encrypt
8+
from assets.serialisers.csg.util import create_hash, recalculate_hash, xor_encrypt
99

1010

1111
class TestAssets(unittest.TestCase):
@@ -122,29 +122,3 @@ def test_csg_load(self) -> None:
122122
serialisers.rbxl.parse(
123123
data, methods={serialisers.rbxl.method.convert_csg},
124124
)
125-
126-
def test_csgmdl2_hash(self) -> None:
127-
'''
128-
Tests that CSG v2 unions have a correct hashing algorithm
129-
'''
130-
url = 'https://github.com/krakow10/rbx_mesh/raw/refs/heads/master/meshes/5692112940_2.meshdata'
131-
with urllib.request.urlopen(url) as data:
132-
data_xor = xor_encrypt(data)
133-
self.assertEqual(
134-
first=create_hash(
135-
data_xor[0x32:0x32+0x3a*0x54],
136-
data_xor[0x0000133E:0x0000133E + 4*0x6c],
137-
data_xor[0x1a:0x2a]
138-
),
139-
second=data_xor[0xa:0x2a],
140-
)
141-
142-
def test_csgmdl5_load(self) -> None:
143-
'''
144-
Tests that CSG v3 unions can be parsed.
145-
'''
146-
url = 'https://github.com/krakow10/rbx_mesh/raw/refs/heads/master/meshes/13626979828.meshdata5'
147-
with urllib.request.urlopen(url) as data:
148-
serialisers.rbxl.parse(
149-
data, methods={serialisers.rbxl.method.convert_csg},
150-
)

Source/tester/test_serialise.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Standard library imports
2+
import urllib.request
3+
import unittest
4+
5+
# Local application imports
6+
from assets import serialisers
7+
from assets.serialisers.csg.util import CSG_HEADER, create_hash, recalculate_hash, xor_encrypt
8+
9+
10+
class TestSerialiser(unittest.TestCase):
11+
'''
12+
Tests for different `serialiser` modules.
13+
'''
14+
15+
def test_csgmdl2_hash(self) -> None:
16+
'''
17+
Tests that CSG v2 unions have a correct hashing algorithm.
18+
'''
19+
url = 'https://github.com/krakow10/rbx_mesh/raw/refs/heads/master/meshes/5692112940_2.meshdata'
20+
with urllib.request.urlopen(url) as response:
21+
data = response.read()
22+
23+
data_xor = xor_encrypt(data)
24+
self.assertEqual(
25+
first=create_hash(
26+
data_xor[0x32:0x32+0x3a*0x54],
27+
data_xor[0x0000133E:0x0000133E + 4*0x6c],
28+
data_xor[0x1a:0x2a]
29+
),
30+
second=data_xor[0xa:0x2a],
31+
)
32+
self.assertEqual(
33+
first=recalculate_hash(data),
34+
second=data,
35+
)
36+
37+
def test_csgmdl5_load(self) -> None:
38+
'''
39+
Tests that CSG v3 unions can be parsed.
40+
'''
41+
url = 'https://github.com/krakow10/rbx_mesh/raw/refs/heads/master/meshes/13626979828.meshdata5'
42+
with urllib.request.urlopen(url) as response:
43+
data = response.read()
44+
45+
(result, _changed) = serialisers.parse(
46+
data, methods={serialisers.method.csg},
47+
)
48+
49+
self.assertTrue(result.startswith(CSG_HEADER.MDL2.value))

0 commit comments

Comments
 (0)