BIOSUtilities v24.10.01

Complete repository overhaul into python project
Re-designed BIOSUtility base template class flow
Re-structured utilities as BIOSUtility inherited
Re-structured project for 3rd-party compatibility
Unified project requirements and package version
Code overhaul with type hints and linting support
Switched external executable dependencies via PATH
BIOSUtility enforces simple check and parse methods
Utilities now work with both path and buffer inputs
Adjusted class, method, function names and parameters
Improved Dell PFS Update Extractor sub-PFAT processing
Improved Award BIOS Module Extractor corruption handling
Improved Apple EFI Image Identifier to expose the EFI ID
Improved Insyde iFlash/iFdPacker Extractor with ISH & PDT
Re-written Apple EFI Package Extractor to support all PKG
This commit is contained in:
Plato Mavropoulos 2024-10-02 00:09:14 +03:00
parent ef50b75ae1
commit cda2fbd0b1
65 changed files with 6239 additions and 5233 deletions

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
# Get Checksum 16-bit
def checksum_16(data: bytes | bytearray, value: int = 0, order: str = 'little') -> int:
""" Calculate Checksum-16 of data, controlling IV and Endianess """
for idx in range(0, len(data), 2):
# noinspection PyTypeChecker
value += int.from_bytes(bytes=data[idx:idx + 2], byteorder=order) # type: ignore
value &= 0xFFFF
return value
# Get Checksum 8-bit XOR
def checksum_8_xor(data: bytes | bytearray, value: int = 0) -> int:
""" Calculate Checksum-8 XOR of data, controlling IV """
for byte in data:
value ^= byte
value ^= 0x0
return value

View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import os
import subprocess
from biosutilities.common.externals import szip_path, tiano_path
from biosutilities.common.system import printer
def szip_code_assert(exit_code: int) -> None:
""" Check 7-Zip bad exit codes (0 OK, 1 Warning) """
if exit_code not in (0, 1):
raise ValueError(f'Bad exit code: {exit_code}')
def is_szip_supported(in_path: str, padding: int = 0, args: list | None = None, silent: bool = True) -> bool:
""" Check if file is 7-Zip supported """
try:
if args is None:
args = []
szip_c: list[str] = [szip_path(), 't', in_path, *args, '-bso0', '-bse0', '-bsp0']
szip_t: subprocess.CompletedProcess[bytes] = subprocess.run(args=szip_c, check=False)
szip_code_assert(exit_code=szip_t.returncode)
except Exception as error: # pylint: disable=broad-except
if not silent:
printer(message=f'Error: 7-Zip could not check support for file {in_path}: {error}!', padding=padding)
return False
return True
def szip_decompress(in_path: str, out_path: str, in_name: str | None, padding: int = 0, args: list | None = None,
check: bool = False, silent: bool = False) -> int:
""" Archive decompression via 7-Zip """
if not in_name:
in_name = 'archive'
try:
if args is None:
args = []
szip_c: list[str] = [szip_path(), 'x', *args, '-aou', '-bso0', '-bse0', '-bsp0', f'-o{out_path}', in_path]
szip_x: subprocess.CompletedProcess[bytes] = subprocess.run(args=szip_c, check=False)
if check:
szip_code_assert(exit_code=szip_x.returncode)
if not os.path.isdir(out_path):
raise OSError(f'Extraction directory not found: {out_path}')
except Exception as error: # pylint: disable=broad-except
if not silent:
printer(message=f'Error: 7-Zip could not extract {in_name} file {in_path}: {error}!', padding=padding)
return 1
if not silent:
printer(message=f'Successful {in_name} decompression via 7-Zip!', padding=padding)
return 0
def efi_compress_sizes(data: bytes | bytearray) -> tuple[int, int]:
""" Get EFI compression sizes """
size_compress: int = int.from_bytes(bytes=data[0x0:0x4], byteorder='little')
size_original: int = int.from_bytes(bytes=data[0x4:0x8], byteorder='little')
return size_compress, size_original
def is_efi_compressed(data: bytes | bytearray, strict: bool = True) -> bool:
""" Check if data is EFI compressed, controlling EOF padding """
size_comp, size_orig = efi_compress_sizes(data=data)
check_diff: bool = size_comp < size_orig
if strict:
check_size: bool = size_comp + 0x8 == len(data)
else:
check_size = size_comp + 0x8 <= len(data)
return check_diff and check_size
def efi_decompress(in_path: str, out_path: str, padding: int = 0, silent: bool = False,
comp_type: str = '--uefi') -> int:
""" EFI/Tiano Decompression via TianoCompress """
try:
subprocess.run(args=[tiano_path(), '-d', in_path, '-o', out_path, '-q', comp_type],
check=True, stdout=subprocess.DEVNULL)
with open(file=in_path, mode='rb') as file:
_, size_orig = efi_compress_sizes(data=file.read())
if os.path.getsize(out_path) != size_orig:
raise OSError('EFI decompressed file & header size mismatch!')
except Exception as error: # pylint: disable=broad-except
if not silent:
printer(message=f'Error: TianoCompress could not extract file {in_path}: {error}!', padding=padding)
return 1
if not silent:
printer(message='Successful EFI decompression via TianoCompress!', padding=padding)
return 0

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import pefile
from biosutilities.common.system import printer
from biosutilities.common.texts import file_to_bytes
def is_ms_pe(in_file: str | bytes) -> bool:
""" Check if input is a PE file """
return bool(ms_pe(in_file=in_file, silent=True))
def ms_pe(in_file: str | bytes, padding: int = 0, fast: bool = True, silent: bool = False) -> pefile.PE | None:
""" Get pefile object from PE file """
pe_file: pefile.PE | None = None
try:
# Analyze detected MZ > PE image buffer
pe_file = pefile.PE(data=file_to_bytes(in_file), fast_load=fast)
except Exception as error: # pylint: disable=broad-except
if not silent:
filename: str = in_file if isinstance(in_file, str) else 'buffer'
printer(message=f'Error: Could not get pefile object from {filename}: {error}!', padding=padding)
return pe_file
def ms_pe_desc(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> bytes:
""" Get PE description from pefile object info """
return ms_pe_info(pe_file=pe_file, padding=padding, silent=silent).get(b'FileDescription', b'')
def ms_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> dict:
""" Get PE info from pefile object """
pe_info: dict = {}
try:
# When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to FileInfo > StringTable
pe_file.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']])
# Retrieve MZ > PE > FileInfo > StringTable information
pe_info = pe_file.FileInfo[0][0].StringTable[0].entries
except Exception as error: # pylint: disable=broad-except
if not silent:
printer(message=f'Error: Could not get PE info from pefile object: {error}!', padding=padding)
return pe_info
def ms_pe_info_show(pe_file: pefile.PE, padding: int = 0) -> None:
""" Print PE info from pefile StringTable """
pe_info: dict = ms_pe_info(pe_file=pe_file, padding=padding)
if isinstance(pe_info, dict):
for title, value in pe_info.items():
info_title: str = title.decode(encoding='utf-8', errors='ignore').strip()
info_value: str = value.decode(encoding='utf-8', errors='ignore').strip()
if info_title and info_value:
printer(message=f'{info_title}: {info_value}', padding=padding, new_line=False)

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import os
import re
import shutil
import sys
from importlib.abc import Loader
from importlib.machinery import ModuleSpec
from importlib.util import module_from_spec, spec_from_file_location
from types import ModuleType
from typing import Type
def big_script_tool() -> Type | None:
""" Get Intel BIOS Guard Script Tool class """
bgst: str | None = shutil.which(cmd='big_script_tool')
if bgst and os.path.isfile(path=bgst):
bgst_spec: ModuleSpec | None = spec_from_file_location(
name='big_script_tool', location=re.sub(r'\.PY$', '.py', bgst))
if bgst_spec and isinstance(bgst_spec.loader, Loader):
bgst_module: ModuleType | None = module_from_spec(spec=bgst_spec)
if bgst_module:
sys.modules['big_script_tool'] = bgst_module
bgst_spec.loader.exec_module(module=bgst_module)
return getattr(bgst_module, 'BigScript')
return None
def comextract_path() -> str:
""" Get ToshibaComExtractor path """
comextract: str | None = shutil.which(cmd='comextract')
if not (comextract and os.path.isfile(path=comextract)):
raise OSError('comextract executable not found!')
return comextract
def szip_path() -> str:
""" Get 7-Zip path """
szip: str | None = shutil.which(cmd='7zzs') or shutil.which(cmd='7z')
if not (szip and os.path.isfile(path=szip)):
raise OSError('7zzs or 7z executable not found!')
return szip
def tiano_path() -> str:
""" Get TianoCompress path """
tiano: str | None = shutil.which(cmd='TianoCompress')
if not (tiano and os.path.isfile(path=tiano)):
raise OSError('TianoCompress executable not found!')
return tiano
def uefifind_path() -> str:
""" Get UEFIFind path """
uefifind: str | None = shutil.which(cmd='UEFIFind')
if not (uefifind and os.path.isfile(path=uefifind)):
raise OSError('UEFIFind executable not found!')
return uefifind
def uefiextract_path() -> str:
""" Get UEFIExtract path """
uefiextract: str | None = shutil.which(cmd='UEFIExtract')
if not (uefiextract and os.path.isfile(path=uefiextract)):
raise OSError('UEFIExtract executable not found!')
return uefiextract

View file

@ -0,0 +1,218 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import os
import re
import shutil
import stat
import sys
from pathlib import Path, PurePath
from typing import Callable, Final
from biosutilities.common.system import system_platform
from biosutilities.common.texts import to_string
MAX_WIN_COMP_LEN: Final[int] = 255
def safe_name(in_name: str) -> str:
"""
Fix illegal/reserved Windows characters
Can also be used to nuke dangerous paths
"""
name_repr: str = repr(in_name).strip("'")
return re.sub(pattern=r'[\\/:"*?<>|]+', repl='_', string=name_repr)
def safe_path(base_path: str, user_paths: str | list | tuple) -> str:
""" Check and attempt to fix illegal/unsafe OS path traversals """
# Convert base path to absolute path
base_path = real_path(in_path=base_path)
# Merge user path(s) to string with OS separators
user_path: str = to_string(in_object=user_paths, sep_char=os.sep)
# Create target path from base + requested user path
target_path: str = norm_path(base_path=base_path, user_path=user_path)
# Check if target path is OS illegal/unsafe
if is_safe_path(base_path=base_path, target_path=target_path):
return target_path
# Re-create target path from base + leveled/safe illegal "path" (now file)
nuked_path: str = norm_path(base_path=base_path, user_path=safe_name(in_name=user_path))
# Check if illegal path leveling worked
if is_safe_path(base_path=base_path, target_path=nuked_path):
return nuked_path
# Still illegal, raise exception to halt execution
raise OSError(f'Encountered illegal path traversal: {user_path}')
def is_safe_path(base_path: str, target_path: str) -> bool:
""" Check for illegal/unsafe OS path traversal """
base_path = real_path(in_path=base_path)
target_path = real_path(in_path=target_path)
common_path: str = os.path.commonpath(paths=(base_path, target_path))
return base_path == common_path
def norm_path(base_path: str, user_path: str) -> str:
""" Create normalized base path + OS separator + user path """
return os.path.normpath(path=base_path + os.sep + user_path)
def real_path(in_path: str) -> str:
""" Get absolute path, resolving any symlinks """
return os.path.realpath(in_path)
def agnostic_path(in_path: str) -> PurePath:
""" Get Windows/Posix OS-agnostic path """
return PurePath(in_path.replace('\\', os.sep))
def path_parent(in_path: str) -> Path:
""" Get absolute parent of path """
return Path(in_path).parent.absolute()
def path_name(in_path: str, limit: bool = False) -> str:
""" Get final path component, with suffix """
comp_name: str = PurePath(in_path).name
is_win: bool = system_platform()[1]
if limit and is_win:
comp_name = comp_name[:MAX_WIN_COMP_LEN - len(extract_suffix())]
return comp_name
def path_stem(in_path: str) -> str:
""" Get final path component, w/o suffix """
return PurePath(in_path).stem
def path_suffixes(in_path: str) -> list[str]:
""" Get list of path file extensions """
return PurePath(in_path).suffixes or ['']
def make_dirs(in_path: str, parents: bool = True, exist_ok: bool = False, delete: bool = False):
""" Create folder(s), controlling parents, existence and prior deletion """
if delete:
delete_dirs(in_path=in_path)
Path.mkdir(Path(in_path), parents=parents, exist_ok=exist_ok)
def delete_dirs(in_path: str) -> None:
""" Delete folder(s), if present """
if Path(in_path).is_dir():
shutil.rmtree(path=in_path, onexc=clear_readonly_callback)
def delete_file(in_path: str) -> None:
""" Delete file, if present """
if Path(in_path).is_file():
clear_readonly(in_path=in_path)
os.remove(path=in_path)
def copy_file(in_path: str, out_path: str, metadata: bool = False) -> None:
""" Copy file to path with or w/o metadata """
if metadata:
shutil.copy2(src=in_path, dst=out_path)
else:
shutil.copy(src=in_path, dst=out_path)
def clear_readonly(in_path: str) -> None:
""" Clear read-only file attribute """
os.chmod(path=in_path, mode=stat.S_IWRITE)
def clear_readonly_callback(in_func: Callable, in_path: str, _) -> None:
""" Clear read-only file attribute (on shutil.rmtree error) """
clear_readonly(in_path=in_path)
in_func(in_path=in_path)
def path_files(in_path: str, follow_links: bool = False) -> list[str]:
""" Walk path to get all files """
file_paths: list[str] = []
for root, _, filenames in os.walk(top=in_path, followlinks=follow_links):
for filename in filenames:
file_paths.append(os.path.abspath(path=os.path.join(root, filename)))
return file_paths
def is_empty_dir(in_path: str, follow_links: bool = False) -> bool:
""" Check if directory is empty (file-wise) """
for _, _, filenames in os.walk(top=in_path, followlinks=follow_links):
if filenames:
return False
return True
def extract_suffix() -> str:
""" Set utility extraction stem """
return '_extracted'
def extract_folder(in_path: str, suffix: str = extract_suffix()) -> str:
""" Get utility extraction directory """
return f'{in_path}{suffix}'
def project_root() -> str:
""" Get project root directory """
return real_path(in_path=str(Path(__file__).parent.parent))
def runtime_root() -> str:
""" Get runtime root directory """
if getattr(sys, 'frozen', False):
root: str = str(Path(sys.executable).parent)
else:
root = project_root()
return real_path(in_path=root)

View file

@ -0,0 +1,125 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import re
from typing import Final
PAT_AMI_PFAT: Final[re.Pattern[bytes]] = re.compile(
pattern=br'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS',
flags=re.DOTALL
)
PAT_AMI_UCP: Final[re.Pattern[bytes]] = re.compile(
pattern=br'@(UAF|HPU).{12}@',
flags=re.DOTALL
)
PAT_APPLE_EFI: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\$IBIOSI\$.{16}\x2E\x00.{6}\x2E\x00.{8}\x2E\x00.{6}\x2E\x00.{20}\x00{2}',
flags=re.DOTALL
)
PAT_APPLE_IM4P: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x16\x04IM4P\x16\x04mefi'
)
PAT_APPLE_PBZX: Final[re.Pattern[bytes]] = re.compile(
pattern=br'pbzx'
)
PAT_APPLE_PKG_XAR: Final[re.Pattern[bytes]] = re.compile(
pattern=br'xar!'
)
PAT_APPLE_PKG_TAR: Final[re.Pattern[bytes]] = re.compile(
pattern=br'<key>IFPkgDescriptionDescription</key>'
)
PAT_AWARD_LZH: Final[re.Pattern[bytes]] = re.compile(
pattern=br'-lh[04567]-'
)
PAT_DELL_FTR: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\xEE\xAA\xEE\x8F\x49\x1B\xE8\xAE\x14\x37\x90'
)
PAT_DELL_HDR: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C',
flags=re.DOTALL
)
PAT_DELL_PKG: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x72\x13\x55\x00.{45}7zXZ',
flags=re.DOTALL
)
PAT_FUJITSU_SFX: Final[re.Pattern[bytes]] = re.compile(
pattern=br'FjSfxBinay\xB2\xAC\xBC\xB9\xFF{4}.{4}\xFF{4}.{4}\xFF{4}\xFC\xFE',
flags=re.DOTALL
)
PAT_INSYDE_IFL: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\$_IFLASH'
)
PAT_INSYDE_SFX: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x0D\x0A;!@InstallEnd@!\x0D\x0A(7z\xBC\xAF\x27|\x6E\xF4\x79\x5F\x4E)'
)
PAT_INTEL_ENG: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))',
flags=re.DOTALL
)
PAT_INTEL_IFD: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x5A\xA5\xF0\x0F.{172}\xFF{16}',
flags=re.DOTALL
)
PAT_MICROSOFT_CAB: Final[re.Pattern[bytes]] = re.compile(
pattern=br'MSCF\x00{4}'
)
PAT_MICROSOFT_MZ: Final[re.Pattern[bytes]] = re.compile(
pattern=br'MZ'
)
PAT_MICROSOFT_PE: Final[re.Pattern[bytes]] = re.compile(
pattern=br'PE\x00{2}'
)
PAT_PHOENIX_TDK: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\$PACK\x00{3}..\x00{2}.\x00{3}',
flags=re.DOTALL
)
PAT_PORTWELL_EFI: Final[re.Pattern[bytes]] = re.compile(
pattern=br'<U{2}>'
)
PAT_TOSHIBA_COM: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x00{2}[\x00-\x02]BIOS.{20}[\x00\x01]',
flags=re.DOTALL
)
PAT_VAIO_CAB: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\xB2\xAC\xBC\xB9\xFF{4}.{4}\xFF{4}.{4}\xFF{4}\xFC\xFE',
flags=re.DOTALL
)
PAT_VAIO_CFG: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\[Setting]\x0D\x0A'
)
PAT_VAIO_CHK: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x0AUseVAIOCheck='
)
PAT_VAIO_EXT: Final[re.Pattern[bytes]] = re.compile(
pattern=br'\x0AExtractPathByUser='
)

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import ctypes
from typing import Any, Final
CHAR: Final[type[ctypes.c_char] | int] = ctypes.c_char
UINT8: Final[type[ctypes.c_ubyte] | int] = ctypes.c_ubyte
UINT16: Final[type[ctypes.c_ushort] | int] = ctypes.c_ushort
UINT32: Final[type[ctypes.c_uint] | int] = ctypes.c_uint
UINT64: Final[type[ctypes.c_uint64] | int] = ctypes.c_uint64
def ctypes_struct(buffer: bytes | bytearray, start_offset: int, class_object: Any,
param_list: list | None = None) -> Any:
"""
https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky
"""
if not param_list:
param_list = []
structure: Any = class_object(*param_list)
struct_len: int = ctypes.sizeof(structure)
struct_data: bytes | bytearray = buffer[start_offset:start_offset + struct_len]
least_len: int = min(len(struct_data), struct_len)
ctypes.memmove(ctypes.addressof(structure), struct_data, least_len)
return structure

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import sys
import platform
from biosutilities.common.texts import to_string
def system_platform() -> tuple[str, bool, bool]:
""" Get OS platform """
sys_os: str = platform.system()
is_win: bool = sys_os == 'Windows'
is_lnx: bool = sys_os in ('Linux', 'Darwin')
return sys_os, is_win, is_lnx
def python_version() -> tuple:
""" Get Python version """
return sys.version_info
def printer(message: str | list | tuple | None = None, padding: int = 0, new_line: bool = True,
pause: bool = False, sep_char: str = ' ') -> None:
""" Show message(s), controlling padding, newline, pausing & separator """
message_string: str = to_string(in_object='' if message is None else message, sep_char=sep_char)
message_output: str = '\n' if new_line else ''
for line_index, line_text in enumerate(iterable=message_string.split('\n')):
line_newline: str = '' if line_index == 0 else '\n'
message_output += f'{line_newline}{" " * padding}{line_text}'
if pause:
input(message_output)
else:
print(message_output)

View file

@ -0,0 +1,157 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
import os
import sys
from argparse import ArgumentParser, Namespace
from typing import Final
from biosutilities import __version__
from biosutilities.common.paths import (delete_dirs, extract_folder, is_empty_dir, path_files,
path_name, path_parent, real_path, runtime_root)
from biosutilities.common.system import system_platform, python_version, printer
from biosutilities.common.texts import remove_quotes, to_boxed, to_ordinal
class BIOSUtility:
""" Base utility class for BIOSUtilities """
TITLE: str = 'BIOS Utility'
ARGUMENTS: list[tuple[list[str], dict[str, str]]] = []
MAX_FAT32_ITEMS: Final[int] = 65535
MIN_PYTHON_VER: Final[tuple[int, int]] = (3, 10)
def __init__(self, arguments: list[str] | None = None) -> None:
self._check_sys_py()
self._check_sys_os()
self.title: str = f'{self.TITLE.strip()} v{__version__}'
argparser: ArgumentParser = ArgumentParser(allow_abbrev=False)
argparser.add_argument('paths', nargs='*')
argparser.add_argument('-e', '--auto-exit', help='skip user action prompts', action='store_true')
argparser.add_argument('-o', '--output-dir', help='output extraction directory')
for argument in self.ARGUMENTS:
argparser.add_argument(*argument[0], **argument[1]) # type: ignore
sys_argv: list[str] = arguments if isinstance(arguments, list) and arguments else sys.argv[1:]
self.arguments: Namespace = argparser.parse_known_args(sys_argv)[0]
self._input_files: list[str] = []
self._output_path: str = ''
def run_utility(self, padding: int = 0) -> int:
""" Run utility after checking for supported format """
self.show_version(padding=padding)
self._setup_input_files(padding=padding)
self._setup_output_dir(padding=padding)
exit_code: int = len(self._input_files)
for input_file in self._input_files:
input_name: str = path_name(in_path=input_file, limit=True)
printer(message=input_name, padding=padding + 4)
if not self.check_format(input_object=input_file):
printer(message='Error: This is not a supported format!', padding=padding + 8)
continue
extract_path: str = os.path.join(self._output_path, extract_folder(in_path=input_name))
if os.path.isdir(extract_path):
for suffix in range(2, self.MAX_FAT32_ITEMS):
renamed_path: str = f'{os.path.normpath(path=extract_path)}_{to_ordinal(in_number=suffix)}'
if not os.path.isdir(renamed_path):
extract_path = renamed_path
break
if self.parse_format(input_object=input_file, extract_path=extract_path,
padding=padding + 8) in [0, None]:
exit_code -= 1
if is_empty_dir(in_path=extract_path):
delete_dirs(in_path=extract_path)
printer(message='Done!\n' if not self.arguments.auto_exit else None, pause=not self.arguments.auto_exit)
return exit_code
def show_version(self, is_boxed: bool = True, padding: int = 0) -> None:
""" Show title and version of utility """
printer(message=to_boxed(in_text=self.title) if is_boxed else self.title, new_line=False, padding=padding)
def parse_format(self, input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> int | None:
""" Process input object as a specific supported format """
raise NotImplementedError(f'Method "parse_format" not implemented at {__name__}')
def check_format(self, input_object: str | bytes | bytearray) -> bool:
""" Check if input object is of specific supported format """
raise NotImplementedError(f'Method "check_format" not implemented at {__name__}')
def _setup_input_files(self, padding: int = 0) -> None:
input_paths: list[str] = self.arguments.paths
if not input_paths:
input_paths = [remove_quotes(in_text=input(f'\n{" " * padding}Enter input file or directory path: '))]
for input_path in [input_path for input_path in input_paths if input_path]:
input_path_real: str = real_path(in_path=input_path)
if os.path.isdir(input_path_real):
self._input_files.extend(path_files(input_path_real))
else:
self._input_files.append(input_path_real)
def _setup_output_dir(self, padding: int = 0) -> None:
output_path: str = self.arguments.output_dir
if not output_path:
output_path = remove_quotes(in_text=input(f'\n{" " * padding}Enter output directory path: '))
if not output_path and self._input_files:
output_path = str(path_parent(in_path=self._input_files[0]))
self._output_path = output_path or runtime_root()
def _check_sys_py(self) -> None:
""" Check Python Version """
sys_py: tuple = python_version()
if sys_py < self.MIN_PYTHON_VER:
min_py_str: str = '.'.join(map(str, self.MIN_PYTHON_VER))
sys_py_str: str = '.'.join(map(str, sys_py[:2]))
raise RuntimeError(f'Python >= {min_py_str} required, not {sys_py_str}')
@staticmethod
def _check_sys_os() -> None:
""" Check OS Platform """
os_tag, is_win, is_lnx = system_platform()
if not (is_win or is_lnx):
raise OSError(f'Unsupported operating system: {os_tag}')

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3 -B
# coding=utf-8
"""
Copyright (C) 2022-2024 Plato Mavropoulos
"""
def to_string(in_object: str | list | tuple, sep_char: str = '') -> str:
""" Get string from given input object """
if isinstance(in_object, (list, tuple)):
out_string: str = sep_char.join(map(str, in_object))
else:
out_string = str(in_object)
return out_string
def to_ordinal(in_number: int) -> str:
"""
Get ordinal (textual) representation of input numerical value
https://leancrew.com/all-this/2020/06/ordinals-in-python/ by Dr. Drang
"""
ordinals: list[str] = ['th', 'st', 'nd', 'rd'] + ['th'] * 10
numerical: int = in_number % 100
if numerical > 13:
return f'{in_number}{ordinals[numerical % 10]}'
return f'{in_number}{ordinals[numerical]}'
def file_to_bytes(in_object: str | bytes | bytearray) -> bytes:
""" Get bytes from given buffer or file path """
if not isinstance(in_object, (bytes, bytearray)):
with open(file=to_string(in_object=in_object), mode='rb') as object_data:
object_bytes: bytes = object_data.read()
else:
object_bytes = in_object
return object_bytes
def bytes_to_hex(in_buffer: bytes, order: str, data_len: int, slice_len: int | None = None) -> str:
""" Converts bytes to hex string, controlling endianess, data size and string slicing """
# noinspection PyTypeChecker
return f'{int.from_bytes(bytes=in_buffer, byteorder=order):0{data_len * 2}X}'[:slice_len] # type: ignore
def remove_quotes(in_text: str) -> str:
""" Remove leading/trailing quotes from path """
out_text: str = to_string(in_object=in_text).strip()
if len(out_text) >= 2:
if (out_text[0] == '"' and out_text[-1] == '"') or (out_text[0] == "'" and out_text[-1] == "'"):
out_text = out_text[1:-1]
return out_text
def to_boxed(in_text: str) -> str:
""" Box string into two horizontal lines of same size """
box_line: str = '-' * len(to_string(in_object=in_text))
return f'{box_line}\n{in_text}\n{box_line}'