BIOSUtilities/biosutilities/common/paths.py
Plato Mavropoulos eda154b0f2 BIOSUtilities v24.10.06
24.10.06

Changed BIOSUtility.parse_format() to return a boolean
Changed 7-Zip and EFI decompressors to return booleans
Apple EFI Package Extractor support for InstallAssistant
Apple EFI Image Identifier support for Apple ROM Version
Added Apple EFI Image Identifier class instance attributes
Improved flow of non-PATH external executable dependencies
Fixed crash when attempting to clear read-only attribute
Fixed incompatibility with Python versions prior to 3.12
Performance improvements when initializing BIOSUtilities
Improved argument naming and definitions of "main" script
Improved the README with new "main" and Apple EFI changes
2024-10-07 01:24:12 +03:00

218 lines
5.6 KiB
Python

#!/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, onerror=clear_readonly_callback) # pylint: disable=deprecated-argument
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(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)