#!/usr/bin/env python3 """ GeViSet Configuration File Parser and Generator Supports reading and writing Geutebruck GeViSet .set configuration files. Binary Format Specification: - Type markers: 0x00 = Section/Container (2-byte LE length + name string) 0x01 = Boolean (1 byte: 0x00 or 0x01) 0x02 = 64-bit integer (8 bytes LE) 0x04 = 32-bit integer (4 bytes LE) 0x07 = Property name (1-byte length + string) 0x08 = String value (2-byte LE length + string) """ import struct import hashlib from pathlib import Path from typing import Any, Dict, List, Union from collections import OrderedDict class GeViSetParser: """Parser for GeViSet configuration files""" # Type markers TYPE_SECTION = 0x00 TYPE_BOOLEAN = 0x01 TYPE_INT64 = 0x02 TYPE_INT32 = 0x04 TYPE_PROPERTY_NAME = 0x07 TYPE_STRING = 0x08 def __init__(self): self.data = None self.pos = 0 self.config = OrderedDict() def read_byte(self) -> int: """Read a single byte""" if self.pos >= len(self.data): raise EOFError(f"Unexpected end of file at position {self.pos}") b = self.data[self.pos] self.pos += 1 return b def read_bytes(self, count: int) -> bytes: """Read multiple bytes""" if self.pos + count > len(self.data): raise EOFError(f"Unexpected end of file at position {self.pos}") b = self.data[self.pos:self.pos + count] self.pos += count return b def read_uint16(self) -> int: """Read 16-bit unsigned integer (little-endian)""" return struct.unpack(' int: """Read 32-bit unsigned integer (little-endian)""" return struct.unpack(' int: """Read 64-bit unsigned integer (little-endian)""" return struct.unpack(' str: """Read a length-prefixed string""" if length_bytes == 1: length = self.read_byte() elif length_bytes == 2: length = self.read_uint16() else: raise ValueError(f"Invalid length_bytes: {length_bytes}") if length == 0: return "" string_bytes = self.read_bytes(length) try: return string_bytes.decode('utf-8') except UnicodeDecodeError: # Fallback to latin-1 return string_bytes.decode('latin-1') def peek_byte(self) -> int: """Peek at the next byte without advancing position""" if self.pos >= len(self.data): raise EOFError(f"Unexpected end of file at position {self.pos}") return self.data[self.pos] def read_value(self, type_marker: int) -> Any: """Read a value based on its type marker""" if type_marker == self.TYPE_BOOLEAN: return bool(self.read_byte()) elif type_marker == self.TYPE_INT32: return self.read_uint32() elif type_marker == self.TYPE_INT64: return self.read_uint64() elif type_marker == self.TYPE_STRING: return self.read_string_with_length(2) else: raise ValueError(f"Unknown value type: 0x{type_marker:02x} at position {self.pos - 1}") def read_section(self, section_name: str = None) -> OrderedDict: """Read a section and its properties After a section name, the format is: - 4 bytes: int32 (metadata, possibly size or count) - Then properties and nested sections """ section = OrderedDict() # Read section metadata (comes after section name) # This is just a 4-byte int32 if section_name: try: metadata_int = self.read_uint32() # Store metadata for reference section['_metadata'] = metadata_int except EOFError: return section # Check if there's a flags/type byte before properties # If next byte is NOT 0x00 (section) or 0x07 (property), skip it if self.pos < len(self.data): next_byte = self.peek_byte() if next_byte not in [0x00, 0x07]: # This is likely a flags/type byte, skip it self.read_byte() while self.pos < len(self.data): # Peek at the next type marker try: type_marker = self.peek_byte() except EOFError: break if type_marker == self.TYPE_SECTION: # Nested section - read it recursively self.read_byte() # Consume type marker sub_section_name = self.read_string_with_length(1) # Empty section name might indicate end of parent section if not sub_section_name: # End of section marker # Usually followed by zeros if self.pos + 3 < len(self.data): self.pos += min(4, len(self.data) - self.pos) break section[sub_section_name] = self.read_section(sub_section_name) elif type_marker == self.TYPE_PROPERTY_NAME: # Property: name + value self.read_byte() # Consume type marker prop_name = self.read_string_with_length(1) if not prop_name: # Empty property name might indicate section end continue # Read value type and value try: value_type = self.read_byte() value = self.read_value(value_type) section[prop_name] = value except (EOFError, ValueError) as e: print(f"Warning: Error reading property '{prop_name}' at position {self.pos}: {e}") break else: # Unknown type - might be end of section or malformed data # Don't infinitely loop - break if we see too many unknowns print(f"Warning: Unknown type marker 0x{type_marker:02x} at position {self.pos}") break return section def parse_file(self, file_path: Union[str, Path]) -> OrderedDict: """Parse a GeViSet configuration file""" file_path = Path(file_path) self.data = file_path.read_bytes() self.pos = 0 self.config = OrderedDict() print(f"Parsing {file_path} ({len(self.data):,} bytes)...") while self.pos < len(self.data): try: type_marker = self.peek_byte() if type_marker == self.TYPE_SECTION: self.read_byte() # Consume type marker section_name = self.read_string_with_length(1) # Use 1-byte length if not section_name: # Check for end-of-file marker if self.pos >= len(self.data) - 10: break continue print(f" Reading section: {section_name}") self.config[section_name] = self.read_section(section_name) else: # Skip unexpected bytes at end of file if self.pos >= len(self.data) - 10: break print(f"Warning: Skipping unexpected byte 0x{type_marker:02x} at position {self.pos}") self.read_byte() except EOFError: break except Exception as e: print(f"Error at position {self.pos}: {e}") import traceback traceback.print_exc() break print(f"Parsing complete. Read {len(self.config)} top-level sections.") return self.config class GeViSetGenerator: """Generator for GeViSet configuration files""" def __init__(self): self.buffer = bytearray() def write_byte(self, value: int): """Write a single byte""" self.buffer.append(value & 0xFF) def write_bytes(self, data: bytes): """Write multiple bytes""" self.buffer.extend(data) def write_uint16(self, value: int): """Write 16-bit unsigned integer (little-endian)""" self.buffer.extend(struct.pack(' 255: raise ValueError(f"String too long for 1-byte length: {len(text_bytes)} bytes") self.write_byte(len(text_bytes)) elif length_bytes == 2: if len(text_bytes) > 65535: raise ValueError(f"String too long for 2-byte length: {len(text_bytes)} bytes") self.write_uint16(len(text_bytes)) else: raise ValueError(f"Invalid length_bytes: {length_bytes}") self.write_bytes(text_bytes) def write_value(self, value: Any): """Write a value with its type marker""" if isinstance(value, bool): self.write_byte(GeViSetParser.TYPE_BOOLEAN) self.write_byte(1 if value else 0) elif isinstance(value, int): # Determine if 32-bit or 64-bit if -2**31 <= value < 2**31: self.write_byte(GeViSetParser.TYPE_INT32) self.write_uint32(value & 0xFFFFFFFF) else: self.write_byte(GeViSetParser.TYPE_INT64) self.write_uint64(value & 0xFFFFFFFFFFFFFFFF) elif isinstance(value, str): self.write_byte(GeViSetParser.TYPE_STRING) self.write_string_with_length(value, 2) else: raise ValueError(f"Unsupported value type: {type(value)}") def write_property(self, name: str, value: Any): """Write a property (name + value)""" # Property name marker self.write_byte(GeViSetParser.TYPE_PROPERTY_NAME) self.write_string_with_length(name, 1) # Property value self.write_value(value) def write_section(self, name: str, content: Dict[str, Any]): """Write a section with its properties""" # Section marker and name self.write_byte(GeViSetParser.TYPE_SECTION) self.write_string_with_length(name, 1) # Use 1-byte length # Write section metadata (just int32) metadata = content.get('_metadata', 0) if isinstance(metadata, dict): # Handle old format metadata = metadata.get('value', 0) self.write_uint32(metadata) # Write properties and nested sections for key, value in content.items(): if key == '_metadata': # Skip metadata - already written continue if isinstance(value, dict): # Nested section self.write_section(key, value) else: # Property self.write_property(key, value) # Section end marker (empty section) self.write_byte(GeViSetParser.TYPE_SECTION) self.write_byte(0) # Empty name (1-byte length) # Followed by zeros self.write_bytes(b'\x00\x00\x00\x00') def generate_file(self, config: OrderedDict, file_path: Union[str, Path]): """Generate a GeViSet configuration file""" file_path = Path(file_path) self.buffer = bytearray() print(f"Generating {file_path}...") for section_name, section_content in config.items(): print(f" Writing section: {section_name}") self.write_section(section_name, section_content) # Write to file file_path.write_bytes(self.buffer) print(f"Generated {len(self.buffer):,} bytes.") def main(): """Example usage""" import sys import json if len(sys.argv) < 2: print("Usage:") print(" Parse: geviset_parser.py [output.json]") print(" Generate: geviset_parser.py ") return input_file = Path(sys.argv[1]) if input_file.suffix == '.set': # Parse mode parser = GeViSetParser() config = parser.parse_file(input_file) # Output to JSON output_file = Path(sys.argv[2]) if len(sys.argv) > 2 else input_file.with_suffix('.json') with output_file.open('w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) print(f"\nConfiguration saved to {output_file}") print(f"Top-level sections: {list(config.keys())}") elif input_file.suffix == '.json': # Generate mode if len(sys.argv) < 3: print("Error: Output .set file required for generation mode") return output_file = Path(sys.argv[2]) with input_file.open('r', encoding='utf-8') as f: config = json.load(f, object_pairs_hook=OrderedDict) generator = GeViSetGenerator() generator.generate_file(config, output_file) print(f"\nConfiguration file generated: {output_file}") if __name__ == '__main__': main()