Skip to content

Instantly share code, notes, and snippets.

@playday3008
Last active July 12, 2025 15:08
Show Gist options
  • Select an option

  • Save playday3008/c26833299fe8373a4190ec9360687a77 to your computer and use it in GitHub Desktop.

Select an option

Save playday3008/c26833299fe8373a4190ec9360687a77 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
QCDT (Qualcomm Device Tree) Patcher
Decompresses LZ4 QCDT files, patches header structure, and saves modified data.
Basically:
1. Read LZ4 compressed QCDT
2. Decompress
3. Parse header
4. Check if extended
- If extended:
1. Parse table
2. Remove model field from table
3. Save table
4. Save QCDT
- If no:
1. Save QCDT
"""
from typing import cast
import argparse
import struct
import sys
from pathlib import Path
try:
import lz4.frame
except ImportError:
print("Error: lz4 module not found. Install it using your package manager or pip: pip install lz4")
sys.exit(1)
class QCDTHeader:
"""Represents the QCDT header structure"""
def __init__(self, data: bytes):
if len(data) < 12:
raise ValueError("Invalid QCDT header: too short")
# Parse header: magic(4) + version(1) + extended(1) + reserved(2) + num_entries(4)
self.magic, self.version, self.extended, self.reserved, self.num_entries = struct.unpack('<4sBBHI', data[:12])
if self.magic != b'QCDT':
raise ValueError(f"Invalid magic: expected b'QCDT', got {self.magic}")
print(f"QCDT Header:")
print(f" Magic: {self.magic}")
print(f" Version: {self.version}")
print(f" Extended: {self.extended}")
print(f" Reserved: {self.reserved}")
print(f" Number of entries: {self.num_entries}")
def pack(self):
"""Pack header back to bytes"""
return struct.pack('<4sBBHI', self.magic, self.version, self.extended, self.reserved, self.num_entries)
class DTEntry:
"""Represents a device tree entry"""
# Original structure size: 4*4 + 4*4 + 4 + 4 + 32 = 72 bytes
ORIGINAL_SIZE = 72
# New structure size: 4*4 + 4*4 + 4 + 4 = 40 bytes (removing 32-byte model field)
NEW_SIZE = 40
def __init__(self, data: bytes, offset: int = 0):
if len(data) < offset + self.ORIGINAL_SIZE:
raise ValueError("Invalid DT entry: insufficient data")
entry_data = data[offset:offset + self.ORIGINAL_SIZE]
# Parse entry: platform_id(4) + variant_id(4) + board_hw_subtype(4) + soc_rev(4) +
# pmic_rev[4](16) + offset(4) + size(4) + model(32)
unpacked = struct.unpack('<IIII4III32s', entry_data)
self.platform_id = unpacked[0]
self.variant_id = unpacked[1]
self.board_hw_subtype = unpacked[2]
self.soc_rev = unpacked[3]
self.pmic_rev = list(unpacked[4:8])
self.dt_offset = unpacked[8]
self.dt_size = unpacked[9]
self.model = unpacked[10].rstrip(b'\x00') # Remove null padding
print(f" Entry: platform_id={self.platform_id}, variant_id=0x{self.variant_id:X}, "
f"model='{self.model.decode('utf-8', errors='ignore')}', "
f"size=0x{self.dt_size:X}, offset=0x{self.dt_offset:X}")
def pack_new_format(self):
"""Pack entry in new format (without model field)"""
return struct.pack('<IIII4III',
self.platform_id,
self.variant_id,
self.board_hw_subtype,
self.soc_rev,
*self.pmic_rev,
self.dt_offset,
self.dt_size)
def decompress_lz4_file(input_file: Path) -> bytes | None:
"""Decompress LZ4 file into memory"""
print(f"Decompressing {input_file}...")
try:
with open(input_file, 'rb') as f:
compressed_data = f.read()
decompressed_data = cast(bytes, lz4.frame.decompress(compressed_data))
print(f"Successfully decompressed {len(compressed_data)} bytes to {len(decompressed_data)} bytes")
return decompressed_data
except Exception as e:
print(f"Error decompressing file: {e}")
return None
def patch_qcdt_data(data: bytes) -> bytes:
"""Patch QCDT data to remove model field from entries"""
print("Patching QCDT data...")
# Parse header
header = QCDTHeader(data)
if header.version != 3 and header.extended != 1:
return data
header.extended = 0
header_size = 12
original_entry_size = DTEntry.ORIGINAL_SIZE
new_entry_size = DTEntry.NEW_SIZE
# Find where device tree data starts (after header and all entries)
dt_data_start = header_size + (header.num_entries * original_entry_size)
if len(data) < dt_data_start:
raise ValueError("Invalid QCDT file: file too small for declared entries")
# Parse entries
entries: list[DTEntry] = []
for i in range(header.num_entries):
entry_offset = header_size + (i * original_entry_size)
entry = DTEntry(data, entry_offset)
entries.append(entry)
# Calculate space to fill with zeros
space_per_entry = original_entry_size - new_entry_size
total_zero_fill = header.num_entries * space_per_entry
print(f"Will fill {total_zero_fill} bytes with zeros")
# Build new data
new_data = bytearray()
# Add header
new_data.extend(header.pack())
# Add entries in new format
for entry in entries:
new_data.extend(entry.pack_new_format())
# Fill emptied space with zeros
zero_fill = b'\x00' * total_zero_fill
new_data.extend(zero_fill)
# Add device tree data (unchanged)
dt_data = data[dt_data_start:]
new_data.extend(dt_data)
print(f"Patched data: {len(data)} bytes -> {len(new_data)} bytes")
return bytes(new_data)
def save_file(data: bytes, output_file: Path):
"""Save data to file"""
print(f"Saving to {output_file}...")
try:
with open(output_file, 'wb') as f:
f.write(data)
print(f"Successfully saved {len(data)} bytes to {output_file}")
except Exception as e:
print(f"Error saving file: {e}")
return False
return True
def main():
parser = argparse.ArgumentParser(description="QCDT Patcher: Decompresses and patches Qualcomm Device Tree files.")
parser.add_argument('input_file', type=str, help='Input LZ4 compressed QCDT file')
parser.add_argument('output_file', type=str, help='Output file for patched QCDT data')
args = parser.parse_args()
input_file = Path(args.input_file)
output_file = Path(args.output_file)
if not input_file.exists():
print(f"Error: Input file '{input_file}' not found")
sys.exit(1)
if not input_file.is_file():
print(f"Error: Input file '{input_file}' is not a regular file")
sys.exit(1)
decompressed_data = decompress_lz4_file(input_file)
if decompressed_data is None:
sys.exit(1)
try:
patched_data = patch_qcdt_data(decompressed_data)
if save_file(patched_data, output_file):
print("Success! QCDT file has been patched.")
else:
sys.exit(1)
except Exception as e:
print(f"Error processing QCDT data: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
import std.mem;
import std.string;
import hex.core;
import type.magic;
#define MAGIC_HEAD "SINGLE_N_LONELY"
#define MAGIC_TAIL "LONELY_N_SINGLE"
struct Part {
std::string::NullString name;
padding[0xF8 - sizeof(name)];
u64 size;
u8 data[size];
padding[while(std::mem::read_unsigned($, 1) == 0x00)];
hex::core::add_virtual_file(name, data);
} [[name(name)]];
struct Bootloader {
type::Magic<MAGIC_HEAD> magic_head [[hidden]];
padding[0x100 - sizeof(magic_head)];
Part parts[while(!std::mem::reached(std::mem::size() - 0x100))] [[inline]];
type::Magic<MAGIC_TAIL> magic_tail [[hidden]];
padding[0x100 - sizeof(magic_tail)];
};
Bootloader bootloader @ 0x0 [[inline]];
// Check "Virtual Filesystem" tab below
import type.magic;
struct dt_entry<auto Extended>
{
u32 platform_id;
u32 variant_id;
u32 board_hw_subtype;
u32 soc_rev;
u32 pmic_rev[4];
u32 offset;
u32 size;
if (Extended)
char model[32];
u8 data[size] @ offset;
};
struct dt_table
{
type::Magic<"QCDT"> magic;
u8 version;
u8 extended;
u16 reserved;
u32 num_entries;
};
dt_table table @ $;
dt_entry<table.extended> entries[table.num_entries] @ $;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment