feat: Add script to validate layout file

This change adds a Python script to validate the compiled layout file
and optionally dump the contents.
lipc-refactor
nirenjan 2026-04-22 15:18:54 -07:00
parent 9aaec8b2f0
commit 4422ee89c0
1 changed files with 125 additions and 0 deletions

View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Validate a compiled layout file, and optionally dump the contents"""
import argparse
import struct
import zlib
class X52LayoutError(Exception):
"""Exception raised when there's a flaw in the layout"""
class X52Layout:
"""X52Layout is a class that parses a compiled layout file, validates it
and dumps the mappings"""
def __init__(self, layout_file):
"""Load the layout from the layout file"""
with open(layout_file, 'rb') as lfd:
self.raw_layout = lfd.read()
if len(self.raw_layout) < 128:
raise X52LayoutError("layout file too short (%d bytes)" % len(self.raw_layout))
self.magic = self._load_magic()
self.version = self._load_version()
self.flags = self._load_flags()
self.count = self._load_count()
self._validate_checksum()
self.name = self._load_layout_name()
self.description = self._load_layout_description()
def _load_magic(self):
"""Load and validate the magic bytes"""
magic = struct.unpack_from('4s', self.raw_layout[0:4])
if magic[0] != b'X52L':
raise X52LayoutError("invalid magic identifier: (%s)" % magic)
return magic
def _load_version(self):
"""Load and validate the version bytes"""
version = struct.unpack_from(">H", self.raw_layout[4:6])
if version[0] != 1:
raise X52LayoutError("invalid version %d" % version)
return version[0]
def _load_flags(self):
"""Load and validate the flags"""
flags = struct.unpack_from('>H', self.raw_layout[6:8])[0]
should_be_zero = flags >> 2
if should_be_zero:
raise X52LayoutError("reserved flags set; upper bits should be 0; got 0x%04x" % flags)
return flags
def _load_count(self):
"""Load and validate the count"""
count = struct.unpack_from('>I', self.raw_layout[8:12])[0]
if count == 0 or count > 0x10FFFF:
raise X52LayoutError("invalid count 0x%x" % count)
expected_length = count * 2 + 128
if len(self.raw_layout) < expected_length:
raise X52LayoutError("Insufficient bytes: expected %d; got %d",
expected_length, len(self.raw_layout))
if len(self.raw_layout) > expected_length:
raise X52LayoutError("Input too long: expected %d; got %d",
expected_length, len(self.raw_layout))
return count
def _validate_checksum(self):
"""Load and validate the checksum"""
checksum = struct.unpack_from('>I', self.raw_layout[12:16])[0]
computed = zlib.crc32(self.raw_layout[:12])
computed = zlib.crc32(b'\0\0\0\0', computed)
computed = zlib.crc32(self.raw_layout[16:], computed) & 0xFFFFFFFF
if checksum != computed:
raise X52LayoutError("corrupted checksum; expected %04x; got %04x" % (checksum, computed))
@staticmethod
def _read_nul_terminated_string(array):
"""Read a nul terminated string from an array"""
format_str = f"{len(array)}s"
raw_string = struct.unpack_from(format_str, array)[0]
return raw_string.rstrip(b'\x00').decode('utf-8')
def _load_layout_name(self):
"""Load the name from the layout"""
name = self._read_nul_terminated_string(self.raw_layout[16:48])
if not name:
raise X52LayoutError("Missing layout name")
return name
def _load_layout_description(self):
"""Load the description from the layout"""
return self._read_nul_terminated_string(self.raw_layout[48:112])
def dump(self):
"""Dump the parsed layout file"""
print("name:", self.name)
print("description:", self.description)
print("magic:", self.magic)
print("version:", self.version)
print("flags: %#x" % self.flags)
print("count:", self.count)
def main():
"""Main function"""
parser = argparse.ArgumentParser(description="Parse and validate x52layout file")
parser.add_argument('-d', '--dump', action='store_true',
help='Dump decoded layout')
parser.add_argument('layout_file', metavar='layout-file',
help='Path to compiled layout file')
args = parser.parse_args()
layout = X52Layout(args.layout_file)
if args.dump:
layout.dump()
if __name__ == '__main__':
main()