import t, c from stdint import * import string import memhub import platmacro # Module-level global MBuddy pointer (initialized by the user in main) _mbuddy: memhub.MemBuddy* # ============================================================================= # Path Separators # ============================================================================= def sep() -> char: """Return the platform path separator (Windows: \\, POSIX: /)""" if platmacro.IS_WINDOWS: return '\\' return '/' def _is_sep(ch: char) -> bool: """Return True if ch is a path separator (/ or \\, cross-platform)""" return ch == '/' or ch == '\\' # ============================================================================= # Internal Helpers # ============================================================================= def _dup_str(s: str) -> str: """Duplicate a string; memory allocated via the global MBuddy""" if s == None: return None length: sizet = len(s) buf: bytes = _mbuddy.alloc(length + 1) if buf == None: return None i: sizet = 0 for ch in s: buf[i] = ch i += 1 buf[length] = 0 return str(buf) def _substr(s: str, start: sizet, end: sizet) -> str: """Extract substring s[start:end]; memory allocated via the global MBuddy""" if s == None: return None if start >= end: return _dup_str("") result_len: sizet = end - start buf: bytes = _mbuddy.alloc(result_len + 1) if buf == None: return None i: sizet = 0 k: sizet = start while k < end: buf[i] = s[k] i += 1 k += 1 buf[i] = 0 return str(buf) # ============================================================================= # Path Predicates # ============================================================================= def isabs(path: str) -> bool: """Return True if path is absolute. POSIX: starts with /. Windows: starts with \\ (UNC) or X:\\ / X:/ (drive).""" if path == None: return False if path[0] == 0: return False if path[0] == '/' or path[0] == '\\': return True if platmacro.IS_WINDOWS: # Check for X:\ or X:/ pattern c0: char = path[0] if (c0 >= 'A' and c0 <= 'Z') or (c0 >= 'a' and c0 <= 'z'): if path[1] == ':': if path[2] == '\\' or path[2] == '/': return True return False # ============================================================================= # Path Join and Split # ============================================================================= def join(path1: str, path2: str) -> str: """Join two paths. If path2 is absolute, return a copy of path2.""" if path1 == None or path1[0] == 0: return _dup_str(path2) if path2 == None or path2[0] == 0: return _dup_str(path1) if isabs(path2): return _dup_str(path2) len1: sizet = len(path1) len2: sizet = len(path2) # Check whether path1 ends with a separator need_sep: bool = True if _is_sep(path1[len1 - 1]): need_sep = False total: sizet = len1 + len2 + 1 if need_sep: total += 1 buf: bytes = _mbuddy.alloc(total) if buf == None: return None # Copy path1 i: sizet = 0 for ch in path1: buf[i] = ch i += 1 # Add separator if need_sep: buf[i] = sep() i += 1 # Copy path2 for ch in path2: buf[i] = ch i += 1 buf[i] = 0 return str(buf) def basename(path: str) -> str: """Return the filename part of path (content after the last separator)""" if path == None: return None length: sizet = len(path) if length == 0: return _dup_str("") # Scan backward for a separator i: sizet = length while i > 0: i -= 1 if _is_sep(path[i]): return _substr(path, i + 1, length) # No separator; return a copy of the whole path return _dup_str(path) def dirname(path: str) -> str: """Return the directory part of path (content before the last separator)""" if path == None: return None length: sizet = len(path) if length == 0: return _dup_str("") # Scan backward for a separator i: sizet = length while i > 0: i -= 1 if _is_sep(path[i]): if i == 0: # Path starts with a separator; return the root directory return _dup_str("/") return _substr(path, 0, i) # No separator; return an empty string return _dup_str("") # ============================================================================= # Extension Handling # ============================================================================= def splitext_name(path: str) -> str: """Return the stem of path with the extension removed. The extension starts at the last '.' (not the first char of the last component).""" if path == None: return None length: sizet = len(path) if length == 0: return _dup_str("") # Scan backward for '.'; stop at a separator i: sizet = length while i > 0: i -= 1 if path[i] == '.': return _substr(path, 0, i) if _is_sep(path[i]): break # No extension; return a copy of the whole path return _dup_str(path) def splitext_ext(path: str) -> str: """Return the extension part of path (including '.'). Return an empty string if none.""" if path == None: return None length: sizet = len(path) if length == 0: return _dup_str("") # Scan backward for '.'; stop at a separator i: sizet = length while i > 0: i -= 1 if path[i] == '.': return _substr(path, i, length) if _is_sep(path[i]): break # No extension; return an empty string return _dup_str("") # ============================================================================= # Path Normalization # ============================================================================= def normpath(path: str) -> str: """Normalize path: unify separators, remove redundant . and .. components, merge consecutive separators. Note: does not resolve symlinks or call system APIs.""" if path == None: return None length: sizet = len(path) if length == 0: return _dup_str(".") # Allocate output buffer (worst case: same length + leading /) buf: bytes = _mbuddy.alloc(length + 2) if buf == None: return None out_pos: sizet = 0 i: sizet = 0 s: char = sep() # Handle the absolute path prefix is_absolute: bool = False if _is_sep(path[0]): is_absolute = True buf[out_pos] = s out_pos += 1 i = 1 # Windows drive prefix (X:\ or X:/) drive_prefix_len: sizet = 0 if platmacro.IS_WINDOWS: if length >= 3: c0: char = path[0] if (c0 >= 'A' and c0 <= 'Z') or (c0 >= 'a' and c0 <= 'z'): if path[1] == ':': if _is_sep(path[2]): buf[out_pos] = path[0] out_pos += 1 buf[out_pos] = ':' out_pos += 1 buf[out_pos] = s out_pos += 1 drive_prefix_len = 3 i = 3 is_absolute = True # Process component by component while i < length: # Skip consecutive separators while i < length and _is_sep(path[i]): i += 1 if i >= length: break # Extract component comp_start: sizet = i while i < length and not _is_sep(path[i]): i += 1 comp_len: sizet = i - comp_start # Handle "." component: skip if comp_len == 1 and path[comp_start] == '.': continue # Handle ".." component: go up one level if comp_len == 2 and path[comp_start] == '.' and path[comp_start + 1] == '.': if out_pos > drive_prefix_len: # Rewind to just before the previous separator out_pos -= 1 while out_pos > drive_prefix_len and not _is_sep(buf[out_pos - 1]): out_pos -= 1 # If rewound to the root prefix, keep the prefix if out_pos == drive_prefix_len and is_absolute: out_pos = drive_prefix_len if drive_prefix_len == 0: # POSIX root: keep / buf[out_pos] = s out_pos += 1 continue # Normal component: copy to output if out_pos > drive_prefix_len and not (drive_prefix_len == 0 and out_pos == 1 and is_absolute): buf[out_pos] = s out_pos += 1 elif out_pos == 0 and not is_absolute: pass # First component, no leading separator needed elif is_absolute and out_pos == drive_prefix_len: # Leading separator already present, do not add again pass else: buf[out_pos] = s out_pos += 1 j: sizet = comp_start while j < comp_start + comp_len: buf[out_pos] = path[j] out_pos += 1 j += 1 # Handle empty result if out_pos == 0: buf[out_pos] = '.' out_pos += 1 buf[out_pos] = 0 return str(buf)