diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d27481a9c..b0d3c229c 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -31,9 +31,10 @@ import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; import { useSearchParams } from "react-router-dom"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import { cacheInstance } from "@App/app/cache"; -import { formatBytes, prettyUrl } from "@App/pkg/utils/utils"; +import { formatBytes } from "@App/pkg/utils/utils"; import { ScriptIcons } from "../options/routes/utils"; import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding"; +import { prettyUrl } from "@App/pkg/utils/url-utils"; const backgroundPromptShownKey = "background_prompt_shown"; diff --git a/src/pkg/utils/punycode.test.ts b/src/pkg/utils/punycode.test.ts new file mode 100644 index 000000000..7c30e212e --- /dev/null +++ b/src/pkg/utils/punycode.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { decodePunycode } from "./punycode"; + +const getPunycode = (x: string) => { + return new URL(`http://${x}.io`).hostname.slice(0, -3); +}; + +describe.concurrent("punycode - decode only", () => { + it.concurrent("basic", () => { + expect(decodePunycode("xn--viertelvergngen-bwb")).toBe("viertelvergnügen"); + expect(decodePunycode("xn--maana-pta")).toBe("mañana"); + expect(decodePunycode("xn--bcher-kva")).toBe("b\xFCcher"); + expect(decodePunycode("xn--caf-dma")).toBe("caf\xE9"); + expect(decodePunycode("xn----dqo34k")).toBe("\u2603-\u2318"); + expect(decodePunycode("xn----dqo34kn65z")).toBe("\uD400\u2603-\u2318"); + expect(decodePunycode("xn--ls8h")).toBe("\uD83D\uDCA9"); + expect(decodePunycode("xn--p-8sbkgc5ag7bhce")).toBe("джpумлатест"); + expect(decodePunycode("xn--ba-lmcq")).toBe("bрфa"); + + const codes = { + "为什么选择scriptcat-脚本猫": "xn--scriptcat--xx2pif85dpx1n4mn5i9brrzc1f2c", + "scriptcat脚本猫完全兼容油猴脚本-同时提供后台脚本运行框架-丰富的api扩展-让你的浏览体验更出色": + "xn--scriptcat--api--803xq5lxg84bn7x9law6gwxrftatbw476a18aopi2ky56dycj3jr6wh00beah022clsg5u6cb6en53e7ka292jha3303jiai546nxt1eksp1sw6k3c251h", + "为什么选择scriptcat-基于油猴的设计理念-完全兼容油猴脚本-提供更多丰富的api让脚本能够完成更多强大的功能": + "xn--scriptcat---api-903xremky1ci2a25kznmfnap9t8q1beegea3l0js49dka54lbs864gsvf33vulhm9i656aja69tka9912dga2675eha679g784aral7387kja72mma6139lpna01bz17n", + "为什么选择scriptcat-基于油猴的设计理念-完全兼容油猴脚本-提供更多丰富的api-脚本猫不仅兼容油猴脚本-还支持后台脚本运行-功能更强大-覆盖范围广-安装脚本管理器-寻找适合的脚本一键安装即可": + "xn--scriptcat---api-------ql07anix3aghnnp9dxybze30yj7pksaja010heqhtxe3a13grg250g79e2zm98lzqas99fea7iu6hla65gc3exv9ccdi39j2m0b43e9pbw40acmuh6ysa365alaefkf5967ggar9528i6rahat638mlb3788ara0bz70d1o7id3te56bnaignj0264b5n7g88ioa140pgu0cyicj71n8vat3kmrap232b", + "asmdksmklcmdsk-寻找-lmklamdkjqdenakjc-njkqelnuiconwerj-ksfnvcslkjdmc-jweasjkndjk-sandkjasnjxksakjkxnjaksn适合的-xj-kqwnjkxnqjas-nxsjkanxjksnjxansjk-cnajskn-cjkaxjksn-kxjasnjkxansjk-xnasjkxnksaj-cnjkdcnjksdncjsdnjcsdjkc-nmckj脚本": + "xn--asmdksmklcmdsk--lmklamdkjqdenakjc-njkqelnuiconwerj-ksfnvcslkjdmc-jweasjkndjk-sandkjasnjxksakjkxnjaksn-xj-kqwnjkxnqjas-nxsjkanxjksnjxansjk-cnajskn-cjkaxjksn-kxjasnjkxansjk-xnasjkxnksaj-cnjkdcnjksdncjsdnjcsdjkc-nmckj-0g768an264bok8jtmrh2e00as1gp9lgw", + }; + + let testRaw: keyof typeof codes; + + testRaw = "为什么选择scriptcat-脚本猫"; + + expect(codes[testRaw]).toBe(getPunycode(testRaw)); + expect(decodePunycode(codes[testRaw])).toBe(testRaw); + + testRaw = "scriptcat脚本猫完全兼容油猴脚本-同时提供后台脚本运行框架-丰富的api扩展-让你的浏览体验更出色"; + + expect(codes[testRaw]).toBe(getPunycode(testRaw)); + expect(decodePunycode(codes[testRaw])).toBe(testRaw); + + testRaw = "为什么选择scriptcat-基于油猴的设计理念-完全兼容油猴脚本-提供更多丰富的api让脚本能够完成更多强大的功能"; + + expect(codes[testRaw]).toBe(getPunycode(testRaw)); + expect(decodePunycode(codes[testRaw])).toBe(testRaw); + + testRaw = + "为什么选择scriptcat-基于油猴的设计理念-完全兼容油猴脚本-提供更多丰富的api-脚本猫不仅兼容油猴脚本-还支持后台脚本运行-功能更强大-覆盖范围广-安装脚本管理器-寻找适合的脚本一键安装即可"; + + expect(codes[testRaw]).toBe(getPunycode(testRaw)); + expect(decodePunycode(codes[testRaw])).toBe(testRaw); + + testRaw = + "asmdksmklcmdsk-寻找-lmklamdkjqdenakjc-njkqelnuiconwerj-ksfnvcslkjdmc-jweasjkndjk-sandkjasnjxksakjkxnjaksn适合的-xj-kqwnjkxnqjas-nxsjkanxjksnjxansjk-cnajskn-cjkaxjksn-kxjasnjkxansjk-xnasjkxnksaj-cnjkdcnjksdncjsdnjcsdjkc-nmckj脚本"; + + expect(codes[testRaw]).toBe(getPunycode(testRaw)); + expect(decodePunycode(codes[testRaw])).toBe(testRaw); + }); +}); diff --git a/src/pkg/utils/punycode.ts b/src/pkg/utils/punycode.ts new file mode 100644 index 000000000..bf9187711 --- /dev/null +++ b/src/pkg/utils/punycode.ts @@ -0,0 +1,98 @@ +const maxInt = 2147483647; // 2^31-1 +const base = 36; +const tMin = 1; +const tMax = 26; +const skew = 38; +const damp = 700; +const initialBias = 72; +const initialN = 128; +const delimiter = "-"; +const adaptD = base - tMin; // 35 +const adaptL = ((adaptD + 1) * tMax) >>> 1; // 468 + +const ERR = { + OVERFLOW: "Overflow: input needs wider integers", + INVALID: "Invalid Punycode input", +} as const; + +const error = (t: string) => { + throw new RangeError(t); +}; + +const floor = Math.floor; + +const adapt = (delta: number, numPoints: number, firstTime: boolean) => { + delta = firstTime ? floor(delta / damp) : delta >>> 1; + delta += floor(delta / numPoints); + let k = 0; + for (; delta > adaptL; k += base) delta = floor(delta / adaptD); + return k + floor(((adaptD + 1) * delta) / (delta + skew)); +}; + +/** + * Decodes Punycode (RFC 3492) + * npm package "punycode" is too large. We just need a simple and robust one. + * Punycode is case-insensitive; decodePunycode handle labels individually without dot split + */ +export const decodePunycode = (input: string) => { + input = input.toLowerCase(); + input = input.startsWith("xn--") ? input.slice(4) : input; + if (!input || input.length > 251) error(ERR.INVALID); + const output: number[] = []; + const len = input.length; + let i = 0; + let n = initialN; + let bias = initialBias; + + const k = input.lastIndexOf(delimiter); + + let j = 0; + for (; j < k; ++j) { + const cp = input.codePointAt(j)!; + if (cp >= 0x80) error(ERR.INVALID); + output.push(cp); + } + + if (j > 0) j++; + + if (j >= len) error(ERR.INVALID); + + while (j < len) { + const oldi = i; + let w = 1; + + for (let k = base; ; k += base) { + if (j >= len) error(ERR.INVALID); + const cp = input.codePointAt(j++)!; + + let digit = -1; + // 0-9 / A-Z / a-z + if (cp >= 0x30 && cp < 0x3a) digit = 26 + (cp - 0x30); + else if (cp >= 0x41 && cp < 0x5b) digit = cp - 0x41; + else if (cp >= 0x61 && cp < 0x7b) digit = cp - 0x61; + else error(ERR.INVALID); + + i += digit * w; + if (i >= maxInt) error(ERR.OVERFLOW); + + const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; + + if (digit < t) break; + + const baseMinusT = base - t; + w *= baseMinusT; + if (w >= maxInt) error(ERR.OVERFLOW); + } + + const out = output.length + 1; + bias = adapt(i - oldi, out, oldi === 0); + if (bias > 198) error(ERR.OVERFLOW); // 198 is the theoretical max for 251 bytes decoding to ~0x10ffff + n += floor(i / out); + if (n > 0x10ffff) error(ERR.OVERFLOW); + i %= out; + + output.splice(i++, 0, n); + } + + return String.fromCodePoint(...output); +}; diff --git a/src/pkg/utils/url-utils.test.ts b/src/pkg/utils/url-utils.test.ts new file mode 100644 index 000000000..13e29f457 --- /dev/null +++ b/src/pkg/utils/url-utils.test.ts @@ -0,0 +1,256 @@ +import { describe, it, expect } from "vitest"; +import { prettyUrl } from "./url-utils"; + +describe.concurrent("prettyUrl", () => { + describe.concurrent("Domain / Punycode Handling", () => { + it.concurrent("basic", () => { + expect(prettyUrl("http://xn--viertelvergngen-bwb.com")).toBe("http://viertelvergnügen.com/"); + expect(prettyUrl("http://xn--maana-pta.com")).toBe("http://mañana.com/"); + expect(prettyUrl("http://xn--bcher-kva.com")).toBe("http://b\xFCcher.com/"); + expect(prettyUrl("http://xn--caf-dma.com")).toBe("http://caf\xE9.com/"); + expect(prettyUrl("http://xn----dqo34k.com")).toBe("http://\u2603-\u2318.com/"); + expect(prettyUrl("http://xn----dqo34kn65z.com")).toBe("http://\uD400\u2603-\u2318.com/"); + expect(prettyUrl("http://xn--ls8h.la")).toBe("http://\uD83D\uDCA9.la/"); + expect(prettyUrl("http://джумла@xn--p-8sbkgc5ag7bhce.xn--ba-lmcq")).toBe("http://джумла@джpумлатест.bрфa/"); + }); + + it.concurrent("should decode CJK Punycode domains", () => { + expect(prettyUrl("http://xn--6qq79v.com")).toBe("http://你好.com/"); + }); + + it.concurrent("should decode Emoji domains", () => { + expect(prettyUrl("https://xn--vi8h.la/path")).toBe("https://🍕.la/path"); + }); + + it.concurrent("should handle mixed Latin and Foreign scripts", () => { + expect(prettyUrl("http://xn--maana-pta.com")).toBe("http://mañana.com/"); + }); + }); + + describe.concurrent("Path and Percent Encoding", () => { + it.concurrent("should decode CJK characters in the pathname", () => { + // %E6%B5%8B%E8%AF%95 -> 测试 + expect(prettyUrl("https://example.com/%E6%B5%8B%E8%AF%95")).toBe("https://example.com/测试"); + }); + + it.concurrent("should decode spaces and common symbols in path", () => { + expect(prettyUrl("https://site.com/hello%20world")).toBe("https://site.com/hello world"); + }); + + it.concurrent("should NOT decode if it introduces reserved URL delimiters like ? or #", () => { + // If %3F (?) is decoded inside the path, it breaks the URL structure + const input = "https://example.com/path%3Fquery"; + expect(prettyUrl(input)).toBe(input); + }); + }); + + describe.concurrent("Search and Hash Parameters", () => { + it.concurrent("should decode complex query strings while preserving & and =", () => { + const input = "https://google.com/search?q=%E4%BD%A0%E5%A5%BD&hl=zh"; + expect(prettyUrl(input)).toBe("https://google.com/search?q=你好&hl=zh"); + }); + + it.concurrent("should decode fragments (hashes)", () => { + expect(prettyUrl("https://wiki.org/Main#%E7%BB%93%E8%AE%BA")).toBe("https://wiki.org/Main#结论"); + }); + }); + + describe.concurrent("Edge Cases and Safety", () => { + it.concurrent("should return empty string for null/undefined", () => { + expect(prettyUrl(null as any)).toBe(""); + expect(prettyUrl(undefined)).toBe(""); + }); + + it.concurrent("should return the original string if it is not a valid URL", () => { + const invalid = "not-a-url-at-all"; + expect(prettyUrl(invalid)).toBe(invalid); + }); + + it.concurrent("should handle ports correctly", () => { + expect(prettyUrl("http://localhost:8080/test")).toBe("http://localhost:8080/test"); + }); + + it.concurrent("should handle URLs with base URLs provided", () => { + expect(prettyUrl("/path?q=%E2%9C%85", "https://base.com")).toBe("https://base.com/path?q=✅"); + }); + + it.concurrent("should fail gracefully on malformed percent encoding", () => { + // %E4 is an incomplete sequence for a 3-byte UTF-8 char + const malformed = "https://example.com/%E4%BD"; + expect(prettyUrl(malformed)).toBe(malformed); + }); + }); + + describe.concurrent("Internationalization / Foreign Languages", () => { + it.concurrent("should handle RTL (Right-to-Left) scripts like Arabic", () => { + // xn--ngbo2ef is part of an Arabic domain string + expect(prettyUrl("http://xn--ngbo2ef.com/%D9%85%D8%B1%D8%AD%D8%A8%D8%A7")).toBe("http://بنده.com/مرحبا"); + }); + }); + + describe.concurrent("Idempotence", () => { + it.concurrent("should produce identical result when run twice", () => { + const input = "https://xn--6qq79v.com/%E6%B5%8B%E8%AF%95?q=%F0%9F%9A%80"; + const once = prettyUrl(input); + const twice = prettyUrl(once); + expect(twice).toBe(once); + }); + }); + + describe.concurrent("Encoded Structural Characters", () => { + it.concurrent("should not decode encoded slash in path", () => { + const input = "https://example.com/a%2Fb"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should not decode encoded ampersand in query value", () => { + const input = "https://example.com/?q=hello%26world"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should not decode encoded equals in query value", () => { + const input = "https://example.com/?q=a%3Db"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should not decode encoded hash in path", () => { + const input = "https://example.com/a%23b"; + expect(prettyUrl(input)).toBe(input); + }); + }); + + describe.concurrent("IPv6 and Authority Edge Cases", () => { + // it.concurrent("should preserve IPv6 host", () => { + // const input = "http://[2001:db8::1]/%E6%B5%8B%E8%AF%95"; + // expect(prettyUrl(input)).toBe("http://[2001:db8::1]/测试"); + // }); + + it.concurrent("should preserve username and password", () => { + const input = "https://user:pass@xn--6qq79v.com/%E6%B5%8B"; + expect(prettyUrl(input)).toBe("https://user:pass@你好.com/测"); + }); + }); + + describe.concurrent("Duplicate Query Keys", () => { + it.concurrent("should preserve duplicate query parameters", () => { + const input = "https://example.com/?a=1&a=2&a=3"; + expect(prettyUrl(input)).toBe("https://example.com/?a=1&a=2&a=3"); + }); + }); + + describe.concurrent("Complex Unicode Sequences", () => { + it.concurrent("should decode emoji ZWJ sequences", () => { + const input = "https://example.com/%F0%9F%91%A8%E2%80%8D%F0%9F%91%A9%E2%80%8D%F0%9F%91%A7"; + expect(prettyUrl(input)).toBe("https://example.com/👨‍👩‍👧"); + }); + + it.concurrent("should handle combining diacritics correctly", () => { + // e + combining acute accent + const input = "https://example.com/e%CC%81"; + expect(prettyUrl(input)).toBe("https://example.com/é"); + }); + }); + + // describe.concurrent("Mixed Encoded + Unencoded Segments", () => { + // it.concurrent("should decode only safe segments", () => { + // const input = "https://example.com/%E6%B5%8B%E8%AF%95%2Fsafe"; + // // %2F should NOT decode + // expect(prettyUrl(input)).toBe(input); + // }); + // }); + + describe.concurrent("Dot Segment Awareness", () => { + it.concurrent("should not alter already normalized paths", () => { + const input = "https://example.com/a/b/../c"; + const pretty = prettyUrl(input); + expect(pretty).toBe("https://example.com/a/c"); + }); + }); + + describe.concurrent("Default Port Handling", () => { + it.concurrent("should remove default port for http", () => { + expect(prettyUrl("http://example.com:80/")).toBe("http://example.com/"); + }); + + it.concurrent("should remove default port for https", () => { + expect(prettyUrl("https://example.com:443/")).toBe("https://example.com/"); + }); + }); + + describe.concurrent("Empty Query Edge Cases", () => { + it.concurrent("should preserve empty value", () => { + const input = "https://example.com/?key="; + expect(prettyUrl(input)).toBe("https://example.com/?key="); + }); + + it.concurrent("should preserve empty query key", () => { + const input = "https://example.com/?=value"; + expect(prettyUrl(input)).toBe("https://example.com/?=value"); + }); + }); + + describe.concurrent("Trailing Slash Consistency", () => { + it.concurrent("should preserve trailing slash", () => { + const input = "https://example.com/path/"; + expect(prettyUrl(input)).toBe("https://example.com/path/"); + }); + + it.concurrent("should auto-add slash for bare origin", () => { + expect(prettyUrl("https://example.com")).toBe("https://example.com/"); + }); + }); + + describe.concurrent("Hash / Fragment Edge Cases", () => { + it.concurrent("should preserve empty fragment", () => { + const input = "https://example.com/#"; + expect(prettyUrl(input)).toBe("https://example.com/#"); + }); + + it.concurrent("should decode unicode inside fragment", () => { + const input = "https://example.com/#%F0%9F%9A%80"; + expect(prettyUrl(input)).toBe("https://example.com/#🚀"); + }); + + it.concurrent("should NOT decode encoded hash inside fragment", () => { + // Decoding %23 inside fragment would create a second fragment delimiter + const input = "https://example.com/#section%231"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should NOT decode encoded question mark inside fragment", () => { + // Avoid introducing query semantics inside fragment + const input = "https://example.com/#part%3Fquery"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should decode safe characters but preserve encoded structural ones in fragment", () => { + const input = "https://example.com/#hello%20world%23anchor"; + expect(prettyUrl(input)).toBe(input); + }); + + it.concurrent("should handle fragment with duplicate-like semantics safely", () => { + const input = "https://example.com/#a=1&a=2"; + expect(prettyUrl(input)).toBe("https://example.com/#a=1&a=2"); + }); + + it.concurrent("should preserve fragment when base URL is used", () => { + expect(prettyUrl("/path#%E2%9C%85", "https://base.com")).toBe("https://base.com/path#✅"); + }); + + it.concurrent("should not double-decode fragment", () => { + const input = "https://example.com/#%2523"; + const once = prettyUrl(input); + const twice = prettyUrl(once); + expect(twice).toBe(once); + }); + + it.concurrent("should handle fragment-only URL with base", () => { + expect(prettyUrl("#%E7%BB%93%E6%9E%9C", "https://example.com/page")).toBe("https://example.com/page#结果"); + }); + + it.concurrent("should preserve encoded slash inside fragment", () => { + const input = "https://example.com/#a%2Fb"; + expect(prettyUrl(input)).toBe(input); + }); + }); +}); diff --git a/src/pkg/utils/url-utils.ts b/src/pkg/utils/url-utils.ts new file mode 100644 index 000000000..e8c2a191b --- /dev/null +++ b/src/pkg/utils/url-utils.ts @@ -0,0 +1,71 @@ +import { decodePunycode } from "./punycode"; + +/** + * Converts a machine-encoded URL into a human-readable format. + * 把编码URL变成使用者可以阅读的格式 + */ +export const prettyUrl = (s: string | undefined | null, baseUrl?: string): string => { + if (!s) return ""; + + const EXTRA = { + DECODE_URI: 0, + DECODE_COMP: 1, + PRESERVE_Q: 2, + PRESERVE_H: 4, + PRESERVE_A: 8, + } as const; + const safeDecode = (val: string, extra: number) => { + try { + const decodeFn = extra & EXTRA.DECODE_COMP ? decodeURIComponent : decodeURI; + let decoded = decodeFn(val); + // Re-encode delimiters to prevent breaking the URL structure + if (extra & EXTRA.PRESERVE_Q) decoded = decoded.replace(/[=&\s]/g, encodeURIComponent); + if (extra & EXTRA.PRESERVE_H) decoded = decoded.replace(/\s/g, encodeURIComponent); + if (extra & EXTRA.PRESERVE_A) decoded = decoded.replace(/[=&:@/\\\s]/g, encodeURIComponent); + return decoded; + } catch { + return val; + } + }; + + try { + const u = new URL(s, baseUrl); + + // 1. Core components: Protocol, Punycode Host, and Port + const protocol = u.protocol ? `${u.protocol}//` : ""; + const host = u.hostname + .split(".") + .map((p) => { + try { + return p.startsWith("xn--") ? decodePunycode(p) : p; + } catch { + // punycode 解码失败时回退到原始 label,避免整个 prettyUrl 失败 + return p; + } + }) + .join("."); + const port = u.port ? `:${u.port}` : ""; + + // 2. Decode Path and Hash safely + const path = safeDecode(u.pathname, EXTRA.DECODE_URI); + let hash = safeDecode(u.hash, EXTRA.DECODE_URI | EXTRA.PRESERVE_H); + if (!hash && s.endsWith("#")) hash = "#"; + + // 3. Search Params: Decode key/value pairs while escaping delimiters + const params = Array.from(new URLSearchParams(u.search)); + const m = params.map( + ([k, v]) => + `${safeDecode(k, EXTRA.DECODE_COMP | EXTRA.PRESERVE_Q)}=${safeDecode(v, EXTRA.DECODE_COMP | EXTRA.PRESERVE_Q)}` + ); + const search = params.length ? `?${m.join("&")}` : ""; + + // 4. Auth: User and Password + const user = safeDecode(u.username, EXTRA.DECODE_COMP | EXTRA.PRESERVE_A); + const pass = safeDecode(u.password, EXTRA.DECODE_COMP | EXTRA.PRESERVE_A); + const auth = user ? `${user}${pass ? `:${pass}` : ""}@` : ""; + + return `${protocol}${auth}${host}${port}${path}${search}${hash}`; + } catch { + return s; + } +}; diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 4ca023d9e..9b9ff2fe4 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -460,40 +460,6 @@ export const formatBytes = (bytes: number, decimals: number = 2): string => { return `${value.toFixed(decimals)} ${units[i]}`; }; -// 把编码URL变成使用者可以阅读的格式 -export const prettyUrl = (s: string | undefined | null, baseUrl?: string) => { - if (s?.includes("://")) { - let u; - try { - u = baseUrl ? new URL(s, baseUrl) : new URL(s); - } catch { - // ignored - } - if (!u) return s; - const pathname = u.pathname; - if (pathname && pathname.includes("%")) { - try { - const raw = decodeURI(pathname); - if ( - raw && - raw.length < pathname.length && - !raw.includes("?") && - !raw.includes("#") && - !raw.includes("&") && - !raw.includes("=") && - !raw.includes("%") && - !raw.includes(":") - ) { - s = s.replace(pathname, raw); - } - } catch { - // ignored - } - } - } - return s; -}; - // TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白 export const normalizeResponseHeaders = (headersString: string) => { if (!headersString) return "";