mirror of
https://github.com/platomav/BIOSUtilities.git
synced 2025-05-19 09:45:42 -04:00
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:
parent
ef50b75ae1
commit
cda2fbd0b1
65 changed files with 6239 additions and 5233 deletions
31
biosutilities/common/checksums.py
Normal file
31
biosutilities/common/checksums.py
Normal 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
|
122
biosutilities/common/compression.py
Normal file
122
biosutilities/common/compression.py
Normal 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
|
72
biosutilities/common/executables.py
Normal file
72
biosutilities/common/executables.py
Normal 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)
|
94
biosutilities/common/externals.py
Normal file
94
biosutilities/common/externals.py
Normal 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
|
218
biosutilities/common/paths.py
Normal file
218
biosutilities/common/paths.py
Normal 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)
|
125
biosutilities/common/patterns.py
Normal file
125
biosutilities/common/patterns.py
Normal 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='
|
||||
)
|
38
biosutilities/common/structs.py
Normal file
38
biosutilities/common/structs.py
Normal 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
|
48
biosutilities/common/system.py
Normal file
48
biosutilities/common/system.py
Normal 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)
|
157
biosutilities/common/templates.py
Normal file
157
biosutilities/common/templates.py
Normal 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}')
|
73
biosutilities/common/texts.py
Normal file
73
biosutilities/common/texts.py
Normal 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}'
|
Loading…
Add table
Add a link
Reference in a new issue