125125except Exception:
126126 PATH_TYPES = (basestring,)
127127
128+ def _ensure_text(s, encoding="utf-8", errors="replace", allow_none=False):
129+ """
130+ Normalize any input to text_type (unicode on Py2, str on Py3).
131+
132+ - bytes/bytearray/memoryview -> decode
133+ - os.PathLike -> fspath then normalize
134+ - None -> "" (unless allow_none=True, then return None)
135+ - everything else -> text_type(s)
136+ """
137+ if s is None:
138+ return None if allow_none else text_type("")
139+
140+ if isinstance(s, text_type):
141+ return s
142+
143+ if isinstance(s, (bytes_type, bytearray, memoryview)):
144+ return bytes(s).decode(encoding, errors)
145+
146+ # Handle pathlib.Path & other path-like objects
147+ try:
148+ import os
149+ if hasattr(os, "fspath"):
150+ fs = os.fspath(s)
151+ if isinstance(fs, text_type):
152+ return fs
153+ if isinstance(fs, (bytes_type, bytearray, memoryview)):
154+ return bytes(fs).decode(encoding, errors)
155+ except Exception:
156+ pass
157+
158+ return text_type(s)
159+
128160def to_text(s, encoding="utf-8", errors="ignore"):
129161 if s is None:
130162 return u""
@@ -898,36 +930,65 @@ def VerbosePrintOutReturn(dbgtxt, outtype="log", dbgenable=True, dgblevel=20, **
898930 return dbgtxt
899931
900932
933+ def _split_posix(name):
934+ """
935+ Return a list of path parts without collapsing '..'.
936+ - Normalize backslashes to '/'
937+ - Strip leading './' (repeated)
938+ - Remove '' and '.' parts; keep '..' for traversal detection
939+ """
940+ if not name:
941+ return []
942+ n = name.replace(u"\\", u"/")
943+ while n.startswith(u"./"):
944+ n = n[2:]
945+ return [p for p in n.split(u"/") if p not in (u"", u".")]
901946
902- def _split_posix(path_text):
903- """Split POSIX paths regardless of OS; return list of components."""
904- # Normalize leading './'
905- if path_text.startswith(u'./'):
906- path_text = path_text[2:]
907- # Strip redundant slashes
908- path_text = re.sub(u'/+', u'/', path_text)
909- # Drop trailing '/' so 'dir/' -> ['dir']
910- if path_text.endswith(u'/'):
911- path_text = path_text[:-1]
912- return path_text.split(u'/') if path_text else []
947+ def _is_abs_like(name):
948+ """Detect absolute-like paths across platforms (/, \\, drive letters, UNC)."""
949+ if not name:
950+ return False
951+ n = name.replace(u"\\", u"/")
913952
914- def _is_abs_like(s):
915- """Absolute targets (POSIX or Windows-drive style)."""
916- return s.startswith(u'/') or s.startswith(u'\\') or re.match(u'^[A-Za-z]:[/\\\\]', s)
953+ # POSIX absolute
954+ if n.startswith(u"/"):
955+ return True
956+
957+ # Windows UNC (\\server\share\...) -> after replace: startswith '//'
958+ if n.startswith(u"//"):
959+ return True
917960
918- def _resolves_outside(base_rel, target_rel):
961+ # Windows drive: 'C:/', 'C:\', or bare 'C:' (treat as absolute-like conservatively)
962+ if len(n) >= 2 and n[1] == u":":
963+ if len(n) == 2:
964+ return True
965+ if n[2:3] in (u"/", u"\\"):
966+ return True
967+ return False
968+
969+ def _resolves_outside(parent, target):
919970 """
920- Given a base directory (relative, POSIX) and a target (relative),
921- return True if base/target resolves outside of base.
922- We anchor under '/' so normpath is root-anchored and portable.
971+ Does a symlink from 'parent' to 'target' escape parent?
972+ - Absolute-like target => escape.
973+ - Compare normalized '/<parent>/<target>' against '/<parent>'.
974+ - 'parent' is POSIX-style ('' means archive root).
923975 """
924- base_clean = u'/'.join(_split_posix(base_rel))
925- target_clean = u'/'.join(_split_posix(target_rel))
926- base_abs = u'/' + base_clean if base_clean else u'/'
927- combined = pp.normpath(pp.join(base_abs, target_clean))
928- if combined == base_abs or combined.startswith(base_abs + u'/'):
929- return False
930- return True
976+ parent = _ensure_text(parent or u"")
977+ target = _ensure_text(target or u"")
978+
979+ # Absolute target is unsafe by definition
980+ if _is_abs_like(target):
981+ return True
982+
983+ import posixpath as pp
984+ root = u"/"
985+ base = pp.normpath(pp.join(root, parent)) # '/dir/sub' or '/'
986+ cand = pp.normpath(pp.join(base, target)) # resolved target under '/'
987+
988+ # ensure trailing slash on base for the prefix test
989+ base_slash = base if base.endswith(u"/") else (base + u"/")
990+ return not (cand == base or cand.startswith(base_slash))
991+
931992
932993def _to_bytes(data, encoding="utf-8", errors="strict"):
933994 """
@@ -1031,9 +1092,6 @@ def _to_text(s, encoding="utf-8", errors="replace", normalize=None, prefer_surro
10311092
10321093 return out
10331094
1034- def ensure_text(s, **kw):
1035- return _to_text(s, **kw)
1036-
10371095def _quote_path_for_wire(path_text):
10381096 # Percent-encode as UTF-8; return ASCII bytes text
10391097 try:
@@ -1399,7 +1457,7 @@ def _guess_filename(url, filename):
13991457 return filename
14001458 path = urlparse(url).path or ''
14011459 base = os.path.basename(path)
1402- return base or 'OutFile .'+__file_format_extension__
1460+ return base or 'FoxFile .'+__file_format_extension__
14031461
14041462# ---- progress + rate limiting helpers ----
14051463try:
@@ -1695,62 +1753,6 @@ def _pace_rate(last_ts, sent_bytes_since_ts, rate_limit_bps, add_bytes):
16951753 return (sleep_s, last_ts, sent_bytes_since_ts)
16961754
16971755
1698- def _split_posix(name):
1699- """
1700- Return a list of path parts without collapsing '..'.
1701- - Normalize backslashes to '/'
1702- - Strip leading './' and redundant slashes
1703- - Keep '..' parts for traversal detection
1704- """
1705- if not name:
1706- return []
1707- n = name.replace(u"\\", u"/")
1708- # drop leading ./ repeatedly
1709- while n.startswith(u"./"):
1710- n = n[2:]
1711- # split and filter empty and '.'
1712- parts = [p for p in n.split(u"/") if p not in (u"", u".")]
1713- return parts
1714-
1715- def _is_abs_like(name):
1716- """Detect absolute-like paths across platforms (/, \, drive letters)."""
1717- if not name:
1718- return False
1719- n = name.replace(u"\\", u"/")
1720- if n.startswith(u"/"):
1721- return True
1722- # Windows drive: C:/ or C:\ (allow lowercase too)
1723- if len(n) >= 3 and n[1] == u":" and n[2] in (u"/", u"\\"):
1724- return True
1725- return False
1726-
1727- def _resolves_outside(parent, target):
1728- """
1729- Does a symlink from 'parent' to 'target' escape parent?
1730- - Treat absolute target as escaping.
1731- - For relative target, join parent + target, normpath, then check if it starts with parent.
1732- - Parent is POSIX-style path ('' means root of archive).
1733- """
1734- parent = _ensure_text(parent or u"")
1735- target = _ensure_text(target or u"")
1736- # absolute target is unsafe by definition
1737- if _is_abs_like(target):
1738- return True
1739-
1740- # Build a virtual root '/' so we can compare safely
1741- # e.g., parent='dir/sub', target='../../etc' -> '/dir/sub/../../etc' -> '/etc' (escapes)
1742- import posixpath
1743- root = u"/"
1744- base = posixpath.normpath(posixpath.join(root, parent)) # '/dir/sub'
1745- candidate = posixpath.normpath(posixpath.join(base, target)) # resolved path under '/'
1746-
1747- # Ensure base always ends with a slash for prefix test
1748- base_slash = base if base.endswith(u"/") else (base + u"/")
1749- # candidate must be base itself or inside base
1750- if candidate == base or candidate.startswith(base_slash):
1751- return False
1752- return True
1753-
17541756def _symlink_type(ftype):
17551757 """
17561758 Return True if ftype denotes a symlink.
@@ -1764,7 +1766,7 @@ def _symlink_type(ftype):
17641766 return True
17651767 except Exception:
17661768 pass
1767- s = ensure_text (ftype).strip().lower()
1769+ s = _ensure_text (ftype).strip().lower()
17681770 return s in (u"2", u"symlink", u"link", u"symbolic_link", u"symbolic-link")
17691771
17701772def DetectTarBombFoxFileArray(listarrayfiles,
@@ -1782,7 +1784,7 @@ def DetectTarBombFoxFileArray(listarrayfiles,
17821784 has_symlinks (bool)
17831785 """
17841786 if to_text is None:
1785- to_text = ensure_text
1787+ to_text = _ensure_text
17861788
17871789 files = listarrayfiles or {}
17881790 members = files.get('ffilelist') or []
0 commit comments