codekingpro/portable-devtools
114k
1import codecs2import re3from string import ascii_letters, ascii_lowercase, digits4from typing import overload5 6BASCII_LOWERCASE = ascii_lowercase.encode("ascii")7BPCT_ALLOWED = {f"%{i:02X}".encode("ascii") for i in range(256)}8GEN_DELIMS = ":/?#[]@"9SUB_DELIMS_WITHOUT_QS = "!$'()*,"10SUB_DELIMS = SUB_DELIMS_WITHOUT_QS + "+&=;"11RESERVED = GEN_DELIMS + SUB_DELIMS12UNRESERVED = ascii_letters + digits + "-._~"13ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS14 15 16_IS_HEX = re.compile(b"[A-Z0-9][A-Z0-9]")17_IS_HEX_STR = re.compile("[A-Fa-f0-9][A-Fa-f0-9]")18 19utf8_decoder = codecs.getincrementaldecoder("utf-8")20 21 22class _Quoter:23 def __init__(24 self,25 *,26 safe: str = "",27 protected: str = "",28 qs: bool = False,29 requote: bool = True,30 ) -> None:31 self._safe = safe32 self._protected = protected33 self._qs = qs34 self._requote = requote35 36 @overload37 def __call__(self, val: str) -> str: ...38 @overload39 def __call__(self, val: None) -> None: ...40 def __call__(self, val: str | None) -> str | None:41 if val is None:42 return None43 if not isinstance(val, str):44 raise TypeError("Argument should be str")45 if not val:46 return ""47 bval = val.encode("utf8", errors="ignore")48 ret = bytearray()49 pct = bytearray()50 safe = self._safe51 safe += ALLOWED52 if not self._qs:53 safe += "+&=;"54 safe += self._protected55 bsafe = safe.encode("ascii")56 idx = 057 while idx < len(bval):58 ch = bval[idx]59 idx += 160 61 if pct:62 if ch in BASCII_LOWERCASE:63 ch = ch - 32 # convert to uppercase64 pct.append(ch)65 if len(pct) == 3: # pragma: no branch # peephole optimizer66 buf = pct[1:]67 if not _IS_HEX.match(buf):68 ret.extend(b"%25")69 pct.clear()70 idx -= 271 continue72 try:73 unquoted = chr(int(pct[1:].decode("ascii"), base=16))74 except ValueError:75 ret.extend(b"%25")76 pct.clear()77 idx -= 278 continue79 80 if unquoted in self._protected:81 ret.extend(pct)82 elif unquoted in safe:83 ret.append(ord(unquoted))84 else:85 ret.extend(pct)86 pct.clear()87 88 # special case, if we have only one char after "%"89 elif len(pct) == 2 and idx == len(bval):90 ret.extend(b"%25")91 pct.clear()92 idx -= 193 94 continue95 96 elif ch == ord("%") and self._requote:97 pct.clear()98 pct.append(ch)99 100 # special case if "%" is last char101 if idx == len(bval):102 ret.extend(b"%25")103 104 continue105 106 if self._qs and ch == ord(" "):107 ret.append(ord("+"))108 continue109 if ch in bsafe:110 ret.append(ch)111 continue112 113 ret.extend((f"%{ch:02X}").encode("ascii"))114 115 ret2 = ret.decode("ascii")116 if ret2 == val:117 return val118 return ret2119 120 121class _Unquoter:122 def __init__(123 self,124 *,125 ignore: str = "",126 unsafe: str = "",127 qs: bool = False,128 plus: bool = False,129 ) -> None:130 self._ignore = ignore131 self._unsafe = unsafe132 self._qs = qs133 self._plus = plus # to match urllib.parse.unquote_plus134 self._quoter = _Quoter()135 self._qs_quoter = _Quoter(qs=True)136 137 @overload138 def __call__(self, val: str) -> str: ...139 @overload140 def __call__(self, val: None) -> None: ...141 def __call__(self, val: str | None) -> str | None:142 if val is None:143 return None144 if not isinstance(val, str):145 raise TypeError("Argument should be str")146 if not val:147 return ""148 decoder = utf8_decoder()149 ret = []150 idx = 0151 while idx < len(val):152 ch = val[idx]153 idx += 1154 if ch == "%" and idx <= len(val) - 2:155 pct = val[idx : idx + 2]156 if _IS_HEX_STR.fullmatch(pct):157 b = bytes([int(pct, base=16)])158 idx += 2159 try:160 unquoted = decoder.decode(b)161 except UnicodeDecodeError:162 start_pct = idx - 3 - len(decoder.buffer) * 3163 ret.append(val[start_pct : idx - 3])164 decoder.reset()165 try:166 unquoted = decoder.decode(b)167 except UnicodeDecodeError:168 ret.append(val[idx - 3 : idx])169 continue170 if not unquoted:171 continue172 if self._qs and unquoted in "+=&;":173 to_add = self._qs_quoter(unquoted)174 if to_add is None: # pragma: no cover175 raise RuntimeError("Cannot quote None")176 ret.append(to_add)177 elif unquoted in self._unsafe or unquoted in self._ignore:178 to_add = self._quoter(unquoted)179 if to_add is None: # pragma: no cover180 raise RuntimeError("Cannot quote None")181 ret.append(to_add)182 else:183 ret.append(unquoted)184 continue185 186 if decoder.buffer:187 start_pct = idx - 1 - len(decoder.buffer) * 3188 ret.append(val[start_pct : idx - 1])189 decoder.reset()190 191 if ch == "+":192 if (not self._qs and not self._plus) or ch in self._unsafe:193 ret.append("+")194 else:195 ret.append(" ")196 continue197 198 if ch in self._unsafe:199 ret.append("%")200 h = hex(ord(ch)).upper()[2:]201 for ch in h:202 ret.append(ch)203 continue204 205 ret.append(ch)206 207 if decoder.buffer:208 ret.append(val[-len(decoder.buffer) * 3 :])209 210 ret2 = "".join(ret)211 if ret2 == val:212 return val213 return ret2214 