新sdat2img
概要
在Android的FOTA更新包中,因为现在大家都去用payload.bin了,所以旧的Block更新的Sparse Image转Raw Image工具就没人更新了
之前留意到Brotli压缩版本的还有人写工具,但是最近发现在一些低性能电视盒子上使用的是lzma压缩方式就没有人在意了。
所以把这整个工具重构了一遍,使其更加好用且性能更好。
库安装
pip3 install Brotli
代码
#!/usr/bin/env python3
import sys
import os
import lzma
import brotli
from pathlib import Path
from typing import List, Tuple, BinaryIO
BLOCK_SIZE = 4096
def isTransferList(file_path: str, sample_size: int = 1024) -> bool:
try:
with open(file_path, 'rb') as f:
return b'\0' not in f.read(sample_size)
except IOError:
return False
def decompressLzmaDat(input_file: str, output_file: str):
with lzma.open(input_file, 'rb') as lzma_file:
with open(output_file, 'wb') as decompressed_file:
decompressed_file.write(lzma_file.read())
def decompressBrotliDat(input_file: str, output_file: str):
with open(input_file, 'rb') as br_file:
with open(output_file, 'wb') as decompressed_file:
decompressed_file.write(brotli.decompress(br_file.read()))
def rangeSet(src: str) -> List[Tuple[int, int]]:
src_set = [int(item) for item in src.split(',')]
if len(src_set) != src_set[0] + 1:
raise ValueError(f'Error parsing data to rangeSet: {src}')
return [(src_set[i], src_set[i+1]) for i in range(1, len(src_set), 2)]
def parseTransferList(path: str) -> Tuple[int, int, List]:
if not isTransferList(path):
raise ValueError(f"The file '{path}' does not appear to be a valid transfer list file.")
with open(path, 'r') as trans_list:
version = int(trans_list.readline())
new_blocks = int(trans_list.readline())
if version >= 2:
trans_list.readline()
trans_list.readline()
commands = []
for line in trans_list:
cmd, *params = line.split()
if cmd in ['erase', 'new', 'zero']:
commands.append([cmd, rangeSet(params[0])])
elif not cmd[0].isdigit():
raise ValueError(f'Command "{cmd}" is not valid.')
return version, new_blocks, commands
def processFile(new_data_file: BinaryIO, output_img: BinaryIO, commands: List, max_file_size: int):
for command in commands:
if command[0] == 'new':
for block in command[1]:
begin, end = block
block_count = end - begin
print(f'Copying {block_count} blocks into position {begin}...')
output_img.seek(begin * BLOCK_SIZE)
output_img.write(new_data_file.read(block_count * BLOCK_SIZE))
else:
print(f'Skipping command {command[0]}...')
if output_img.tell() < max_file_size:
output_img.truncate(max_file_size)
def main(transfer_list_file: str, new_data_file: str, output_image_file: str):
version, new_blocks, commands = parseTransferList(transfer_list_file)
android_versions = {
1: 'Android 5.0',
2: 'Android 5.1',
3: 'Android 6.0',
4: 'Android 7.0 or Higher'
}
print(f'{android_versions.get(version, "Unknown")} Version Image Detected')
output_img_path = Path(output_image_file)
if output_img_path.exists():
raise FileExistsError(f'Output file "{output_img_path}" already exists')
decompressed_file = None
if 'lzma' in new_data_file.lower():
print("LZMA file detected. Decompressing...")
decompressed_file = new_data_file + '.decompressed'
decompressLzmaDat(new_data_file, decompressed_file)
new_data_file = decompressed_file
print("Decompression Completed!")
elif new_data_file.lower().endswith('.br'):
print("Brotli file detected. Decompressing...")
decompressed_file = new_data_file + '.decompressed'
decompressBrotliDat(new_data_file, decompressed_file)
new_data_file = decompressed_file
print("Decompression Completed!")
with open(new_data_file, 'rb') as new_data, output_img_path.open('wb') as output_img:
all_block_sets = [i for command in commands for i in command[1]]
max_file_size = max(pair[1] for pair in all_block_sets) * BLOCK_SIZE
processFile(new_data, output_img, commands, max_file_size)
print(f'Done! Output image: {output_img_path.resolve()}')
if decompressed_file:
os.remove(decompressed_file)
print("Temporary decompressed file removed.")
if __name__ == '__main__':
if len(sys.argv) < 3:
print('Usage: sdat2img_v2.py <transfer.list> <dat|dat.lzma|dat.br> [raw.img]')
print('<transfer.list>:Transfer List File')
print('<dat|dat.lzma|dat.br>:New Dat File (Support Uncompressed, LZMA or Brotli)')
print('[raw.img]:Output File Name of Raw Image\n')
sys.exit(1)
transfer_list_file = sys.argv[1]
new_data_file = sys.argv[2]
if len(sys.argv) > 3:
output_image_file = sys.argv[3]
else:
base_name = os.path.basename(sys.argv[1]).split('.')[0]
output_image_file = f"{base_name}.raw.img"
try:
main(transfer_list_file, new_data_file, output_image_file)
except Exception as e:
print(f"An error occurred: {e}", file=sys.stderr)
sys.exit(1)