Updated Dell PFS/PKG Update Extractor v6.0_a15

Padding is now added between sub-PFS PFAT Entry gaps

Thanks for the help @NikolajSchlej
This commit is contained in:
Plato Mavropoulos 2022-10-07 01:32:30 +03:00
parent 6de50c422f
commit c1f4ab9121

View file

@ -7,7 +7,7 @@ Dell PFS/PKG Update Extractor
Copyright (C) 2018-2022 Plato Mavropoulos
"""
TITLE = 'Dell PFS/PKG Update Extractor v6.0_a13'
TITLE = 'Dell PFS/PKG Update Extractor v6.0_a15'
import os
import io
@ -192,82 +192,82 @@ class DellPfsPfatMetadata(ctypes.LittleEndianStructure):
# Each section starts with a 0x30 header, which begins with pattern 72135500.
# The section length is found at 0x10-0x14 and its (optional) MD5 hash at 0x20-0x30.
# Section data can be raw or LZMA2 (7zXZ) compressed. The latter contains the PFS update image.
def is_pfs_pkg(in_file):
in_buffer = file_to_bytes(in_file)
def is_pfs_pkg(input_file):
input_buffer = file_to_bytes(input_file)
return PAT_DELL_PKG.search(in_buffer)
return PAT_DELL_PKG.search(input_buffer)
# The Dell PFS update images usually contain multiple sections.
# Each section is zlib-compressed with header pattern ********++EEAA761BECBB20F1E651--789C,
# where ******** is the zlib stream size, ++ is the section type and -- the header Checksum XOR 8.
# The "Firmware" section has type AA and its files are stored in PFS format.
# The "Utility" section has type BB and its files are stored in PFS, BIN or 7z formats.
def is_pfs_hdr(in_file):
in_buffer = file_to_bytes(in_file)
def is_pfs_hdr(input_file):
input_buffer = file_to_bytes(input_file)
return bool(PAT_DELL_HDR.search(in_buffer))
return bool(PAT_DELL_HDR.search(input_buffer))
# Each section is followed by the footer pattern ********EEAAEE8F491BE8AE143790--,
# where ******** is the zlib stream size and ++ the footer Checksum XOR 8.
def is_pfs_ftr(in_file):
in_buffer = file_to_bytes(in_file)
def is_pfs_ftr(input_file):
input_buffer = file_to_bytes(input_file)
return bool(PAT_DELL_FTR.search(in_buffer))
return bool(PAT_DELL_FTR.search(input_buffer))
# Check if input is Dell PFS/PKG image
def is_dell_pfs(in_file):
in_buffer = file_to_bytes(in_file)
def is_dell_pfs(input_file):
input_buffer = file_to_bytes(input_file)
is_pkg = is_pfs_pkg(in_buffer)
is_pkg = is_pfs_pkg(input_buffer)
is_hdr = is_pfs_hdr(in_buffer)
is_hdr = is_pfs_hdr(input_buffer)
is_ftr = is_pfs_ftr(in_buffer)
is_ftr = is_pfs_ftr(input_buffer)
return bool(is_pkg or is_hdr and is_ftr)
# Parse & Extract Dell PFS/PKG Update image
def pfs_pkg_parse(in_file, output_path, padding=0, structure=True, advanced=True):
in_buffer = file_to_bytes(in_file)
def pfs_pkg_parse(input_file, extract_path, padding=0, structure=True, advanced=True):
input_buffer = file_to_bytes(input_file)
make_dirs(output_path, delete=True)
make_dirs(extract_path, delete=True)
is_dell_pkg = is_pfs_pkg(in_buffer)
is_dell_pkg = is_pfs_pkg(input_buffer)
if is_dell_pkg:
pfs_results = thinos_pkg_extract(in_buffer, output_path)
pfs_results = thinos_pkg_extract(input_buffer, extract_path)
else:
pfs_results = {path_stem(in_file) if os.path.isfile(in_file) else 'Image': in_buffer}
pfs_results = {path_stem(input_file) if os.path.isfile(input_file) else 'Image': input_buffer}
# Parse each Dell PFS image contained in the input file
for pfs_index,(pfs_name,pfs_buffer) in enumerate(pfs_results.items(), start=1):
# At ThinOS PKG packages, multiple PFS images may be included in separate model-named folders
pfs_path = os.path.join(output_path, f'{pfs_index} {pfs_name}') if is_dell_pkg else output_path
pfs_path = os.path.join(extract_path, f'{pfs_index} {pfs_name}') if is_dell_pkg else extract_path
# Parse each PFS ZLIB section
for zlib_offset in get_section_offsets(pfs_buffer):
# Call the PFS ZLIB section parser function
pfs_section_parse(pfs_buffer, zlib_offset, pfs_path, pfs_name, pfs_index, 1, False, padding, structure, advanced)
# Extract Dell ThinOS PKG 7zXZ
def thinos_pkg_extract(in_file, output_path):
in_buffer = file_to_bytes(in_file)
def thinos_pkg_extract(input_file, extract_path):
input_buffer = file_to_bytes(input_file)
# Initialize PFS results (Name: Buffer)
pfs_results = {}
# Search input image for ThinOS PKG 7zXZ header
thinos_pkg_match = PAT_DELL_PKG.search(in_buffer)
thinos_pkg_match = PAT_DELL_PKG.search(input_buffer)
lzma_len_off = thinos_pkg_match.start() + 0x10
lzma_len_int = int.from_bytes(in_buffer[lzma_len_off:lzma_len_off + 0x4], 'little')
lzma_len_int = int.from_bytes(input_buffer[lzma_len_off:lzma_len_off + 0x4], 'little')
lzma_bin_off = thinos_pkg_match.end() - 0x5
lzma_bin_dat = in_buffer[lzma_bin_off:lzma_bin_off + lzma_len_int]
lzma_bin_dat = input_buffer[lzma_bin_off:lzma_bin_off + lzma_len_int]
# Check if the compressed 7zXZ stream is complete
if len(lzma_bin_dat) != lzma_len_int:
return pfs_results
working_path = os.path.join(output_path, 'THINOS_PKG_TEMP')
working_path = os.path.join(extract_path, 'THINOS_PKG_TEMP')
make_dirs(working_path, delete=True)
@ -319,7 +319,7 @@ def get_section_offsets(buffer):
return pfs_zlib_list
# Dell PFS ZLIB Section Parser
def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, pfs_count, is_rec, padding=0, structure=True, advanced=True):
def pfs_section_parse(zlib_data, zlib_start, extract_path, pfs_name, pfs_index, pfs_count, is_rec, padding=0, structure=True, advanced=True):
is_zlib_error = False # Initialize PFS ZLIB-related error state
section_type = zlib_data[zlib_start - 0x1] # Byte before PFS ZLIB Section pattern is Section Type (e.g. AA, BB)
@ -329,7 +329,7 @@ def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, p
printer(f'Extracting Dell PFS {pfs_index} > {pfs_name} > {section_name}', padding)
# Set PFS ZLIB Section extraction sub-directory path
section_path = os.path.join(output_path, safe_name(section_name))
section_path = os.path.join(extract_path, safe_name(section_name))
# Create extraction sub-directory and delete old (if present, not in recursions)
make_dirs(section_path, delete=(not is_rec), parents=True, exist_ok=True)
@ -395,7 +395,7 @@ def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, p
pfs_extract(section_data, pfs_index, pfs_name, pfs_count, section_path, padding, structure, advanced)
# Parse & Extract Dell PFS Volume
def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, padding=0, structure=True, advanced=True):
def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, extract_path, padding=0, structure=True, advanced=True):
# Show PFS Volume indicator
if structure:
printer('PFS Volume:', padding)
@ -619,7 +619,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, padding=0,
sub_pfs_name = f'{info_all[pfs_count - 2][1]} v{info_all[pfs_count - 2][2]}' if info_all else ' UNKNOWN'
# Set the sub-PFS output path (create sub-folders for each sub-PFS and its ZLIB sections)
sub_pfs_path = os.path.join(output_path, f'{pfs_count} {safe_name(sub_pfs_name)}')
sub_pfs_path = os.path.join(extract_path, f'{pfs_count} {safe_name(sub_pfs_name)}')
# Recursively call the PFS ZLIB Section Parser function for the sub-PFS Volume (pfs_index = pfs_count)
pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, True, padding + 4, structure, advanced)
@ -691,7 +691,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, padding=0,
# Write/Extract PFS Entry files
for file in write_files:
full_name = f'{pfs_index} {pfs_name} -- {file_index} {file_name} v{file_version}' # Full PFS Entry Name
pfs_file_write(file[0], file[1], file_type, full_name, output_path, padding, structure, advanced)
pfs_file_write(file[0], file[1], file_type, full_name, extract_path, padding, structure, advanced)
# Get PFS Footer Data after PFS Header Payload
pfs_footer = buffer[PFS_HEAD_LEN + pfs_hdr.PayloadSize:PFS_HEAD_LEN + pfs_hdr.PayloadSize + PFS_FOOT_LEN]
@ -766,7 +766,7 @@ def parse_pfat_pfs(entry_hdr, entry_data, padding=0, structure=True):
pfat_footer = entry_data[PFS_HEAD_LEN + entry_hdr.PayloadSize:PFS_HEAD_LEN + entry_hdr.PayloadSize + PFS_FOOT_LEN]
# Parse all sub-PFS Payload PFAT Entries
pfat_data_all = [] # Storage for all sub-PFS PFAT Entries Order/Offset & Payload/Raw Data
pfat_entries_all = [] # Storage for all sub-PFS PFAT Entries Order/Offset & Payload/Raw Data
pfat_entry_start = 0 # Increasing sub-PFS PFAT Entry start offset
pfat_entry_index = 0 # Increasing sub-PFS PFAT Entry count index
_, pfs_entry_size = get_pfs_entry(pfat_payload, 0) # Get initial PFS PFAT Entry Size for loop
@ -855,26 +855,36 @@ def parse_pfat_pfs(entry_hdr, entry_data, padding=0, structure=True):
pfat_entry_data_raw = pfat_payload_data # Prefer Data from PFAT Block, in case PFAT Entry differs
# Store each sub-PFS PFAT Entry Order/Offset and Payload/Raw Data (w/o PFAT)
pfat_data_all.append((pfat_entry_off, pfat_entry_data_raw))
pfat_entries_all.append((pfat_entry_off, pfat_entry_data_raw))
pfat_entry_start = pfat_next_entry # Next sub-PFS PFAT Entry starts after sub-PFS Entry Metadata Signature
pfat_entry_index += 1
pfat_data_all.sort() # Sort all sub-PFS PFAT Entries payloads/data based on their Order/Offset
pfat_entries_all.sort() # Sort all sub-PFS PFAT Entries payloads/data based on their Order/Offset
entry_data = b'' # Initialize new sub-PFS Entry Data
for pfat_data in pfat_data_all:
entry_data += pfat_data[1] # Merge all sub-PFS PFAT Entry Payload/Raw into the final sub-PFS Entry Data
sorted_start_expected = pfat_entries_all[0][0] # Initialize sub-PFS PFAT Entry expected Offset
final_entry_data = b'' # Initialize final sub-PFS Entry Data from ordered PFAT Entries
# Verify that the Order/Offset of the last PFAT Entry w/ its Size matches the final sub-PFS Entry Data Size
if len(entry_data) != pfat_data_all[-1][0] + len(pfat_data_all[-1][1]):
# Parse all sorted sub-PFS PFAT Entries and merge their payload/data
for sorted_start,sorted_data in pfat_entries_all:
# Fill any data gaps between sorted sub-PFS PFAT Entries with padding
if sorted_start != sorted_start_expected:
# The sub-PFS PFAT Entry expected Start is the previous Offset + Size
final_entry_data += b'\xFF' * (sorted_start - sorted_start_expected)
final_entry_data += sorted_data # Append sorted sub-PFS PFAT Entry payload/data
sorted_start_expected = sorted_start + len(sorted_data) # Set next sub-PFS PFAT Entry expected Start
# Verify that the end offset of the last PFAT Entry matches the final sub-PFS Entry Data Size
if len(final_entry_data) != pfat_entries_all[-1][0] + len(pfat_entries_all[-1][1]):
printer('Error: Detected sub-PFS PFAT Entry Buffer & Last Offset Size mismatch!', padding + 8)
# Analyze sub-PFS Footer Structure
chk_pfs_ftr(pfat_footer, pfat_payload, entry_hdr.PayloadSize, 'Sub-PFS', padding + 4, structure)
return entry_data
return final_entry_data
# Get Dell PFS Entry Structure & Size via its Version
def get_pfs_entry(buffer, offset):