Created
February 13, 2026 21:02
-
-
Save ma5ter/2f71eadb81d2d9516e8e35659d19bfcf to your computer and use it in GitHub Desktop.
Creality K1C 2025 Update Image Unpacker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This script implements the same checking and unpacking logic as upgbox utility | |
| import struct | |
| import os | |
| import time | |
| from typing import List, Optional | |
| import sys | |
| import zlib | |
| def crc32(crc: int, data: bytes, length: int) -> int: | |
| return zlib.crc32(data[:length], crc & 0xFFFFFFFF) & 0xFFFFFFFF | |
| def fprintf(level: int, fmt: str, *args): | |
| """Default fprintf implementation.""" | |
| msg = fmt % args if args else fmt | |
| if level == 0: # Error | |
| print(f"ERROR: {msg}", file=sys.stderr) | |
| elif level == 1: # Warning | |
| print(f"WARNING: {msg}", file=sys.stderr) | |
| else: # Info (level 2) | |
| print(msg) | |
| class Partition: | |
| """Partition structure.""" | |
| def __init__(self): | |
| self.partname = b'' | |
| @staticmethod | |
| def unpack(data: bytes): | |
| """Unpack partition from bytes.""" | |
| part = Partition() | |
| # Assuming partition name is 64 bytes to fit the layout | |
| part.partname = data[:64] | |
| return part | |
| class ImageStruct: | |
| """Image node structure (220 bytes for CRC, 224 total with CRC field).""" | |
| STRUCT_SIZE = 224 # 0xE0 | |
| CRC_SIZE = 220 # Everything except no_crc32 | |
| def __init__(self): | |
| self.img_name = b'' | |
| self.partition = Partition() | |
| self.img_size = 0 | |
| self.img_offset = 0 | |
| self.write_offset = 0 | |
| self.img_crc32 = 0 | |
| self.no_crc32 = 0 | |
| @staticmethod | |
| def unpack(data: bytes): | |
| """Unpack image node from bytes.""" | |
| if len(data) < ImageStruct.STRUCT_SIZE: | |
| raise ValueError("Insufficient data for image node") | |
| node = ImageStruct() | |
| offset = 0 | |
| # img_name (128 bytes) - Main identifier, analogous to PkgHdStr.name | |
| node.img_name = data[offset:offset + 128] | |
| offset += 128 | |
| # partition (64 bytes) - Struct containing partname | |
| node.partition = Partition.unpack(data[offset:offset + 64]) | |
| offset += 64 | |
| # img_size (8 bytes, uint64) | |
| node.img_size = struct.unpack('<Q', data[offset:offset + 8])[0] | |
| offset += 8 | |
| # img_offset (8 bytes, uint64) | |
| node.img_offset = struct.unpack('<Q', data[offset:offset + 8])[0] | |
| offset += 8 | |
| # write_offset (8 bytes, uint64) | |
| node.write_offset = struct.unpack('<Q', data[offset:offset + 8])[0] | |
| offset += 8 | |
| # img_crc32 (4 bytes, uint32) | |
| node.img_crc32 = struct.unpack('<I', data[offset:offset + 4])[0] | |
| offset += 4 | |
| # no_crc32 (4 bytes, uint32) | |
| node.no_crc32 = struct.unpack('<I', data[offset:offset + 4])[0] | |
| offset += 4 | |
| return node | |
| class Image: | |
| """Partition image node wrapper class.""" | |
| def __init__(self): | |
| self.fd = None | |
| self.index = 0 | |
| self.already_ckc = False | |
| self.node = ImageStruct() | |
| self.partname = b'' | |
| self.next = None | |
| def check_header_crc(self, raw_data: bytes) -> int: | |
| """Check image node CRC32.""" | |
| # CRC over first 220 bytes | |
| crc = crc32(0, raw_data, ImageStruct.CRC_SIZE) | |
| if crc == self.node.no_crc32: | |
| return 0 | |
| partname = self.node.partition.partname.decode('utf-8', errors='ignore').rstrip('\x00') | |
| print(f"Partition {partname} node CRC32 check failed: {hex(self.node.no_crc32)} - {hex(crc)}") | |
| return -1 | |
| def check_data_crc(self, fd: int) -> int: | |
| """Check image data CRC32.""" | |
| if self.fd is None: | |
| fprintf(0, "File descriptor is not available") | |
| return -1 | |
| try: | |
| os.lseek(fd, self.node.img_offset, os.SEEK_SET) | |
| except OSError as e: | |
| fprintf(0, "Failed to seek to image offset: %s", e) | |
| return -1 | |
| try: | |
| rbuf = os.read(fd, self.node.img_size) | |
| rlen = len(rbuf) | |
| except OSError as e: | |
| fprintf(0, "Failed to read package: %s", e) | |
| return -1 | |
| if crc32(0, rbuf, rlen) == self.node.img_crc32: | |
| return 0 | |
| else: | |
| return -1 | |
| def save(self, path: str) -> int: | |
| """Save image data to a file.""" | |
| if self.fd is None: | |
| fprintf(0, "File descriptor is not available") | |
| return -1 | |
| # Extract image name and clean it | |
| img_name = self.node.img_name.decode('utf-8', errors='ignore').rstrip('\x00') | |
| if not img_name: | |
| fprintf(0, "Image name is empty") | |
| return -1 | |
| # Create output file path | |
| os.makedirs(path, exist_ok=True) | |
| output_path = os.path.join(path, img_name) | |
| # Read image data | |
| try: | |
| os.lseek(self.fd, self.node.img_offset, os.SEEK_SET) | |
| img_data = os.read(self.fd, self.node.img_size) | |
| except OSError as e: | |
| fprintf(0, "Failed to read image data: %s", str(e)) | |
| return -1 | |
| if len(img_data) != self.node.img_size: | |
| fprintf(0, "Incomplete image data read (expected %d, got %d)", self.node.img_size, len(img_data)) | |
| return -1 | |
| # Write to file | |
| try: | |
| with open(output_path, 'wb') as f: | |
| if img_data.startswith(b'SCBT'): | |
| sig_path = output_path + '.sig' | |
| with open(sig_path, 'wb') as sig_f: | |
| sig_f.write(img_data[:2048]) | |
| f.write(img_data[2048:]) | |
| else: | |
| f.write(img_data) | |
| fprintf(2, "Saved image '%s' to %s", img_name, output_path) | |
| return 0 | |
| except OSError as e: | |
| fprintf(0, "Failed to write image to %s: %s", output_path, str(e)) | |
| return -1 | |
| def print_info(self, buffer: List[str]): | |
| """Format image node info.""" | |
| node = self.node | |
| img_size_mb = node.img_size / (1024 * 1024) | |
| img_name = node.img_name.decode('utf-8', errors='ignore').rstrip('\x00') | |
| partname = node.partition.partname.decode('utf-8', errors='ignore').rstrip('\x00') | |
| buffer.append(f"# Node image: {img_name}") | |
| buffer.append(f" Partition: {partname}") | |
| buffer.append(f" Size: {hex(node.img_size)} ({node.img_size}B {img_size_mb:.2f}MB)") | |
| buffer.append(f" Offset: {hex(node.img_offset)} ({node.img_offset})") | |
| buffer.append(f" Write_ofs: {hex(node.write_offset)} ({node.write_offset})") | |
| buffer.append(f" Img crc32: {hex(node.img_crc32)}") | |
| buffer.append(f" Node crc32: {hex(node.no_crc32)}") | |
| buffer.append("") | |
| class Update: | |
| """Update Package""" | |
| class Head: | |
| """Package header structure (312 bytes for CRC, 316 total with CRC field).""" | |
| STRUCT_SIZE = 0x13C # 316 bytes | |
| CRC_SIZE = 0x138 # 312 bytes (everything except hd_crc32) | |
| def __init__(self): | |
| self.name = b'' | |
| self.soft_desc = b'' | |
| self.soft_sn = b'' | |
| self.version = b'' | |
| self.build_time = 0 | |
| self.pack_size = 0 | |
| self.img_num = 0 | |
| self.img_w_mode = 0 | |
| self.hd_crc32 = 0 | |
| @staticmethod | |
| def unpack(data: bytes): | |
| """Unpack package header from bytes.""" | |
| if len(data) < Update.Head.STRUCT_SIZE: | |
| raise ValueError("Insufficient data for package header") | |
| head = Update.Head() | |
| offset = 0 | |
| # name (128 bytes) - offset 0x00 | |
| head.name = data[offset:offset + 128] | |
| offset = 0x80 | |
| # soft_desc (64 bytes) - offset 0x80 | |
| head.soft_desc = data[offset:offset + 64] | |
| offset = 0xC0 | |
| # soft_sn (64 bytes) - offset 0xC0 | |
| head.soft_sn = data[offset:offset + 64] | |
| offset = 0x100 | |
| # version (32 bytes) - offset 0x100 | |
| head.version = data[offset:offset + 32] | |
| offset = 0x120 | |
| # build_time (8 bytes, qword) - offset 0x120 | |
| head.build_time = struct.unpack('<Q', data[offset:offset + 8])[0] | |
| offset = 0x128 | |
| # pack_size (8 bytes, qword) - offset 0x128 | |
| head.pack_size = struct.unpack('<Q', data[offset:offset + 8])[0] | |
| offset = 0x130 | |
| # img_num (4 bytes, dword) - offset 0x130 | |
| head.img_num = struct.unpack('<I', data[offset:offset + 4])[0] | |
| offset = 0x134 | |
| # img_w_mode (4 bytes, dword) - offset 0x134 | |
| head.img_w_mode = struct.unpack('<I', data[offset:offset + 4])[0] | |
| offset = 0x138 | |
| # hd_crc32 (4 bytes, dword) - offset 0x138 | |
| head.hd_crc32 = struct.unpack('<I', data[offset:offset + 4])[0] | |
| return head | |
| def __init__(self, upgfile: str): | |
| self.upgfile = upgfile | |
| self.head = Update.Head() | |
| self.images: List[Image] = [] | |
| self.fd = None | |
| @staticmethod | |
| def load(upgfile: str, fprintf=None) -> Optional['Update']: | |
| """Main package check function.""" | |
| pack = Update(upgfile) | |
| if not os.path.exists(upgfile): | |
| fprintf(0, "Package file (%s) does not exist or is unreadable", upgfile) | |
| return None | |
| if not os.access(upgfile, os.R_OK): | |
| fprintf(0, "Package file (%s) is not accessible", upgfile) | |
| return None | |
| try: | |
| fd = os.open(upgfile, os.O_RDONLY | (os.O_BINARY if hasattr(os, 'O_BINARY') else 0)) | |
| except OSError: | |
| fprintf(0, "Failed to open %s", upgfile) | |
| return None | |
| try: | |
| if pack.load_head(fd) != 0: | |
| return None | |
| pack.images = [] | |
| if pack.load_nodes(fd) != 0: | |
| return None | |
| if pack.check_images(fd) != 0: | |
| pack.images.clear() | |
| return None | |
| pack.fd = fd | |
| return pack | |
| except Exception as e: | |
| fprintf(0, "Unexpected error: %s", str(e)) | |
| os.close(fd) | |
| return None | |
| def check_head_crc(self, raw_data: bytes) -> int: | |
| """Check package header CRC32.""" | |
| # CRC over first 312 bytes | |
| crc = crc32(0, raw_data, Update.Head.CRC_SIZE) | |
| if crc == self.head.hd_crc32: | |
| return 0 | |
| name = self.head.name.decode('utf-8', errors='ignore').rstrip('\x00') | |
| print(f"Package {name} header CRC32 check failed: {hex(self.head.hd_crc32)} - {hex(crc)}") | |
| return -1 | |
| def load_head(self, fd: int) -> int: | |
| """Check package header.""" | |
| os.lseek(fd, 0, os.SEEK_SET) | |
| header_data = os.read(fd, Update.Head.STRUCT_SIZE) | |
| if len(header_data) != Update.Head.STRUCT_SIZE: | |
| fprintf(0, "Failed to read %s header", self.upgfile) | |
| return -1 | |
| # Unpack structure | |
| try: | |
| self.head = Update.Head.unpack(header_data) | |
| except Exception as e: | |
| fprintf(0, "Failed to unpack header: %s", str(e)) | |
| return -1 | |
| if self.check_head_crc(header_data): | |
| fprintf(0, "Package file %s header check failed", self.upgfile) | |
| return -1 | |
| # Check filename match | |
| filename = os.path.basename(self.upgfile) | |
| pkg_name = self.head.name.decode('utf-8', errors='ignore').rstrip('\x00') | |
| if filename != pkg_name: | |
| fprintf(1, "Package file %s has been modified (original: %s)", self.upgfile, pkg_name) | |
| # Display header info | |
| str_info = [] | |
| self.print_info(str_info) | |
| fprintf(2, "%s", "\n" + "\n".join(str_info)) | |
| fprintf(2, "Package file %s header check passed", self.upgfile) | |
| return 0 | |
| def load_nodes(self, fd: int) -> int: | |
| """Check all image nodes.""" | |
| os.lseek(fd, Update.Head.STRUCT_SIZE, os.SEEK_SET) | |
| num = self.head.img_num | |
| for index in range(num): | |
| node = Image() | |
| node.fd = fd | |
| node.index = index | |
| node.already_ckc = False | |
| node_data = os.read(fd, ImageStruct.STRUCT_SIZE) | |
| if len(node_data) != ImageStruct.STRUCT_SIZE: | |
| fprintf(0, "Failed to read image node") | |
| return -1 | |
| # Unpack structure | |
| try: | |
| node.node = ImageStruct.unpack(node_data) | |
| except Exception as e: | |
| fprintf(0, "Failed to unpack node: %s", str(e)) | |
| return -1 | |
| if node.check_header_crc(node_data): | |
| fprintf(0, "Image node check failed") | |
| return -1 | |
| node.partname = node.node.partition.partname | |
| # Add to list | |
| self.images.append(node) | |
| # Display node info | |
| str_info = [] | |
| node.print_info(str_info) | |
| fprintf(2, "\n" + "\n".join(str_info)) | |
| fprintf(2, "Package file %s node check passed", self.upgfile) | |
| return 0 | |
| def check_images(self, fd: int) -> int: | |
| """Check image content CRCs with duplicate detection.""" | |
| for node in self.images: | |
| skip = False | |
| # Check for duplicates | |
| for od in self.images: | |
| if (od.already_ckc and | |
| od.node.img_offset == node.node.img_offset and | |
| od.node.img_size == node.node.img_size and | |
| od.node.img_crc32 == node.node.img_crc32): | |
| skip = True | |
| break | |
| partname = node.partname.decode('utf-8', errors='ignore').rstrip('\x00') | |
| if skip: | |
| node.already_ckc = True | |
| fprintf(2, "# Check index %d image %s skipped", node.index, partname) | |
| else: | |
| if node.check_data_crc(fd): | |
| fprintf(0, "Check index %d image %s failed", node.index, partname) | |
| return -1 | |
| node.already_ckc = True | |
| fprintf(2, "# Check index %d image %s passed", node.index, partname) | |
| return 0 | |
| def print_info(self, buffer: List[str]): | |
| """Format package header info.""" | |
| head = self.head | |
| build_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(head.build_time)) | |
| pack_size_mb = head.pack_size / (1024 * 1024) | |
| name = head.name.decode('utf-8', errors='ignore').rstrip('\x00') | |
| version = head.version.decode('utf-8', errors='ignore').rstrip('\x00') | |
| soft_sn = head.soft_sn.decode('utf-8', errors='ignore').rstrip('\x00') | |
| soft_desc = head.soft_desc.decode('utf-8', errors='ignore').rstrip('\x00') | |
| buffer.append(f"# Package name: {name}") | |
| buffer.append(f" Build time: {build_time}") | |
| buffer.append(f" Size: {hex(head.pack_size)} ({head.pack_size}B {pack_size_mb:.2f}MB)") | |
| buffer.append(f" Image num: {head.img_num}") | |
| buffer.append(f" Write mode: {head.img_w_mode}") | |
| buffer.append(f" Version: {version}") | |
| buffer.append(f" Serial num: {soft_sn}") | |
| buffer.append(f" Desc: {soft_desc}") | |
| buffer.append(f" Head crc32: {hex(head.hd_crc32)}") | |
| buffer.append("") | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 2: | |
| print("Usage: python unpack.py <update_file>") | |
| sys.exit(1) | |
| update = Update.load(sys.argv[1]) | |
| if update is not None: | |
| print("\n=== Package check PASSED! ===\n") | |
| for image in update.images: | |
| image.save('/tmp') | |
| os.close(update.fd) | |
| sys.exit(0) | |
| else: | |
| print("\n=== Package check FAILED! ===") | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment