import struct,zlib from datetime import datetime from dataclasses import dataclass from enum import IntEnum class NpkPartID(IntEnum): NAME_INFO =0x01 # Package information: name, ver, etc. DESCRIPTION =0x02 # Package description DEPENDENCIES =0x03 # Package Dependencies FILE_CONTAINER =0x04 # Files container zlib 1.2.3 INSTALL_SCRIPT =0x07 # Install script UNINSTALL_SCRIPT =0x08 # Uninstall script SIGNATURE =0x09 # Package signature ARCHITECTURE =0x10 # Package architecture (e.g. i386) PKG_CONFLICTS =0x11 # Package conflicts PKG_INFO =0x12 FEATURES =0x13 PKG_FEATURES =0x14 SQUASHFS =0x15 # SquashFS NULL_BLOCK =0X16 GIT_COMMIT =0x17 # Git commit CHANNEL =0x18 # Release type (e.g. stable, testing, etc.) HEADER =0x19 @dataclass class NpkPartItem: id: NpkPartID data: bytes|object class NpkNameInfo: _format = '<16s4sI12s' def __init__(self,name:str,version:str,build_time=datetime.now(),_unknow=b'\x00'*12): self._name = name[:16].encode().ljust(16,b'\x00') self._version = self.encode_version(version) self._build_time = int(build_time.timestamp()) self._unknow = _unknow def serialize(self)->bytes: return struct.pack(self._format, self._name,self._version,self._build_time,self._unknow) @staticmethod def unserialize_from(data:bytes)->'NpkNameInfo': assert len(data) == struct.calcsize(NpkNameInfo._format),'Invalid data length' _name, _version,_build_time,_unknow = struct.unpack_from(NpkNameInfo._format,data) return NpkNameInfo(_name.decode(),NpkNameInfo.decode_version(_version),datetime.fromtimestamp(_build_time),_unknow) def __len__ (self)->int: return struct.calcsize(self._format) @property def name(self)->str: return self._name.decode().strip('\x00') @name.setter def name(self,value:str): self._name = value[:16].encode().ljust(16,b'\x00') @staticmethod def decode_version(value:bytes): revision,build,minor,major = struct.unpack_from('4B',value) if build == 97: build = 'alpha' elif build == 98: build = 'beta' elif build == 99: build = 'rc' elif build == 102: if revision & 0x80: build = 'test' revision &= 0x7f else: build = 'final' else: build = 'unknown' return f'{major}.{minor}.{revision}.{build}' @staticmethod def encode_version(value:str): s = value.split('.') if 4 != len(s) and s[3] in [ 'alpha', 'beta', 'rc','final', 'test']: raise ValueError('Invalid version string') major = int(s[0]) minor = int(s[1]) revision = int(s[2]) if s[3] == 'alpha': build = 97 elif s[3] == 'beta': build = 98 elif s[3] == 'rc': build = 99 elif s[3] == 'final': build = 102 revision &= 0x7f else: #'test' build = 102 revision |= 0x80 return struct.pack('4B',revision,build,minor,major) @property def version(self)->str: return self.decode_version(self._version) @version.setter def version(self,value:str = '7.15.1.final'): self._version = self.encode_version(value) @property def build_time(self): return datetime.fromtimestamp(self._build_time) @build_time.setter def build_time(self,value:datetime): self._build_time = int(value.timestamp()) class NpkFileContainer: _format = 'bytes: compressed_data = b'' compressor = zlib.compressobj() for item in self._items: data = struct.pack(self._format, item.perm,item.type,item.usr_or_grp, item.modify_time,item.revision,item.rc,item.minor,item.major,item.create_time,item.unknow,len(item.data),len(item.name)) data += item.name + item.data compressed_data += compressor.compress(data) return compressed_data + compressor.flush() @staticmethod def unserialize_from(data:bytes): items:list['NpkFileContainer.NpkFileItem'] = [] decompressed_data = zlib.decompress(data) while len(decompressed_data): offset = struct.calcsize(NpkFileContainer._format) perm,type,usr_or_grp, modify_time,revision,rc,minor,major,create_time,unknow,data_size,name_size= struct.unpack_from(NpkFileContainer._format, decompressed_data) name = decompressed_data[offset:offset+name_size] data = decompressed_data[offset+name_size:offset+name_size+data_size] items.append(NpkFileContainer.NpkFileItem(perm,type,usr_or_grp, modify_time,revision,rc,minor,major,create_time,unknow,name,data)) decompressed_data = decompressed_data[offset+name_size+data_size:] return NpkFileContainer(items) def __len__ (self)->int: return len(self.serialize()) def __getitem__(self,index:int)->'NpkFileContainer.NpkFileItem': return self._items[index] def __iter__(self): for item in self._items: yield item class Package: def __init__(self) -> None: self._parts:list[NpkPartItem] = [] def __iter__(self): for part in self._parts: yield part def __getitem__(self, id:NpkPartID): for part in self._parts: if part.id == id: return part part = NpkPartItem(id,b'') self._parts.append(part) return part class NovaPackage(Package): NPK_MAGIC = 0xbad0f11e def __init__(self,data:bytes=b''): super().__init__() self._packages:list[Package] = [] offset = 0 self._has_pkg = False while offset < len(data): part_id,part_size = struct.unpack_from('bytes: parts = package._parts if package else self._parts for part in parts: data_header = struct.pack('