mirror of
https://github.com/platomav/BIOSUtilities.git
synced 2025-05-13 22:54:46 -04:00
Panasonic BIOS Package Extractor v4.0
Added ability to parse nested Panasonic BIOS update executable directly Restructured logic to allow more flexibility on input executable parsing Populated code type hints and applied multiple small improvements
This commit is contained in:
parent
fdfdab011d
commit
bdf926f6fd
2 changed files with 103 additions and 108 deletions
|
@ -11,13 +11,15 @@ import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from re import Match
|
||||||
|
|
||||||
import pefile
|
import pefile
|
||||||
|
|
||||||
from dissect.util.compression import lznt1
|
from dissect.util.compression import lznt1
|
||||||
|
|
||||||
from common.comp_szip import is_szip_supported, szip_decompress
|
from common.comp_szip import is_szip_supported, szip_decompress
|
||||||
from common.path_ops import get_path_files, make_dirs, path_stem, safe_name
|
from common.path_ops import get_path_files, make_dirs, path_stem, safe_name
|
||||||
from common.pe_ops import get_pe_file, get_pe_info, is_pe_file, show_pe_info
|
from common.pe_ops import get_pe_desc, get_pe_file, is_pe_file, show_pe_info
|
||||||
from common.patterns import PAT_MICROSOFT_CAB
|
from common.patterns import PAT_MICROSOFT_CAB
|
||||||
from common.system import printer
|
from common.system import printer
|
||||||
from common.templates import BIOSUtility
|
from common.templates import BIOSUtility
|
||||||
|
@ -25,81 +27,73 @@ from common.text_ops import file_to_bytes
|
||||||
|
|
||||||
from AMI_PFAT_Extract import is_ami_pfat, parse_pfat_file
|
from AMI_PFAT_Extract import is_ami_pfat, parse_pfat_file
|
||||||
|
|
||||||
TITLE = 'Panasonic BIOS Package Extractor v3.0'
|
TITLE = 'Panasonic BIOS Package Extractor v4.0'
|
||||||
|
|
||||||
|
|
||||||
def is_panasonic_pkg(in_file):
|
def is_panasonic_pkg(input_object: str | bytes | bytearray) -> bool:
|
||||||
""" Check if input is Panasonic BIOS Package PE """
|
""" Check if input is Panasonic BIOS Package PE """
|
||||||
|
|
||||||
in_buffer = file_to_bytes(in_file)
|
pe_file: pefile.PE | None = get_pe_file(input_object, silent=True)
|
||||||
|
|
||||||
pe_file = get_pe_file(in_buffer, silent=True)
|
|
||||||
|
|
||||||
if not pe_file:
|
if not pe_file:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pe_info = get_pe_info(pe_file, silent=True)
|
if get_pe_desc(pe_file, silent=True).decode('utf-8', 'ignore').upper() not in (PAN_PE_DESC_UNP, PAN_PE_DESC_UPD):
|
||||||
|
|
||||||
if not pe_info:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if pe_info.get(b'FileDescription', b'').upper() != b'UNPACK UTILITY':
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not PAT_MICROSOFT_CAB.search(in_buffer):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def panasonic_cab_extract(buffer, extract_path, padding=0):
|
def panasonic_pkg_name(input_object: str | bytes | bytearray) -> str:
|
||||||
|
""" Get Panasonic BIOS Package file name, when applicable """
|
||||||
|
|
||||||
|
if isinstance(input_object, str) and os.path.isfile(input_object):
|
||||||
|
return safe_name(path_stem(input_object))
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def panasonic_cab_extract(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> str:
|
||||||
""" Search and Extract Panasonic BIOS Package PE CAB archive """
|
""" Search and Extract Panasonic BIOS Package PE CAB archive """
|
||||||
|
|
||||||
pe_path, pe_file, pe_info = [None] * 3
|
input_data: bytes = file_to_bytes(input_object)
|
||||||
|
|
||||||
cab_bgn = PAT_MICROSOFT_CAB.search(buffer).start()
|
cab_match: Match[bytes] | None = PAT_MICROSOFT_CAB.search(input_data)
|
||||||
cab_len = int.from_bytes(buffer[cab_bgn + 0x8:cab_bgn + 0xC], 'little')
|
|
||||||
cab_end = cab_bgn + cab_len
|
|
||||||
|
|
||||||
cab_bin = buffer[cab_bgn:cab_end]
|
if cab_match:
|
||||||
|
cab_bgn: int = cab_match.start()
|
||||||
|
|
||||||
cab_tag = f'[0x{cab_bgn:06X}-0x{cab_end:06X}]'
|
cab_end: int = cab_bgn + int.from_bytes(input_data[cab_bgn + 0x8:cab_bgn + 0xC], 'little')
|
||||||
|
|
||||||
cab_path = os.path.join(extract_path, f'CAB_{cab_tag}.cab')
|
cab_tag: str = f'[0x{cab_bgn:06X}-0x{cab_end:06X}]'
|
||||||
|
|
||||||
|
cab_path: str = os.path.join(extract_path, f'CAB_{cab_tag}.cab')
|
||||||
|
|
||||||
with open(cab_path, 'wb') as cab_file:
|
with open(cab_path, 'wb') as cab_file:
|
||||||
cab_file.write(cab_bin) # Store CAB archive
|
cab_file.write(input_data[cab_bgn:cab_end]) # Store CAB archive
|
||||||
|
|
||||||
if is_szip_supported(cab_path, padding, check=True):
|
if is_szip_supported(cab_path, padding, check=True):
|
||||||
printer(f'Panasonic BIOS Package > PE > CAB {cab_tag}', padding)
|
printer(f'Panasonic BIOS Package > PE > CAB {cab_tag}', padding)
|
||||||
|
|
||||||
if szip_decompress(cab_path, extract_path, 'CAB', padding + 4, check=True) == 0:
|
if szip_decompress(cab_path, extract_path, 'CAB', padding + 4, check=True) == 0:
|
||||||
os.remove(cab_path) # Successful extraction, delete CAB archive
|
os.remove(cab_path) # Successful extraction, delete CAB archive
|
||||||
else:
|
|
||||||
return pe_path, pe_file, pe_info
|
|
||||||
else:
|
|
||||||
return pe_path, pe_file, pe_info
|
|
||||||
|
|
||||||
for file_path in get_path_files(extract_path):
|
for extracted_file_path in get_path_files(extract_path):
|
||||||
pe_file = get_pe_file(file_path, padding, silent=True)
|
extracted_pe_file: pefile.PE | None = get_pe_file(extracted_file_path, padding, silent=True)
|
||||||
|
|
||||||
if pe_file:
|
if extracted_pe_file:
|
||||||
pe_info = get_pe_info(pe_file, padding, silent=True)
|
extracted_pe_desc: bytes = get_pe_desc(extracted_pe_file, silent=True)
|
||||||
|
|
||||||
if pe_info.get(b'FileDescription', b'').upper() == b'BIOS UPDATE':
|
if extracted_pe_desc.decode('utf-8', 'ignore').upper() == PAN_PE_DESC_UPD:
|
||||||
pe_path = file_path
|
return extracted_file_path
|
||||||
|
|
||||||
break
|
return ''
|
||||||
else:
|
|
||||||
return pe_path, pe_file, pe_info
|
|
||||||
|
|
||||||
return pe_path, pe_file, pe_info
|
|
||||||
|
|
||||||
|
|
||||||
def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0):
|
def panasonic_res_extract(pe_file: pefile.PE, extract_path: str, pe_name: str = '', padding: int = 0) -> bool:
|
||||||
""" Extract & Decompress Panasonic BIOS Update PE RCDATA (LZNT1) """
|
""" Extract & Decompress Panasonic BIOS Update PE RCDATA (LZNT1) """
|
||||||
|
|
||||||
is_rcdata = False
|
is_rcdata: bool = False
|
||||||
|
|
||||||
# When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to RCDATA Directories
|
# When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to RCDATA Directories
|
||||||
pe_file.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']])
|
pe_file.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']])
|
||||||
|
@ -110,40 +104,40 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0):
|
||||||
is_rcdata = True
|
is_rcdata = True
|
||||||
|
|
||||||
for resource in entry.directory.entries:
|
for resource in entry.directory.entries:
|
||||||
res_bgn = resource.directory.entries[0].data.struct.OffsetToData
|
res_bgn: int = resource.directory.entries[0].data.struct.OffsetToData
|
||||||
res_len = resource.directory.entries[0].data.struct.Size
|
res_len: int = resource.directory.entries[0].data.struct.Size
|
||||||
res_end = res_bgn + res_len
|
res_end: int = res_bgn + res_len
|
||||||
|
|
||||||
res_bin = pe_file.get_data(res_bgn, res_len)
|
res_bin: bytes = pe_file.get_data(res_bgn, res_len)
|
||||||
|
|
||||||
res_tag = f'{pe_name} [0x{res_bgn:06X}-0x{res_end:06X}]'
|
res_tag: str = f'{pe_name} [0x{res_bgn:06X}-0x{res_end:06X}]'.strip()
|
||||||
|
|
||||||
res_out = os.path.join(extract_path, f'{res_tag}')
|
res_out: str = os.path.join(extract_path, f'{res_tag}')
|
||||||
|
|
||||||
printer(res_tag, padding + 4)
|
printer(res_tag, padding)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res_raw = lznt1.decompress(res_bin[0x8:])
|
res_raw: bytes = lznt1.decompress(res_bin[0x8:])
|
||||||
|
|
||||||
if len(res_raw) != int.from_bytes(res_bin[0x4:0x8], 'little'):
|
if len(res_raw) != int.from_bytes(res_bin[0x4:0x8], 'little'):
|
||||||
raise ValueError('LZNT1_DECOMPRESS_BAD_SIZE')
|
raise ValueError('LZNT1_DECOMPRESS_BAD_SIZE')
|
||||||
|
|
||||||
printer('Succesfull LZNT1 decompression via Dissect!', padding + 8)
|
printer('Succesfull LZNT1 decompression via Dissect!', padding + 4)
|
||||||
except Exception as error: # pylint: disable=broad-except
|
except Exception as error: # pylint: disable=broad-except
|
||||||
logging.debug('Error: LZNT1 decompression of %s failed: %s', res_tag, error)
|
logging.debug('Error: LZNT1 decompression of %s failed: %s', res_tag, error)
|
||||||
|
|
||||||
res_raw = res_bin
|
res_raw = res_bin
|
||||||
|
|
||||||
printer('Succesfull PE Resource extraction!', padding + 8)
|
printer('Succesfull PE Resource extraction!', padding + 4)
|
||||||
|
|
||||||
# Detect & Unpack AMI BIOS Guard (PFAT) BIOS image
|
# Detect & Unpack AMI BIOS Guard (PFAT) BIOS image
|
||||||
if is_ami_pfat(res_raw):
|
if is_ami_pfat(res_raw):
|
||||||
pfat_dir = os.path.join(extract_path, res_tag)
|
pfat_dir: str = os.path.join(extract_path, res_tag)
|
||||||
|
|
||||||
parse_pfat_file(res_raw, pfat_dir, padding + 12)
|
parse_pfat_file(res_raw, pfat_dir, padding + 8)
|
||||||
else:
|
else:
|
||||||
if is_pe_file(res_raw):
|
if is_pe_file(res_raw):
|
||||||
res_ext = 'exe'
|
res_ext: str = 'exe'
|
||||||
elif res_raw.startswith(b'[') and res_raw.endswith((b'\x0D\x0A', b'\x0A')):
|
elif res_raw.startswith(b'[') and res_raw.endswith((b'\x0D\x0A', b'\x0A')):
|
||||||
res_ext = 'txt'
|
res_ext = 'txt'
|
||||||
else:
|
else:
|
||||||
|
@ -153,9 +147,9 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0):
|
||||||
printer(new_line=False)
|
printer(new_line=False)
|
||||||
|
|
||||||
for line in io.BytesIO(res_raw).readlines():
|
for line in io.BytesIO(res_raw).readlines():
|
||||||
line_text = line.decode('utf-8', 'ignore').rstrip()
|
line_text: str = line.decode('utf-8', 'ignore').rstrip()
|
||||||
|
|
||||||
printer(line_text, padding + 12, new_line=False)
|
printer(line_text, padding + 8, new_line=False)
|
||||||
|
|
||||||
with open(f'{res_out}.{res_ext}', 'wb') as out_file:
|
with open(f'{res_out}.{res_ext}', 'wb') as out_file:
|
||||||
out_file.write(res_raw)
|
out_file.write(res_raw)
|
||||||
|
@ -163,78 +157,73 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0):
|
||||||
return is_rcdata
|
return is_rcdata
|
||||||
|
|
||||||
|
|
||||||
def panasonic_img_extract(pe_name, pe_path, pe_file, extract_path, padding=0):
|
def panasonic_img_extract(pe_file: pefile.PE, extract_path: str, pe_name: str = '', padding: int = 0) -> bool:
|
||||||
""" Extract Panasonic BIOS Update PE Data when RCDATA is not available """
|
""" Extract Panasonic BIOS Update PE Data when RCDATA is not available """
|
||||||
|
|
||||||
pe_data = file_to_bytes(pe_path)
|
pe_data: bytes = bytes(pe_file.__data__)
|
||||||
|
|
||||||
sec_bgn = pe_file.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY[
|
sec_bgn: int = pe_file.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY[
|
||||||
'IMAGE_DIRECTORY_ENTRY_SECURITY']].VirtualAddress
|
'IMAGE_DIRECTORY_ENTRY_SECURITY']].VirtualAddress
|
||||||
|
|
||||||
img_bgn = pe_file.OPTIONAL_HEADER.BaseOfData + pe_file.OPTIONAL_HEADER.SizeOfInitializedData
|
img_bgn: int = pe_file.OPTIONAL_HEADER.BaseOfData + pe_file.OPTIONAL_HEADER.SizeOfInitializedData
|
||||||
img_end = sec_bgn or len(pe_data)
|
img_end: int = sec_bgn or len(pe_data)
|
||||||
|
|
||||||
img_bin = pe_data[img_bgn:img_end]
|
img_bin: bytes = pe_data[img_bgn:img_end]
|
||||||
|
|
||||||
img_tag = f'{pe_name} [0x{img_bgn:X}-0x{img_end:X}]'
|
img_tag: str = f'{pe_name} [0x{img_bgn:X}-0x{img_end:X}]'.strip()
|
||||||
|
|
||||||
img_out = os.path.join(extract_path, f'{img_tag}.bin')
|
img_out: str = os.path.join(extract_path, f'{img_tag}.bin')
|
||||||
|
|
||||||
printer(img_tag, padding + 4)
|
printer(img_tag, padding)
|
||||||
|
|
||||||
with open(img_out, 'wb') as out_img:
|
with open(img_out, 'wb') as out_img:
|
||||||
out_img.write(img_bin)
|
out_img.write(img_bin)
|
||||||
|
|
||||||
printer('Succesfull PE Data extraction!', padding + 8)
|
printer('Succesfull PE Data extraction!', padding + 4)
|
||||||
|
|
||||||
return bool(img_bin)
|
return bool(img_bin)
|
||||||
|
|
||||||
|
|
||||||
def panasonic_pkg_extract(input_file, extract_path, padding=0):
|
def panasonic_pkg_extract(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> int:
|
||||||
""" Parse & Extract Panasonic BIOS Package PE """
|
""" Parse & Extract Panasonic BIOS Package PE """
|
||||||
|
|
||||||
input_buffer = file_to_bytes(input_file)
|
upd_pe_file: pefile.PE | None = get_pe_file(input_object, padding)
|
||||||
|
|
||||||
|
upd_pe_name: str = panasonic_pkg_name(input_object)
|
||||||
|
|
||||||
|
printer(f'Panasonic BIOS Package > PE ({upd_pe_name})\n'.replace(' ()', ''), padding)
|
||||||
|
|
||||||
|
show_pe_info(upd_pe_file, padding + 4)
|
||||||
|
|
||||||
make_dirs(extract_path, delete=True)
|
make_dirs(extract_path, delete=True)
|
||||||
|
|
||||||
pkg_pe_file = get_pe_file(input_buffer, padding)
|
upd_pe_path: str = panasonic_cab_extract(input_object, extract_path, padding + 8)
|
||||||
|
|
||||||
if not pkg_pe_file:
|
upd_padding: int = padding
|
||||||
return 2
|
|
||||||
|
|
||||||
pkg_pe_info = get_pe_info(pkg_pe_file, padding)
|
if upd_pe_path:
|
||||||
|
upd_padding = padding + 16
|
||||||
|
|
||||||
if not pkg_pe_info:
|
upd_pe_name = panasonic_pkg_name(upd_pe_path)
|
||||||
return 3
|
|
||||||
|
|
||||||
pkg_pe_name = path_stem(input_file)
|
printer(f'Panasonic BIOS Update > PE ({upd_pe_name})\n'.replace(' ()', ''), upd_padding)
|
||||||
|
|
||||||
printer(f'Panasonic BIOS Package > PE ({pkg_pe_name})\n', padding)
|
upd_pe_file = get_pe_file(upd_pe_path, upd_padding)
|
||||||
|
|
||||||
show_pe_info(pkg_pe_info, padding + 4)
|
show_pe_info(upd_pe_file, upd_padding + 4)
|
||||||
|
|
||||||
upd_pe_path, upd_pe_file, upd_pe_info = panasonic_cab_extract(input_buffer, extract_path, padding + 4)
|
|
||||||
|
|
||||||
if not (upd_pe_path and upd_pe_file and upd_pe_info):
|
|
||||||
return 4
|
|
||||||
|
|
||||||
upd_pe_name = safe_name(path_stem(upd_pe_path))
|
|
||||||
|
|
||||||
printer(f'Panasonic BIOS Update > PE ({upd_pe_name})\n', padding + 12)
|
|
||||||
|
|
||||||
show_pe_info(upd_pe_info, padding + 16)
|
|
||||||
|
|
||||||
is_upd_res, is_upd_img = False, False
|
|
||||||
|
|
||||||
is_upd_res = panasonic_res_extract(upd_pe_name, upd_pe_file, extract_path, padding + 16)
|
|
||||||
|
|
||||||
if not is_upd_res:
|
|
||||||
is_upd_img = panasonic_img_extract(upd_pe_name, upd_pe_path, upd_pe_file, extract_path, padding + 16)
|
|
||||||
|
|
||||||
os.remove(upd_pe_path)
|
os.remove(upd_pe_path)
|
||||||
|
|
||||||
return 0 if is_upd_res or is_upd_img else 1
|
is_upd_extracted: bool = panasonic_res_extract(upd_pe_file, extract_path, upd_pe_name, upd_padding + 8)
|
||||||
|
|
||||||
|
if not is_upd_extracted:
|
||||||
|
is_upd_extracted = panasonic_img_extract(upd_pe_file, extract_path, upd_pe_name, upd_padding + 8)
|
||||||
|
|
||||||
|
return 0 if is_upd_extracted else 1
|
||||||
|
|
||||||
|
|
||||||
|
PAN_PE_DESC_UNP: str = 'UNPACK UTILITY'
|
||||||
|
PAN_PE_DESC_UPD: str = 'BIOS UPDATE'
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
BIOSUtility(title=TITLE, check=is_panasonic_pkg, main=panasonic_pkg_extract).run_utility()
|
BIOSUtility(title=TITLE, check=is_panasonic_pkg, main=panasonic_pkg_extract).run_utility()
|
||||||
|
|
|
@ -20,26 +20,30 @@ def is_pe_file(in_file: str | bytes) -> bool:
|
||||||
def get_pe_file(in_file: str | bytes, padding: int = 0, fast: bool = True, silent: bool = False) -> pefile.PE | None:
|
def get_pe_file(in_file: str | bytes, padding: int = 0, fast: bool = True, silent: bool = False) -> pefile.PE | None:
|
||||||
""" Get pefile object from PE file """
|
""" Get pefile object from PE file """
|
||||||
|
|
||||||
in_buffer = file_to_bytes(in_file)
|
pe_file: pefile.PE | None = None
|
||||||
|
|
||||||
pe_file = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Analyze detected MZ > PE image buffer
|
# Analyze detected MZ > PE image buffer
|
||||||
pe_file = pefile.PE(data=in_buffer, fast_load=fast)
|
pe_file = pefile.PE(data=file_to_bytes(in_file), fast_load=fast)
|
||||||
except Exception as error: # pylint: disable=broad-except
|
except Exception as error: # pylint: disable=broad-except
|
||||||
if not silent:
|
if not silent:
|
||||||
_filename = in_file if type(in_file).__name__ == 'string' else 'buffer'
|
filename: str = in_file if isinstance(in_file, str) else 'buffer'
|
||||||
|
|
||||||
printer(f'Error: Could not get pefile object from {_filename}: {error}!', padding)
|
printer(f'Error: Could not get pefile object from {filename}: {error}!', padding)
|
||||||
|
|
||||||
return pe_file
|
return pe_file
|
||||||
|
|
||||||
|
|
||||||
|
def get_pe_desc(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> bytes:
|
||||||
|
""" Get PE description from pefile object info """
|
||||||
|
|
||||||
|
return get_pe_info(pe_file, padding, silent).get(b'FileDescription', b'')
|
||||||
|
|
||||||
|
|
||||||
def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> dict:
|
def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> dict:
|
||||||
""" Get PE info from pefile object """
|
""" Get PE info from pefile object """
|
||||||
|
|
||||||
pe_info = {}
|
pe_info: dict = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to FileInfo > StringTable
|
# When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to FileInfo > StringTable
|
||||||
|
@ -54,13 +58,15 @@ def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> d
|
||||||
return pe_info
|
return pe_info
|
||||||
|
|
||||||
|
|
||||||
def show_pe_info(pe_info: dict, padding: int = 0) -> None:
|
def show_pe_info(pe_file: pefile.PE, padding: int = 0) -> None:
|
||||||
""" Print PE info from pefile StringTable """
|
""" Print PE info from pefile StringTable """
|
||||||
|
|
||||||
|
pe_info: dict = get_pe_info(pe_file=pe_file, padding=padding)
|
||||||
|
|
||||||
if isinstance(pe_info, dict):
|
if isinstance(pe_info, dict):
|
||||||
for title, value in pe_info.items():
|
for title, value in pe_info.items():
|
||||||
info_title = title.decode('utf-8', 'ignore').strip()
|
info_title: str = title.decode('utf-8', 'ignore').strip()
|
||||||
info_value = value.decode('utf-8', 'ignore').strip()
|
info_value: str = value.decode('utf-8', 'ignore').strip()
|
||||||
|
|
||||||
if info_title and info_value:
|
if info_title and info_value:
|
||||||
printer(f'{info_title}: {info_value}', padding, new_line=False)
|
printer(f'{info_title}: {info_value}', padding, new_line=False)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue