84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
/**
|
|
* Pure string predicates for artifact path syntax. The local artifact
|
|
* resolver, the Electron native specifier check, and (by reference) the
|
|
* live-DOM and source-map URL handling share these character-level rules.
|
|
*
|
|
* Policy divergence (deliberate, not yet unified):
|
|
* - Local artifact paths resolve against an inventoried container root and
|
|
* report rejected | external | not-found. Confinement is lexical
|
|
* (normalizeJoinedLogicalPath must stay inside the root).
|
|
* - Live DOM destinations resolve against the document base URL and report
|
|
* approved | outside_policy | unsupported against allowedOrigins
|
|
* (see CdpCaptureDocuments.domDestination).
|
|
* - Source-map fetches require exact-origin http(s) without credentials
|
|
* (see WebSourceMapFetcher.approvedUrl).
|
|
* Unifying those outcome vocabularies is a separate, observable change.
|
|
*/
|
|
|
|
/** Whether a reference carries a URI scheme (`https:`, `file:`, …). */
|
|
export const hasScheme = (value: string): boolean =>
|
|
/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value);
|
|
|
|
/** Whether a reference is scheme-qualified or protocol-relative. */
|
|
export const looksExternal = (value: string): boolean =>
|
|
hasScheme(value) || value.startsWith("//");
|
|
|
|
/** Drop any query string and fragment from a declared reference. */
|
|
export const stripQueryAndFragment = (value: string): string =>
|
|
value.split("#", 1)[0]?.split("?", 1)[0] ?? "";
|
|
|
|
/** Path syntax admitted for a canonical artifact path. */
|
|
export const admitsCanonicalPathSyntax = (value: string): boolean =>
|
|
!value.includes("\0") &&
|
|
!value.includes("\\") &&
|
|
!/%(?:2e|2f|5c)/iu.test(value);
|
|
|
|
/** C0 controls and space, which the URL parser strips around its input. */
|
|
const isUrlTrimmed = (code: number): boolean => code <= 0x20;
|
|
|
|
/**
|
|
* The URL text a browser parses from an HTML URL attribute: HTML strips
|
|
* surrounding ASCII whitespace, and the URL parser strips surrounding C0
|
|
* controls and spaces and removes every tab and newline.
|
|
*/
|
|
export const htmlUrlText = (value: string): string => {
|
|
let start = 0;
|
|
let end = value.length;
|
|
while (start < end && isUrlTrimmed(value.charCodeAt(start))) start += 1;
|
|
while (end > start && isUrlTrimmed(value.charCodeAt(end - 1))) end -= 1;
|
|
return value.slice(start, end).replace(/[\t\n\r]/gu, "");
|
|
};
|
|
|
|
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
const UTF8_ENCODER = new TextEncoder();
|
|
|
|
/**
|
|
* Percent-decode a URL path as the URL standard does: valid `%XX` sequences
|
|
* become bytes and any other `%` stays literal. Returns null when the bytes
|
|
* are not UTF-8 or include NUL, because no canonical artifact path can match
|
|
* them. Callers refuse encoded dot and separator bytes before decoding.
|
|
*/
|
|
export const percentDecodeUrlPath = (value: string): string | null => {
|
|
if (!/%[0-9a-f]{2}/iu.test(value)) return value;
|
|
const bytes: number[] = [];
|
|
for (let index = 0; index < value.length;) {
|
|
const escape = value.slice(index, index + 3);
|
|
if (/^%[0-9a-f]{2}$/iu.test(escape)) {
|
|
bytes.push(Number.parseInt(escape.slice(1), 16));
|
|
index += 3;
|
|
continue;
|
|
}
|
|
const character = String.fromCodePoint(value.codePointAt(index) ?? 0);
|
|
bytes.push(...UTF8_ENCODER.encode(character));
|
|
index += character.length;
|
|
}
|
|
try {
|
|
const decoded = UTF8.decode(new Uint8Array(bytes));
|
|
return decoded.includes("\0") ? null : decoded;
|
|
} catch (cause: unknown) {
|
|
// Invalid UTF-8 cannot name an inventoried path.
|
|
void cause;
|
|
return null;
|
|
}
|
|
};
|