366 lines
13 KiB
Python
366 lines
13 KiB
Python
|
#!/usr/bin/env python3
|
||
|
#
|
||
|
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
|
||
|
#
|
||
|
|
||
|
from typing import Optional, Union
|
||
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||
|
|
||
|
from qiling.const import QL_OS, QL_OS_POSIX
|
||
|
|
||
|
AnyPurePath = Union[PurePosixPath, PureWindowsPath]
|
||
|
|
||
|
|
||
|
class QlOsPath:
|
||
|
"""Virtual to host path manipulations helper.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, rootfs: str, cwd: str, emulos: QL_OS) -> None:
|
||
|
"""Initialize a path manipulation object.
|
||
|
|
||
|
Args:
|
||
|
rootfs : host path to serve as the virtual root directory
|
||
|
cwd : virtual current working directory
|
||
|
emuls : emulated operating system
|
||
|
"""
|
||
|
|
||
|
nt_path_os = (QL_OS.WINDOWS, QL_OS.DOS)
|
||
|
posix_path_os = QL_OS_POSIX
|
||
|
|
||
|
# rootfs is a local directory on the host, and expected to exist
|
||
|
self._rootfs_path = Path(rootfs).resolve(strict=True)
|
||
|
|
||
|
# determine how virtual paths should be handled
|
||
|
if emulos in nt_path_os:
|
||
|
self.PureVirtualPath = PureWindowsPath
|
||
|
|
||
|
elif emulos in posix_path_os:
|
||
|
self.PureVirtualPath = PurePosixPath
|
||
|
|
||
|
else:
|
||
|
raise ValueError(f'unexpected os type: {emulos}')
|
||
|
|
||
|
self.cwd = cwd
|
||
|
|
||
|
# <TEMPORARY>
|
||
|
# temporary aliases for backward compatibility
|
||
|
self.transform_to_relative_path = self.virtual_abspath
|
||
|
self.transform_to_real_path = self.virtual_to_host_path
|
||
|
# </TEMPORARY>
|
||
|
|
||
|
@staticmethod
|
||
|
def __strip_parent_refs(path: AnyPurePath) -> AnyPurePath:
|
||
|
"""Strip leading parent dir references, if any.
|
||
|
"""
|
||
|
|
||
|
if path.parts:
|
||
|
pardir = r'..'
|
||
|
|
||
|
while path.parts[0] == pardir:
|
||
|
path = path.relative_to(pardir)
|
||
|
|
||
|
return path
|
||
|
|
||
|
@property
|
||
|
def root(self) -> str:
|
||
|
return str(self._cwd_anchor)
|
||
|
|
||
|
@property
|
||
|
def cwd(self) -> str:
|
||
|
return str(self._cwd_anchor / self._cwd_vpath)
|
||
|
|
||
|
@cwd.setter
|
||
|
def cwd(self, virtpath: str) -> None:
|
||
|
vpath = self.PureVirtualPath(virtpath)
|
||
|
|
||
|
if not vpath.is_absolute():
|
||
|
raise ValueError(f'current working directory must be an absolute path: {virtpath}')
|
||
|
|
||
|
# extract the virtual path anchor so we can append cwd path to rootfs later on.
|
||
|
# however, we will still need the anchor to provide full virtual paths
|
||
|
cwd_anchor = self.PureVirtualPath(vpath.anchor)
|
||
|
cwd_vpath = vpath.relative_to(cwd_anchor)
|
||
|
|
||
|
cwd_vpath = QlOsPath.__strip_parent_refs(cwd_vpath)
|
||
|
|
||
|
self._cwd_anchor = cwd_anchor
|
||
|
self._cwd_vpath = cwd_vpath
|
||
|
|
||
|
def __virtual_abspath(self, virtpath: Union[str, AnyPurePath]) -> AnyPurePath:
|
||
|
"""Get the absolute virtual path representation of a virtual path.
|
||
|
This method does not follow symbolic links or parent directory references.
|
||
|
|
||
|
Args:
|
||
|
virtpath: virtual path to resolve, either relative or absolute
|
||
|
|
||
|
Returns: An absolute virtual path
|
||
|
"""
|
||
|
|
||
|
vpath = self.PureVirtualPath(virtpath)
|
||
|
|
||
|
if vpath.is_absolute():
|
||
|
return vpath
|
||
|
|
||
|
# rebase on top the current working directory. in case vpath is an absolute
|
||
|
# path, cwd will be discarded
|
||
|
absvpath = self._cwd_vpath / vpath
|
||
|
|
||
|
# referencing root's parent directory should circle back to root.
|
||
|
# remove any leading parent dir references absvpath might have
|
||
|
absvpath = QlOsPath.__strip_parent_refs(absvpath)
|
||
|
|
||
|
return self._cwd_anchor / absvpath
|
||
|
|
||
|
def __resolved_vsymlink(self, basepath: Path, name: str):
|
||
|
"""Attempt to resolve a virtual symbolic link.
|
||
|
|
||
|
A virtual symbolic link points to a location within the virtual file
|
||
|
system, not to be confused with paths on the host file system.
|
||
|
|
||
|
For example:
|
||
|
a vsymlink that points to '/var/tmp' should be resolved to
|
||
|
'my/rootfs/path/var/tmp' on the host
|
||
|
"""
|
||
|
|
||
|
fullpath = self._rootfs_path / basepath / name
|
||
|
vpath = None
|
||
|
|
||
|
if fullpath.is_symlink():
|
||
|
resolved = fullpath.resolve(strict=False)
|
||
|
|
||
|
try:
|
||
|
# the resolve method turns fullpath into an absolute path on the host,
|
||
|
# but we need it to be an absolute virtual path. to convert the host
|
||
|
# path to virtual we have to make sure it resides within rootfs.
|
||
|
#
|
||
|
# if the host path is indeed under rootfs, we can rebase the virtual
|
||
|
# portion on top of rootfs and return an absolute virtual path.
|
||
|
#
|
||
|
# returning an absolute path will discard the accumulated vpath.
|
||
|
|
||
|
vpath = self._cwd_anchor / resolved.relative_to(self._rootfs_path)
|
||
|
except ValueError:
|
||
|
# failing to convert the host path into a virtual one means that either
|
||
|
# the symbolic link is already an absolute virtual path, or it is pointing
|
||
|
# to an external directory on the host file system, which means it does
|
||
|
# not reflect a virtual path.
|
||
|
#
|
||
|
# on both cases we may return the path as-is, discarding the accumulated
|
||
|
# vpath.
|
||
|
#
|
||
|
# that, however, may not work in case the host filesystem and the virtual
|
||
|
# one are not of the same type (e.g. a virtual Windows path on top of a
|
||
|
# Linux host): the absolute path will not discard the accumulated vpath
|
||
|
# and will be appended - which is a corner case we do not know how to
|
||
|
# handle efficiently.
|
||
|
|
||
|
vpath = resolved
|
||
|
|
||
|
return vpath
|
||
|
|
||
|
# this will work only if hosting os = virtual os
|
||
|
def __virtual_resolve(self, virtpath: Union[str, AnyPurePath]) -> AnyPurePath:
|
||
|
"""Resolve a virtual path, including symbolic links and directory
|
||
|
references it might include. Path must not include circular symbolic
|
||
|
links.
|
||
|
|
||
|
Args:
|
||
|
virtpath: virtual path to resolve, either relative or absolute
|
||
|
|
||
|
Returns: An absolute virtual path
|
||
|
"""
|
||
|
|
||
|
vpath = self.PureVirtualPath(virtpath)
|
||
|
|
||
|
# if not already, turn vpath into an absolute path
|
||
|
if not vpath.is_absolute():
|
||
|
vpath = self.__virtual_abspath(vpath)
|
||
|
|
||
|
# accumulate paths as we progress through the resolution process.
|
||
|
#
|
||
|
# since symlink inspection and resolution can only be done on an
|
||
|
# actual file system, each step in the progress has to be translated
|
||
|
# into its correpsonding host path. that is the reason we keep track
|
||
|
# on the acumulated host path in parallel to the virtual one
|
||
|
#
|
||
|
# note: the reason we do not set acc_hpath to rootfs is to prevent
|
||
|
# parent dir refs from traversing beyond rootfs directory.
|
||
|
|
||
|
acc_hpath = Path()
|
||
|
acc_vpath = self.PureVirtualPath(vpath.anchor)
|
||
|
|
||
|
# eliminate virtual path's anchor to allow us accumulate its
|
||
|
# parts on top of rootfs
|
||
|
vpath = vpath.relative_to(vpath.anchor)
|
||
|
|
||
|
for part in vpath.parts:
|
||
|
if part == '..':
|
||
|
acc_hpath = acc_hpath.parent
|
||
|
acc_vpath = acc_vpath.parent
|
||
|
|
||
|
else:
|
||
|
# if this is a symlink attempt to resolve it
|
||
|
vtemp = self.__resolved_vsymlink(acc_hpath, part)
|
||
|
|
||
|
# not a symlink; accumulate path part
|
||
|
if vtemp is None:
|
||
|
acc_hpath = acc_hpath / part
|
||
|
acc_vpath = acc_vpath / part
|
||
|
|
||
|
else:
|
||
|
# rebase it on top of the accumulated virtual path
|
||
|
new_vpath = acc_vpath / vtemp
|
||
|
|
||
|
# recursively resolve the new virtual path we got
|
||
|
vres = self.__virtual_resolve(new_vpath)
|
||
|
|
||
|
acc_hpath = Path(vres)
|
||
|
acc_vpath = vres
|
||
|
|
||
|
return acc_vpath
|
||
|
|
||
|
def __virtual_to_host_path(self, virtpath: Union[str, AnyPurePath]) -> Path:
|
||
|
"""Convert a virtual path to its corresponding path on the host.
|
||
|
|
||
|
This method partialy normalizes the virtual path and does not resolve
|
||
|
references to parent directories neither virtual symbolic links
|
||
|
"""
|
||
|
|
||
|
absvpath = self.__virtual_abspath(virtpath)
|
||
|
|
||
|
# remove path anchor to allow path to be rebased
|
||
|
vpath = absvpath.relative_to(absvpath.anchor)
|
||
|
|
||
|
# rebase virtual path on top of rootfs to get the host path
|
||
|
return self._rootfs_path / vpath
|
||
|
|
||
|
def __is_safe_host_path(self, hostpath: Path, strict: bool = False) -> bool:
|
||
|
"""Sanitize the specified host path and make sure it does not traverse out
|
||
|
of the rootfs directory hierarchy.
|
||
|
|
||
|
Args:
|
||
|
hostpath : a local path to sanitize
|
||
|
strict : whether to raise an error if target path does not exist
|
||
|
|
||
|
Returns: whether the path is safe to use
|
||
|
"""
|
||
|
|
||
|
# canonicalization before assertion: resolve any relative path references and
|
||
|
# symbolic links that may exist.
|
||
|
#
|
||
|
# in case strict is set to True and the path does not exist, a FileNotFoundError
|
||
|
# is raised. this error is left for the user to catch and handle
|
||
|
hostpath = hostpath.resolve(strict=strict)
|
||
|
|
||
|
try:
|
||
|
# to prevent path-traversal issues we have to make sure hostpath ended up
|
||
|
# as a subpath of rootfs. the following method will fail if that is not
|
||
|
# the case
|
||
|
_ = hostpath.relative_to(self._rootfs_path)
|
||
|
except ValueError:
|
||
|
return False
|
||
|
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
def host_to_virtual_path(self, hostpath: str) -> str:
|
||
|
"""Convert a host path to its corresponding virtual path relative to rootfs.
|
||
|
|
||
|
Args:
|
||
|
hostpath : host path
|
||
|
|
||
|
Returns: the corresponding virtual path
|
||
|
|
||
|
Raises: `ValueError` in case the host path does not map to a rootfs location
|
||
|
"""
|
||
|
|
||
|
resolved = Path(hostpath).resolve(strict=False)
|
||
|
virtpath = self._cwd_anchor / resolved.relative_to(self._rootfs_path)
|
||
|
|
||
|
return str(virtpath)
|
||
|
|
||
|
def is_virtual_abspath(self, virtpath: str) -> bool:
|
||
|
"""Determine whether a given virtual path is absolute.
|
||
|
|
||
|
Args:
|
||
|
virtpath : virtual path to query
|
||
|
|
||
|
Returns: `True` if `virtpath` is an absolute path, `False` if relative
|
||
|
"""
|
||
|
|
||
|
vpath = self.PureVirtualPath(virtpath)
|
||
|
|
||
|
return vpath.is_absolute()
|
||
|
|
||
|
def virtual_abspath(self, virtpath: str) -> str:
|
||
|
"""Convert a relative virtual path to an absolute virtual path based
|
||
|
on the current working directory.
|
||
|
|
||
|
Args:
|
||
|
virtpath : relative virtual path
|
||
|
|
||
|
Returns: the absolute virtual path
|
||
|
"""
|
||
|
|
||
|
absvpath = self.__virtual_abspath(virtpath)
|
||
|
|
||
|
return str(absvpath)
|
||
|
|
||
|
def virtual_to_host_path(self, virtpath: str) -> str:
|
||
|
"""Convert a virtual path to its corresponding path on the hosting system.
|
||
|
|
||
|
Args:
|
||
|
virtpath : path on the emulated system. the path may be either absolute
|
||
|
or relative
|
||
|
|
||
|
Returns: the corresponding path on the hosting system
|
||
|
"""
|
||
|
|
||
|
absvpath = self.__virtual_resolve(virtpath)
|
||
|
hostpath = self.__virtual_to_host_path(absvpath)
|
||
|
|
||
|
return str(hostpath)
|
||
|
|
||
|
def is_safe_host_path(self, hostpath: str) -> bool:
|
||
|
hpath = Path(hostpath)
|
||
|
|
||
|
return self.__is_safe_host_path(hpath, strict=False)
|
||
|
|
||
|
@staticmethod
|
||
|
def __host_casefold_path(hostpath: str) -> Optional[str]:
|
||
|
# assuming posix host
|
||
|
p = PurePosixPath(hostpath)
|
||
|
norm = Path(p.anchor)
|
||
|
|
||
|
for elem in p.relative_to(norm).parts:
|
||
|
folded = elem.casefold()
|
||
|
|
||
|
try:
|
||
|
norm = next(entry for entry in norm.iterdir() if entry.name.casefold() == folded)
|
||
|
except StopIteration:
|
||
|
return None
|
||
|
|
||
|
return str(norm)
|
||
|
|
||
|
def host_casefold_path(self, hostpath: str) -> Optional[str]:
|
||
|
"""As opposed to POSIX paths, NT paths are case insensitive and may be specified
|
||
|
in multiple ways. When emulating an NT file system on top of POSIX one, virtual
|
||
|
NT paths and files might not be found because they are specified in a different
|
||
|
case than the one that is actually used on the hosting POSIX system.
|
||
|
|
||
|
This method translates a case insensitive path into the actual case sensitive
|
||
|
name that is used on the hosting POSIX file system.
|
||
|
|
||
|
Args:
|
||
|
hostpath: a path on the host, case insensitive
|
||
|
|
||
|
Returns: the corresponding path on the host system, or None if the path does not
|
||
|
exist
|
||
|
"""
|
||
|
|
||
|
# only relevant if the emulated file system is NT-based
|
||
|
if self.PureVirtualPath is PureWindowsPath:
|
||
|
return QlOsPath.__host_casefold_path(hostpath)
|
||
|
|
||
|
return hostpath
|