Module refinery.lib.inno.pathfunc
Setup's rules for paths: Inno Setup's PathFunc unit, which Setup and its support functions use,
the wizard's tidying of folder and group names, and Windows' GetFullPathName, which both rest on.
Inno Setup 7 changed how drives and slashes are read; PathFunc.of() gives the rules of a release.
Expand source code Browse git
"""
Setup's rules for paths: Inno Setup's `PathFunc` unit, which Setup and its support functions use,
the wizard's tidying of folder and group names, and Windows' `GetFullPathName`, which both rest on.
Inno Setup 7 changed how drives and slashes are read; `PathFunc.of` gives the rules of a release.
"""
from __future__ import annotations
from refinery.lib.inno.delphi.strings import trim
from refinery.lib.inno.release import SetupRelease
_SLASHES = '\\/'
_DEVICES = frozenset({
'AUX',
'CON',
'CONIN$',
'CONOUT$',
'NUL',
'PRN',
*(F'COM{k}' for k in '123456789\u00b9\u00b2\u00b3'),
*(F'LPT{k}' for k in '123456789\u00b9\u00b2\u00b3'),
})
"""
The names of devices that Windows maps a path to, depending on its version.
"""
def _drive_root(folder: str) -> str:
"""
The root of the drive or share of a full path: `C:\\` or `\\\\server\\share`.
"""
if folder[:2] == '\\\\':
server_end = folder.find('\\', 2)
if server_end < 0:
return folder
share_end = folder.find('\\', server_end + 1)
return folder if share_end < 0 else folder[:share_end]
return F'{folder[:2]}\\'
def _components(text: str) -> list[str]:
return [part for part in text.split('\\') if part]
def full_path_name(name: str, folder: str) -> str | None:
"""
Windows' `GetFullPathName` for the given path and current folder, which is a full path; `None`
where Windows fails, for a path that is empty or only spaces. Slashes become backslashes and
repeated separators one; `.` and `..` parts go, never above the root of a drive or a share; a
part before others that ends in one dot loses it, and the last part loses its trailing dots and
spaces. A drive-relative path is relative to the current folder when it is on that drive, else
to the root of the drive. Device names and the prefixes `\\\\?\\` and `\\\\.\\` are not modelled.
"""
if not name.strip(' '):
return None
path = name.replace('/', '\\')
if path[:3] in ('\\\\?', '\\\\.') and (len(path) == 3 or path[3] == '\\'):
raise NotImplementedError(F'The emulator does not model the path {name}.')
trailing = path.endswith('\\')
if path[:2] == '\\\\':
server_end = path.find('\\', 2)
if server_end < 0:
return F'\\\\{path[2:].rstrip(". ")}'
share_start = server_end
while share_start < len(path) and path[share_start] == '\\':
share_start += 1
share_end = path.find('\\', share_start)
if share_end < 0:
share = path[share_start:].rstrip('. ')
return F'{path[:server_end]}\\{share}'
root = F'{path[:server_end]}\\{path[share_start:share_end]}'
stack: list[str] = []
rest = path[share_end:]
elif path[1:2] == ':':
drive = path[:2]
if path[2:3] == '\\':
root = F'{drive}\\'
stack = []
rest = path[3:]
else:
if folder[1:2] == ':' and folder[0].upper() == drive[0].upper():
root = _drive_root(folder)
stack = _components(folder[len(root):])
else:
root = F'{drive}\\'
stack = []
rest = path[2:]
else:
root = _drive_root(folder)
if path[:1] == '\\':
stack = []
else:
stack = _components(folder[len(root):])
rest = path
for part in rest.split('\\'):
if not part or part == '.':
continue
if part == '..':
if stack:
stack.pop()
continue
stack.append(part)
middle = len(stack) if trailing else len(stack) - 1
for index in range(middle):
part = stack[index]
if part.endswith('.') and not part.endswith('..'):
stack[index] = part[:-1]
if stack and not trailing:
last = stack[-1].rstrip('. ')
if last.split('.')[0].split(':')[0].rstrip(' ').upper() in _DEVICES:
raise NotImplementedError(F'The emulator does not model the device name in the path {name}.')
if last:
stack[-1] = last
else:
stack.pop()
trailing = True
if not stack:
return root if root.endswith('\\') or not trailing else F'{root}\\'
full = '\\'.join((root.rstrip('\\'), *stack))
return F'{full}\\' if trailing else full
class PathFunc:
"""
The functions of Inno Setup's `PathFunc` unit that Setup and its support functions use, in one
release: `inno7` selects the rules of Inno Setup 7, which reads the namespace prefixes
`\\\\?\\` and `\\\\.\\` as part of a drive, keeps a third leading slash, and fails a path that
it cannot expand instead of keeping it.
"""
def __init__(self, inno7: bool):
self.inno7 = inno7
@staticmethod
def of(release: SetupRelease) -> PathFunc:
return PATH_FUNC_7 if release.since((7, 0, 0)) else PATH_FUNC_6
def __repr__(self):
return F'{self.__class__.__name__}({7 if self.inno7 else 6})'
@staticmethod
def add_backslash(text: str) -> str:
if text and text[-1] not in _SLASHES:
return F'{text}\\'
return text
@staticmethod
def remove_backslash(text: str) -> str:
return text.rstrip(_SLASHES)
def remove_backslash_unless_root(self, text: str) -> str:
drive = self.drive_part_length(text, True)
end = len(text)
while end > drive and text[end - 1] in _SLASHES:
end -= 1
return text[:end]
def drive_part_length(self, name: str, significant_slash: bool = False) -> int:
"""
`PathDrivePartLengthEx`: the length of `x:` or of `\\\\server\\share` at the start of the
name; with `significant_slash`, also of the slash after `x:` and of a leading slash.
"""
length = len(name)
if length >= 2 and name[0] in _SLASHES and name[1] in _SLASHES:
if self.inno7:
return self._namespace_drive_part_length(name, significant_slash)
index = 2
separators = 0
while index < length:
if name[index] in _SLASHES:
separators += 1
if separators >= 2:
break
index += 1
while index < length and name[index] in _SLASHES:
index += 1
else:
index += 1
return index
if length >= 1 and name[0] in _SLASHES:
return 1 if significant_slash else 0
if length >= 2 and name[1] == ':':
if significant_slash and length >= 3 and name[2] in _SLASHES:
return 3
return 2
return 0
@staticmethod
def _namespace_drive_part_length(name: str, significant_slash: bool) -> int:
length = len(name)
namespace = False
drive = False
components = 2
current = 0
start = index = 2
while index < length:
if name[index] not in _SLASHES:
index += 1
continue
size = index - start
if current == 0:
if size == 1 and name[start] in '?.':
namespace = True
elif current == 1 and namespace:
if size == 2:
if name[start + 1] == ':':
drive = True
elif size == 3 and name[start:start + 3].upper() == 'UNC':
components += 2
current += 1
if current >= components:
if drive and significant_slash:
index += 1
break
index += 1
while index < length and name[index] in _SLASHES:
index += 1
start = index
return index
def path_part_length(self, name: str, slashes_after: bool) -> int:
"""
`PathPathPartLength`: the length of the path before the last part of the name, with the
slashes after it when `slashes_after` is set, never less than the drive part.
"""
result = self.drive_part_length(name, True)
keep = result
index = result
while index < len(name):
if name[index] in _SLASHES:
result = index + 1 if slashes_after else keep
index += 1
else:
index += 1
keep = index
return result
def extension_position(self, name: str) -> int | None:
"""
The index of the last dot in the last part of the name.
"""
position = None
for index in range(self.path_part_length(name, True), len(name)):
if name[index] == '.':
position = index
return position
def change_ext(self, name: str, extension: str) -> str:
if (position := self.extension_position(name)) is None:
return F'{name}{extension}'
return F'{name[:position]}{extension}'
def extract_dir(self, name: str) -> str:
return name[:self.path_part_length(name, False)]
def extract_drive(self, name: str) -> str:
return name[:self.drive_part_length(name)]
def extract_ext(self, name: str) -> str:
if (position := self.extension_position(name)) is None:
return ''
return name[position:]
def extract_name(self, name: str) -> str:
return name[self.path_part_length(name, True):]
def extract_path(self, name: str) -> str:
return name[:self.path_part_length(name, True)]
def normalize_slashes(self, text: str) -> str:
"""
`PathNormalizeSlashes`: forward slashes become backslashes and repeated backslashes one,
except for two at the start; Inno Setup 7 also keeps a third.
"""
chars = list(text.replace('/', '\\'))
if self.inno7:
end = 3 if chars[:2] == ['\\', '\\'] else 1
for index in range(len(chars) - 1, end - 1, -1):
if chars[index] == '\\' and chars[index - 1] == '\\':
del chars[index]
else:
index = 0
while index < len(chars) - 1:
if chars[index] == '\\' and chars[index + 1] == '\\' and index > 0:
del chars[index + 1]
else:
index += 1
return ''.join(chars)
def expand(self, name: str, folder: str) -> str:
"""
`PathExpand`: the full path of the name in the given current folder. Before Inno Setup 7,
a path that Windows cannot expand, or whose full path has 4096 characters or more, stays as
it is; Inno Setup 7 gives an empty text for a path that is empty or only spaces and fails
for any other path that it cannot expand.
"""
full = full_path_name(name, folder)
if self.inno7:
if not name.strip(' '):
return ''
if full is None or len(name) > 0x7FFE or len(full) > 0x7FFE:
raise PathFuncError(F'PathExpand: GetFullPathName failed (length: {len(name)})')
return full
if full is None or len(full) >= 4096:
return name
return full
def extract_relative_path(self, base: str, destination: str) -> str:
"""
Setup's `NewExtractRelativePath`: the path of the destination relative to the folder of
the base, or the destination when the two are on different drives.
"""
base = self.normalize_slashes(base)
destination = self.normalize_slashes(destination)
base_drive = self.extract_drive(base)
destination_drive = self.extract_drive(destination)
if base_drive.lower() != destination_drive.lower():
return destination
base_parts = _Cursor(self.extract_path(base)[len(base_drive):])
destination_parts = _Cursor(self.extract_path(destination)[len(destination_drive):])
base_part = base_parts.next()
destination_part = destination_parts.next()
while (
base_part is not None
and destination_part is not None
and base_part.lower() == destination_part.lower()
):
base_part = base_parts.next()
destination_part = destination_parts.next()
result = '..\\' * base_parts.remaining()
if destination_part:
result = F'{result}{destination_part}\\'
return F'{result}{destination_parts.rest()}{self.extract_name(destination)}'
def tidy_up_dir_name(self, path: str) -> str:
"""
The wizard's `TidyUpDirName`: trims spaces, normalizes slashes and removes trailing
backslashes until nothing changes.
"""
while True:
path = self.remove_backslash_unless_root(self.normalize_slashes(trim(path)))
if len(path) == len(trim(path)):
return path
def tidy_up_group_name(self, path: str) -> str:
"""
The wizard's `TidyUpGroupName`: trims spaces, removes leading, repeated and trailing
backslashes until nothing changes.
"""
while True:
path = trim(path).lstrip(_SLASHES)
path = self.remove_backslash(self.normalize_slashes(path))
if len(path) == len(trim(path)):
return path
def _components_end_in_spaces(path: str) -> bool:
return any(part.endswith(' ') for part in path.split('\\'))
def _components_are_dots(path: str) -> bool:
return any(part.strip(' ') and not part.strip(' ').strip('.') for part in path.split('\\'))
def _has_control_characters(path: str) -> bool:
return any(c <= '\x1F' for c in path)
_BAD_CHARACTERS = frozenset('/:*?"<>|')
def valid_dir_name(path: str, allow_unc: bool, allow_root: bool) -> bool:
"""
The checks of the wizard's `ValidateCustomDirEdit` on a tidied folder, but for the drive, which
the emulated machine is taken to have: at most 240 characters; a UNC path only when allowed and
with a backslash after the server; else a drive letter, a colon and a backslash, and more than
the root unless allowed; no control character, no part that ends in a space or is only dots;
none of the characters `/:*?"<>|` after the first two.
"""
if len(path) > 240:
return False
if path[:2] == '\\\\':
if not allow_unc or path.find('\\', 2) <= 2:
return False
elif (
len(path) < (3 if allow_root else 4)
or not 'A' <= path[0].upper() <= 'Z'
or path[1] != ':'
or path[2] != '\\'
):
return False
if _has_control_characters(path) or _components_end_in_spaces(path) or _components_are_dots(path):
return False
return not any(c in _BAD_CHARACTERS for c in path[2:])
def valid_group_name(name: str) -> bool:
"""
The checks of the wizard's `ValidateGroupEdit` on a tidied group: at most 120 characters, no
control character, no part that ends in a space, not empty, none of the characters
`/:*?"<>|`.
"""
if len(name) > 120 or _has_control_characters(name) or _components_end_in_spaces(name):
return False
return bool(name) and not any(c in _BAD_CHARACTERS for c in name)
class _Cursor:
"""
Walks the parts of a path between backslashes as `NewExtractRelativePath` does.
"""
def __init__(self, path: str):
self.parts = path.split('\\')
self.index: int | None = 0 if path else None
def next(self) -> str | None:
if (index := self.index) is None:
return None
self.index = index + 1 if index + 1 < len(self.parts) else None
return self.parts[index]
def remaining(self) -> int:
if (index := self.index) is None:
return 0
return len(self.parts) - index
def rest(self) -> str:
if (index := self.index) is None:
return ''
return '\\'.join(self.parts[index:])
class PathFuncError(Exception):
"""
The error of Inno Setup 7's `PathExpand` for a path that it cannot expand.
"""
def expand_file_name(name: str, folder: str) -> str:
"""
Delphi's `ExpandFileName`, which Setup's `ExpandUNCFileName` uses on a machine without network
drives: the full path, or an empty text where Windows fails.
"""
full = full_path_name(name, folder)
return '' if full is None else full
PATH_FUNC_6 = PathFunc(inno7=False)
PATH_FUNC_7 = PathFunc(inno7=True)
Functions
def full_path_name(name, folder)-
Windows'
GetFullPathNamefor the given path and current folder, which is a full path;Nonewhere Windows fails, for a path that is empty or only spaces. Slashes become backslashes and repeated separators one;.and..parts go, never above the root of a drive or a share; a part before others that ends in one dot loses it, and the last part loses its trailing dots and spaces. A drive-relative path is relative to the current folder when it is on that drive, else to the root of the drive. Device names and the prefixes\\?\and\\.\are not modelled.Expand source code Browse git
def full_path_name(name: str, folder: str) -> str | None: """ Windows' `GetFullPathName` for the given path and current folder, which is a full path; `None` where Windows fails, for a path that is empty or only spaces. Slashes become backslashes and repeated separators one; `.` and `..` parts go, never above the root of a drive or a share; a part before others that ends in one dot loses it, and the last part loses its trailing dots and spaces. A drive-relative path is relative to the current folder when it is on that drive, else to the root of the drive. Device names and the prefixes `\\\\?\\` and `\\\\.\\` are not modelled. """ if not name.strip(' '): return None path = name.replace('/', '\\') if path[:3] in ('\\\\?', '\\\\.') and (len(path) == 3 or path[3] == '\\'): raise NotImplementedError(F'The emulator does not model the path {name}.') trailing = path.endswith('\\') if path[:2] == '\\\\': server_end = path.find('\\', 2) if server_end < 0: return F'\\\\{path[2:].rstrip(". ")}' share_start = server_end while share_start < len(path) and path[share_start] == '\\': share_start += 1 share_end = path.find('\\', share_start) if share_end < 0: share = path[share_start:].rstrip('. ') return F'{path[:server_end]}\\{share}' root = F'{path[:server_end]}\\{path[share_start:share_end]}' stack: list[str] = [] rest = path[share_end:] elif path[1:2] == ':': drive = path[:2] if path[2:3] == '\\': root = F'{drive}\\' stack = [] rest = path[3:] else: if folder[1:2] == ':' and folder[0].upper() == drive[0].upper(): root = _drive_root(folder) stack = _components(folder[len(root):]) else: root = F'{drive}\\' stack = [] rest = path[2:] else: root = _drive_root(folder) if path[:1] == '\\': stack = [] else: stack = _components(folder[len(root):]) rest = path for part in rest.split('\\'): if not part or part == '.': continue if part == '..': if stack: stack.pop() continue stack.append(part) middle = len(stack) if trailing else len(stack) - 1 for index in range(middle): part = stack[index] if part.endswith('.') and not part.endswith('..'): stack[index] = part[:-1] if stack and not trailing: last = stack[-1].rstrip('. ') if last.split('.')[0].split(':')[0].rstrip(' ').upper() in _DEVICES: raise NotImplementedError(F'The emulator does not model the device name in the path {name}.') if last: stack[-1] = last else: stack.pop() trailing = True if not stack: return root if root.endswith('\\') or not trailing else F'{root}\\' full = '\\'.join((root.rstrip('\\'), *stack)) return F'{full}\\' if trailing else full def valid_dir_name(path, allow_unc, allow_root)-
The checks of the wizard's
ValidateCustomDirEditon a tidied folder, but for the drive, which the emulated machine is taken to have: at most 240 characters; a UNC path only when allowed and with a backslash after the server; else a drive letter, a colon and a backslash, and more than the root unless allowed; no control character, no part that ends in a space or is only dots; none of the characters/:*?"<>|after the first two.Expand source code Browse git
def valid_dir_name(path: str, allow_unc: bool, allow_root: bool) -> bool: """ The checks of the wizard's `ValidateCustomDirEdit` on a tidied folder, but for the drive, which the emulated machine is taken to have: at most 240 characters; a UNC path only when allowed and with a backslash after the server; else a drive letter, a colon and a backslash, and more than the root unless allowed; no control character, no part that ends in a space or is only dots; none of the characters `/:*?"<>|` after the first two. """ if len(path) > 240: return False if path[:2] == '\\\\': if not allow_unc or path.find('\\', 2) <= 2: return False elif ( len(path) < (3 if allow_root else 4) or not 'A' <= path[0].upper() <= 'Z' or path[1] != ':' or path[2] != '\\' ): return False if _has_control_characters(path) or _components_end_in_spaces(path) or _components_are_dots(path): return False return not any(c in _BAD_CHARACTERS for c in path[2:]) def valid_group_name(name)-
The checks of the wizard's
ValidateGroupEditon a tidied group: at most 120 characters, no control character, no part that ends in a space, not empty, none of the characters/:*?"<>|.Expand source code Browse git
def valid_group_name(name: str) -> bool: """ The checks of the wizard's `ValidateGroupEdit` on a tidied group: at most 120 characters, no control character, no part that ends in a space, not empty, none of the characters `/:*?"<>|`. """ if len(name) > 120 or _has_control_characters(name) or _components_end_in_spaces(name): return False return bool(name) and not any(c in _BAD_CHARACTERS for c in name) def expand_file_name(name, folder)-
Delphi's
ExpandFileName, which Setup'sExpandUNCFileNameuses on a machine without network drives: the full path, or an empty text where Windows fails.Expand source code Browse git
def expand_file_name(name: str, folder: str) -> str: """ Delphi's `ExpandFileName`, which Setup's `ExpandUNCFileName` uses on a machine without network drives: the full path, or an empty text where Windows fails. """ full = full_path_name(name, folder) return '' if full is None else full
Classes
class PathFunc (inno7)-
The functions of Inno Setup's
PathFuncunit that Setup and its support functions use, in one release:inno7selects the rules of Inno Setup 7, which reads the namespace prefixes\\?\and\\.\as part of a drive, keeps a third leading slash, and fails a path that it cannot expand instead of keeping it.Expand source code Browse git
class PathFunc: """ The functions of Inno Setup's `PathFunc` unit that Setup and its support functions use, in one release: `inno7` selects the rules of Inno Setup 7, which reads the namespace prefixes `\\\\?\\` and `\\\\.\\` as part of a drive, keeps a third leading slash, and fails a path that it cannot expand instead of keeping it. """ def __init__(self, inno7: bool): self.inno7 = inno7 @staticmethod def of(release: SetupRelease) -> PathFunc: return PATH_FUNC_7 if release.since((7, 0, 0)) else PATH_FUNC_6 def __repr__(self): return F'{self.__class__.__name__}({7 if self.inno7 else 6})' @staticmethod def add_backslash(text: str) -> str: if text and text[-1] not in _SLASHES: return F'{text}\\' return text @staticmethod def remove_backslash(text: str) -> str: return text.rstrip(_SLASHES) def remove_backslash_unless_root(self, text: str) -> str: drive = self.drive_part_length(text, True) end = len(text) while end > drive and text[end - 1] in _SLASHES: end -= 1 return text[:end] def drive_part_length(self, name: str, significant_slash: bool = False) -> int: """ `PathDrivePartLengthEx`: the length of `x:` or of `\\\\server\\share` at the start of the name; with `significant_slash`, also of the slash after `x:` and of a leading slash. """ length = len(name) if length >= 2 and name[0] in _SLASHES and name[1] in _SLASHES: if self.inno7: return self._namespace_drive_part_length(name, significant_slash) index = 2 separators = 0 while index < length: if name[index] in _SLASHES: separators += 1 if separators >= 2: break index += 1 while index < length and name[index] in _SLASHES: index += 1 else: index += 1 return index if length >= 1 and name[0] in _SLASHES: return 1 if significant_slash else 0 if length >= 2 and name[1] == ':': if significant_slash and length >= 3 and name[2] in _SLASHES: return 3 return 2 return 0 @staticmethod def _namespace_drive_part_length(name: str, significant_slash: bool) -> int: length = len(name) namespace = False drive = False components = 2 current = 0 start = index = 2 while index < length: if name[index] not in _SLASHES: index += 1 continue size = index - start if current == 0: if size == 1 and name[start] in '?.': namespace = True elif current == 1 and namespace: if size == 2: if name[start + 1] == ':': drive = True elif size == 3 and name[start:start + 3].upper() == 'UNC': components += 2 current += 1 if current >= components: if drive and significant_slash: index += 1 break index += 1 while index < length and name[index] in _SLASHES: index += 1 start = index return index def path_part_length(self, name: str, slashes_after: bool) -> int: """ `PathPathPartLength`: the length of the path before the last part of the name, with the slashes after it when `slashes_after` is set, never less than the drive part. """ result = self.drive_part_length(name, True) keep = result index = result while index < len(name): if name[index] in _SLASHES: result = index + 1 if slashes_after else keep index += 1 else: index += 1 keep = index return result def extension_position(self, name: str) -> int | None: """ The index of the last dot in the last part of the name. """ position = None for index in range(self.path_part_length(name, True), len(name)): if name[index] == '.': position = index return position def change_ext(self, name: str, extension: str) -> str: if (position := self.extension_position(name)) is None: return F'{name}{extension}' return F'{name[:position]}{extension}' def extract_dir(self, name: str) -> str: return name[:self.path_part_length(name, False)] def extract_drive(self, name: str) -> str: return name[:self.drive_part_length(name)] def extract_ext(self, name: str) -> str: if (position := self.extension_position(name)) is None: return '' return name[position:] def extract_name(self, name: str) -> str: return name[self.path_part_length(name, True):] def extract_path(self, name: str) -> str: return name[:self.path_part_length(name, True)] def normalize_slashes(self, text: str) -> str: """ `PathNormalizeSlashes`: forward slashes become backslashes and repeated backslashes one, except for two at the start; Inno Setup 7 also keeps a third. """ chars = list(text.replace('/', '\\')) if self.inno7: end = 3 if chars[:2] == ['\\', '\\'] else 1 for index in range(len(chars) - 1, end - 1, -1): if chars[index] == '\\' and chars[index - 1] == '\\': del chars[index] else: index = 0 while index < len(chars) - 1: if chars[index] == '\\' and chars[index + 1] == '\\' and index > 0: del chars[index + 1] else: index += 1 return ''.join(chars) def expand(self, name: str, folder: str) -> str: """ `PathExpand`: the full path of the name in the given current folder. Before Inno Setup 7, a path that Windows cannot expand, or whose full path has 4096 characters or more, stays as it is; Inno Setup 7 gives an empty text for a path that is empty or only spaces and fails for any other path that it cannot expand. """ full = full_path_name(name, folder) if self.inno7: if not name.strip(' '): return '' if full is None or len(name) > 0x7FFE or len(full) > 0x7FFE: raise PathFuncError(F'PathExpand: GetFullPathName failed (length: {len(name)})') return full if full is None or len(full) >= 4096: return name return full def extract_relative_path(self, base: str, destination: str) -> str: """ Setup's `NewExtractRelativePath`: the path of the destination relative to the folder of the base, or the destination when the two are on different drives. """ base = self.normalize_slashes(base) destination = self.normalize_slashes(destination) base_drive = self.extract_drive(base) destination_drive = self.extract_drive(destination) if base_drive.lower() != destination_drive.lower(): return destination base_parts = _Cursor(self.extract_path(base)[len(base_drive):]) destination_parts = _Cursor(self.extract_path(destination)[len(destination_drive):]) base_part = base_parts.next() destination_part = destination_parts.next() while ( base_part is not None and destination_part is not None and base_part.lower() == destination_part.lower() ): base_part = base_parts.next() destination_part = destination_parts.next() result = '..\\' * base_parts.remaining() if destination_part: result = F'{result}{destination_part}\\' return F'{result}{destination_parts.rest()}{self.extract_name(destination)}' def tidy_up_dir_name(self, path: str) -> str: """ The wizard's `TidyUpDirName`: trims spaces, normalizes slashes and removes trailing backslashes until nothing changes. """ while True: path = self.remove_backslash_unless_root(self.normalize_slashes(trim(path))) if len(path) == len(trim(path)): return path def tidy_up_group_name(self, path: str) -> str: """ The wizard's `TidyUpGroupName`: trims spaces, removes leading, repeated and trailing backslashes until nothing changes. """ while True: path = trim(path).lstrip(_SLASHES) path = self.remove_backslash(self.normalize_slashes(path)) if len(path) == len(trim(path)): return pathStatic methods
def of(release)-
Expand source code Browse git
@staticmethod def of(release: SetupRelease) -> PathFunc: return PATH_FUNC_7 if release.since((7, 0, 0)) else PATH_FUNC_6 def add_backslash(text)-
Expand source code Browse git
@staticmethod def add_backslash(text: str) -> str: if text and text[-1] not in _SLASHES: return F'{text}\\' return text def remove_backslash(text)-
Expand source code Browse git
@staticmethod def remove_backslash(text: str) -> str: return text.rstrip(_SLASHES)
Methods
def remove_backslash_unless_root(self, text)-
Expand source code Browse git
def remove_backslash_unless_root(self, text: str) -> str: drive = self.drive_part_length(text, True) end = len(text) while end > drive and text[end - 1] in _SLASHES: end -= 1 return text[:end] def drive_part_length(self, name, significant_slash=False)-
PathDrivePartLengthEx: the length ofx:or of\\server\shareat the start of the name; withsignificant_slash, also of the slash afterx:and of a leading slash.Expand source code Browse git
def drive_part_length(self, name: str, significant_slash: bool = False) -> int: """ `PathDrivePartLengthEx`: the length of `x:` or of `\\\\server\\share` at the start of the name; with `significant_slash`, also of the slash after `x:` and of a leading slash. """ length = len(name) if length >= 2 and name[0] in _SLASHES and name[1] in _SLASHES: if self.inno7: return self._namespace_drive_part_length(name, significant_slash) index = 2 separators = 0 while index < length: if name[index] in _SLASHES: separators += 1 if separators >= 2: break index += 1 while index < length and name[index] in _SLASHES: index += 1 else: index += 1 return index if length >= 1 and name[0] in _SLASHES: return 1 if significant_slash else 0 if length >= 2 and name[1] == ':': if significant_slash and length >= 3 and name[2] in _SLASHES: return 3 return 2 return 0 def path_part_length(self, name, slashes_after)-
PathPathPartLength: the length of the path before the last part of the name, with the slashes after it whenslashes_afteris set, never less than the drive part.Expand source code Browse git
def path_part_length(self, name: str, slashes_after: bool) -> int: """ `PathPathPartLength`: the length of the path before the last part of the name, with the slashes after it when `slashes_after` is set, never less than the drive part. """ result = self.drive_part_length(name, True) keep = result index = result while index < len(name): if name[index] in _SLASHES: result = index + 1 if slashes_after else keep index += 1 else: index += 1 keep = index return result def extension_position(self, name)-
The index of the last dot in the last part of the name.
Expand source code Browse git
def extension_position(self, name: str) -> int | None: """ The index of the last dot in the last part of the name. """ position = None for index in range(self.path_part_length(name, True), len(name)): if name[index] == '.': position = index return position def change_ext(self, name, extension)-
Expand source code Browse git
def change_ext(self, name: str, extension: str) -> str: if (position := self.extension_position(name)) is None: return F'{name}{extension}' return F'{name[:position]}{extension}' def extract_dir(self, name)-
Expand source code Browse git
def extract_dir(self, name: str) -> str: return name[:self.path_part_length(name, False)] def extract_drive(self, name)-
Expand source code Browse git
def extract_drive(self, name: str) -> str: return name[:self.drive_part_length(name)] def extract_ext(self, name)-
Expand source code Browse git
def extract_ext(self, name: str) -> str: if (position := self.extension_position(name)) is None: return '' return name[position:] def extract_name(self, name)-
Expand source code Browse git
def extract_name(self, name: str) -> str: return name[self.path_part_length(name, True):] def extract_path(self, name)-
Expand source code Browse git
def extract_path(self, name: str) -> str: return name[:self.path_part_length(name, True)] def normalize_slashes(self, text)-
PathNormalizeSlashes: forward slashes become backslashes and repeated backslashes one, except for two at the start; Inno Setup 7 also keeps a third.Expand source code Browse git
def normalize_slashes(self, text: str) -> str: """ `PathNormalizeSlashes`: forward slashes become backslashes and repeated backslashes one, except for two at the start; Inno Setup 7 also keeps a third. """ chars = list(text.replace('/', '\\')) if self.inno7: end = 3 if chars[:2] == ['\\', '\\'] else 1 for index in range(len(chars) - 1, end - 1, -1): if chars[index] == '\\' and chars[index - 1] == '\\': del chars[index] else: index = 0 while index < len(chars) - 1: if chars[index] == '\\' and chars[index + 1] == '\\' and index > 0: del chars[index + 1] else: index += 1 return ''.join(chars) def expand(self, name, folder)-
PathExpand: the full path of the name in the given current folder. Before Inno Setup 7, a path that Windows cannot expand, or whose full path has 4096 characters or more, stays as it is; Inno Setup 7 gives an empty text for a path that is empty or only spaces and fails for any other path that it cannot expand.Expand source code Browse git
def expand(self, name: str, folder: str) -> str: """ `PathExpand`: the full path of the name in the given current folder. Before Inno Setup 7, a path that Windows cannot expand, or whose full path has 4096 characters or more, stays as it is; Inno Setup 7 gives an empty text for a path that is empty or only spaces and fails for any other path that it cannot expand. """ full = full_path_name(name, folder) if self.inno7: if not name.strip(' '): return '' if full is None or len(name) > 0x7FFE or len(full) > 0x7FFE: raise PathFuncError(F'PathExpand: GetFullPathName failed (length: {len(name)})') return full if full is None or len(full) >= 4096: return name return full def extract_relative_path(self, base, destination)-
Setup's
NewExtractRelativePath: the path of the destination relative to the folder of the base, or the destination when the two are on different drives.Expand source code Browse git
def extract_relative_path(self, base: str, destination: str) -> str: """ Setup's `NewExtractRelativePath`: the path of the destination relative to the folder of the base, or the destination when the two are on different drives. """ base = self.normalize_slashes(base) destination = self.normalize_slashes(destination) base_drive = self.extract_drive(base) destination_drive = self.extract_drive(destination) if base_drive.lower() != destination_drive.lower(): return destination base_parts = _Cursor(self.extract_path(base)[len(base_drive):]) destination_parts = _Cursor(self.extract_path(destination)[len(destination_drive):]) base_part = base_parts.next() destination_part = destination_parts.next() while ( base_part is not None and destination_part is not None and base_part.lower() == destination_part.lower() ): base_part = base_parts.next() destination_part = destination_parts.next() result = '..\\' * base_parts.remaining() if destination_part: result = F'{result}{destination_part}\\' return F'{result}{destination_parts.rest()}{self.extract_name(destination)}' def tidy_up_dir_name(self, path)-
The wizard's
TidyUpDirName: trims spaces, normalizes slashes and removes trailing backslashes until nothing changes.Expand source code Browse git
def tidy_up_dir_name(self, path: str) -> str: """ The wizard's `TidyUpDirName`: trims spaces, normalizes slashes and removes trailing backslashes until nothing changes. """ while True: path = self.remove_backslash_unless_root(self.normalize_slashes(trim(path))) if len(path) == len(trim(path)): return path def tidy_up_group_name(self, path)-
The wizard's
TidyUpGroupName: trims spaces, removes leading, repeated and trailing backslashes until nothing changes.Expand source code Browse git
def tidy_up_group_name(self, path: str) -> str: """ The wizard's `TidyUpGroupName`: trims spaces, removes leading, repeated and trailing backslashes until nothing changes. """ while True: path = trim(path).lstrip(_SLASHES) path = self.remove_backslash(self.normalize_slashes(path)) if len(path) == len(trim(path)): return path
class PathFuncError (*args, **kwargs)-
The error of Inno Setup 7's
PathExpandfor a path that it cannot expand.Expand source code Browse git
class PathFuncError(Exception): """ The error of Inno Setup 7's `PathExpand` for a path that it cannot expand. """Ancestors
- builtins.Exception
- builtins.BaseException