55__all__ = ["CCSchemaParser" ]
66
77import typing as t
8+ from enum import Enum
89
910import numpy as np
1011
1920 import numpy .typing as npt
2021
2122
23+ class IndexMode (Enum ):
24+ """Enum for vertex index mode."""
25+
26+ MODE_16BIT = 16
27+ MODE_32BIT = 32
28+
29+
2230class CCSchemaParser (BaseSchemaParser ):
2331 """Parser for the 'CC' HPS compression schema."""
2432
@@ -38,7 +46,7 @@ def parse(self, context: ParseContext) -> ParseResult:
3846 :return: The parsing result containing the decoded mesh and commands.
3947 """
4048 vertices , vertex_commands = self .parse_vertices (context .vertex_data )
41- faces , face_commands = self .parse_faces (context .face_data )
49+ faces , face_commands = self .parse_faces (context .face_data , context . face_count , len ( vertices ) )
4250
4351 uv = self ._parse_texture_coords (context .texture_coords_data , vertices .shape [0 ], faces )
4452 texture_images = [image for image in context .texture_images if isinstance (image , bytes )]
@@ -64,20 +72,43 @@ def parse(self, context: ParseContext) -> ParseResult:
6472 face_commands = face_commands ,
6573 )
6674
67- def parse_faces (self , data : bytes ) -> tuple [npt .NDArray [np .integer ], list [hpc .AnyFaceCommand ]]:
75+ def parse_faces (
76+ self ,
77+ data : bytes ,
78+ face_count : int ,
79+ vertex_count : int ,
80+ ) -> tuple [npt .NDArray [np .integer ], list [hpc .AnyFaceCommand ]]:
6881 """Parse face data from bytes.
6982
7083 :param data: The raw byte data containing face data.
84+ :param face_count: Expected number of faces (used for validation).
85+ :param vertex_count: Total number of vertices (used for validation).
7186 :return: An array of face indices (M, 3) and the face commands.
7287 """
73- self ._clear ()
74-
75- commands = self ._parse_commands (data )
76- for command in commands :
77- self ._process_command (command )
78-
79- faces = np .array (self ._faces , dtype = np .int32 )
80- return faces , commands
88+ errors : list [HPSParseError ] = []
89+ for mode in (IndexMode .MODE_16BIT , IndexMode .MODE_32BIT ):
90+ try :
91+ self ._clear ()
92+
93+ commands = self ._parse_commands (data , mode )
94+ for command in commands :
95+ self ._process_command (command , vertex_count )
96+
97+ faces = np .array (self ._faces , dtype = np .int32 )
98+ if len (faces ) != face_count :
99+ raise HPSParseError (
100+ f"Face count mismatch in { mode .name } mode: expected { face_count } , got { len (faces )} "
101+ )
102+
103+ return faces , commands
104+ except HPSParseError as e :
105+ errors .append (e )
106+ continue
107+
108+ raise HPSParseError (
109+ "Failed to parse face data with both 16-bit and 32-bit index modes.\n "
110+ "Errors encountered:\n " + "\n " .join (f" - { e } " for e in errors )
111+ )
81112
82113 def parse_vertices (self , data : bytes ) -> tuple [npt .NDArray [np .floating ], list [hpc .AnyVertexCommand ]]:
83114 """Parse vertex data from bytes.
@@ -206,10 +237,11 @@ def _next_global_vertex(self) -> int:
206237 self ._global_vertex_ptr += 1
207238 return v
208239
209- def _parse_commands (self , data : bytes ) -> list [hpc .AnyFaceCommand ]:
240+ def _parse_commands (self , data : bytes , mode : IndexMode ) -> list [hpc .AnyFaceCommand ]:
210241 """Parse face commands from the binary data.
211242
212243 :param data: The raw byte data containing face commands.
244+ :param mode: The index mode (16-bit or 32-bit).
213245 :return: A list of parsed face commands.
214246 """
215247 reader = BinaryReader (data )
@@ -225,16 +257,17 @@ def _parse_commands(self, data: bytes) -> list[hpc.AnyFaceCommand]:
225257 raise HPSParseError ("Upper 4 bits of face command byte must be zero" , offset = reader .position - 1 )
226258
227259 opcode = command_byte & 0x0F
228- command = self ._parse_single_command (reader , opcode )
260+ command = self ._parse_single_command (reader , opcode , mode )
229261 commands .append (command )
230262
231263 return commands
232264
233- def _parse_single_command (self , reader : BinaryReader , opcode : int ) -> hpc .AnyFaceCommand :
265+ def _parse_single_command (self , reader : BinaryReader , opcode : int , mode : IndexMode ) -> hpc .AnyFaceCommand :
234266 """Parse a single command given its opcode.
235267
236268 :param reader: The binary reader positioned after the opcode byte.
237269 :param opcode: The command opcode.
270+ :param mode: The index mode (16-bit or 32-bit).
238271 :return: The parsed command.
239272 :raises HPSParseError: If the opcode is unknown.
240273 """
@@ -250,6 +283,14 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
250283 case hpc .FaceCommandType .RESTART :
251284 return hpc .Restart ()
252285 case hpc .FaceCommandType .RESTART_16 :
286+ if mode == IndexMode .MODE_32BIT :
287+ # 16-bit opcode but 32-bit payload (╯°□°)╯︵ ┻━┻
288+ return hpc .Restart16 (
289+ v0 = reader .read_uint32 (),
290+ v1 = reader .read_uint32 (),
291+ v2 = reader .read_uint32 (),
292+ )
293+
253294 return hpc .Restart16 (
254295 v0 = reader .read_uint16 (),
255296 v1 = reader .read_uint16 (),
@@ -262,8 +303,11 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
262303 v2 = reader .read_uint32 (),
263304 )
264305 case hpc .FaceCommandType .ABSOLUTE_16 :
265- # Why can the Absolute16 command have 32-bit values? (╯°□°)╯︵ ┻━┻
266- return hpc .Absolute16 (v = reader .read_uint32 ())
306+ if mode == IndexMode .MODE_32BIT :
307+ # 16-bit opcode but 32-bit payload (╯°□°)╯︵ ┻━┻
308+ return hpc .Absolute16 (v = reader .read_uint32 ())
309+
310+ return hpc .Absolute16 (v = reader .read_uint16 ())
267311 case hpc .FaceCommandType .ABSOLUTE_32 :
268312 return hpc .Absolute32 (v = reader .read_uint32 ())
269313 case hpc .FaceCommandType .REMOVE :
@@ -273,10 +317,11 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
273317 case _:
274318 raise HPSParseError (f"Unknown face command opcode: { opcode } " , offset = reader .position )
275319
276- def _process_command (self , command : hpc .AnyFaceCommand ) -> None :
320+ def _process_command (self , command : hpc .AnyFaceCommand , vertex_count : int | None = None ) -> None :
277321 """Process a single face command and update internal state.
278322
279323 :param command: The command to process.
324+ :param vertex_count: The total number of vertices for bounds checking.
280325 """
281326 match command .op :
282327 case hpc .FaceCommandType .VERTEX_LIST :
@@ -295,8 +340,10 @@ def _process_command(self, command: hpc.AnyFaceCommand) -> None:
295340 v2 = self ._next_global_vertex ()
296341 self ._create_restart_face (v0 , v1 , v2 )
297342 case hpc .FaceCommandType .RESTART_16 | hpc .FaceCommandType .RESTART_32 :
343+ self ._validate_indices (command .v0 , command .v1 , command .v2 , vertex_count = vertex_count )
298344 self ._create_restart_face (command .v0 , command .v1 , command .v2 )
299345 case hpc .FaceCommandType .ABSOLUTE_16 | hpc .FaceCommandType .ABSOLUTE_32 :
346+ self ._validate_indices (command .v , vertex_count = vertex_count )
300347 self ._extend_current_edge (command .v )
301348 self ._increase_edge_pointer (2 )
302349 case hpc .FaceCommandType .REMOVE :
@@ -425,3 +472,17 @@ def _remove_current_edge(self) -> None:
425472 self ._current_edge_idx = curr_idx % len (self ._edge_list )
426473 else :
427474 self ._current_edge_idx = 0
475+
476+ def _validate_indices (self , * indices : int , vertex_count : int | None ) -> None :
477+ """Validate vertex indices are within expected range.
478+
479+ :param indices: The vertex indices to validate.
480+ :param vertex_count: The total number of vertices for bounds checking.
481+ :raises HPSParseError: If index is out of bounds.
482+ """
483+ if vertex_count is None :
484+ return
485+
486+ for v in indices :
487+ if v < 0 or v >= vertex_count :
488+ raise HPSParseError (f"Vertex index { v } out of bounds (0 to { vertex_count - 1 } )" )
0 commit comments