UNPKG

@libsql/core

Version:

libSQL driver for TypeScript and JavaScript

121 lines (120 loc) 4.6 kB
// URI parser based on RFC 3986 // We can't use the standard `URL` object, because we want to support relative `file:` URLs like // `file:relative/path/database.db`, which are not correct according to RFC 8089, which standardizes the // `file` scheme. import { LibsqlError } from "./api.js"; export function parseUri(text) { const match = URI_RE.exec(text); if (match === null) { throw new LibsqlError(`The URL '${text}' is not in a valid format`, "URL_INVALID"); } const groups = match.groups; const scheme = groups["scheme"]; const authority = groups["authority"] !== undefined ? parseAuthority(groups["authority"]) : undefined; const path = percentDecode(groups["path"]); const query = groups["query"] !== undefined ? parseQuery(groups["query"]) : undefined; const fragment = groups["fragment"] !== undefined ? percentDecode(groups["fragment"]) : undefined; return { scheme, authority, path, query, fragment }; } const URI_RE = (() => { const SCHEME = "(?<scheme>[A-Za-z][A-Za-z.+-]*)"; const AUTHORITY = "(?<authority>[^/?#]*)"; const PATH = "(?<path>[^?#]*)"; const QUERY = "(?<query>[^#]*)"; const FRAGMENT = "(?<fragment>.*)"; return new RegExp(`^${SCHEME}:(//${AUTHORITY})?${PATH}(\\?${QUERY})?(#${FRAGMENT})?$`, "su"); })(); function parseAuthority(text) { const match = AUTHORITY_RE.exec(text); if (match === null) { throw new LibsqlError("The authority part of the URL is not in a valid format", "URL_INVALID"); } const groups = match.groups; const host = percentDecode(groups["host_br"] ?? groups["host"]); const port = groups["port"] ? parseInt(groups["port"], 10) : undefined; const userinfo = groups["username"] !== undefined ? { username: percentDecode(groups["username"]), password: groups["password"] !== undefined ? percentDecode(groups["password"]) : undefined, } : undefined; return { host, port, userinfo }; } const AUTHORITY_RE = (() => { return new RegExp(`^((?<username>[^:]*)(:(?<password>.*))?@)?((?<host>[^:\\[\\]]*)|(\\[(?<host_br>[^\\[\\]]*)\\]))(:(?<port>[0-9]*))?$`, "su"); })(); // Query string is parsed as application/x-www-form-urlencoded according to the Web URL standard: // https://url.spec.whatwg.org/#urlencoded-parsing function parseQuery(text) { const sequences = text.split("&"); const pairs = []; for (const sequence of sequences) { if (sequence === "") { continue; } let key; let value; const splitIdx = sequence.indexOf("="); if (splitIdx < 0) { key = sequence; value = ""; } else { key = sequence.substring(0, splitIdx); value = sequence.substring(splitIdx + 1); } pairs.push({ key: percentDecode(key.replaceAll("+", " ")), value: percentDecode(value.replaceAll("+", " ")), }); } return { pairs }; } function percentDecode(text) { try { return decodeURIComponent(text); } catch (e) { if (e instanceof URIError) { throw new LibsqlError(`URL component has invalid percent encoding: ${e}`, "URL_INVALID", undefined, e); } throw e; } } export function encodeBaseUrl(scheme, authority, path) { if (authority === undefined) { throw new LibsqlError(`URL with scheme ${JSON.stringify(scheme + ":")} requires authority (the "//" part)`, "URL_INVALID"); } const schemeText = `${scheme}:`; const hostText = encodeHost(authority.host); const portText = encodePort(authority.port); const userinfoText = encodeUserinfo(authority.userinfo); const authorityText = `//${userinfoText}${hostText}${portText}`; let pathText = path.split("/").map(encodeURIComponent).join("/"); if (pathText !== "" && !pathText.startsWith("/")) { pathText = "/" + pathText; } return new URL(`${schemeText}${authorityText}${pathText}`); } function encodeHost(host) { return host.includes(":") ? `[${encodeURI(host)}]` : encodeURI(host); } function encodePort(port) { return port !== undefined ? `:${port}` : ""; } function encodeUserinfo(userinfo) { if (userinfo === undefined) { return ""; } const usernameText = encodeURIComponent(userinfo.username); const passwordText = userinfo.password !== undefined ? `:${encodeURIComponent(userinfo.password)}` : ""; return `${usernameText}${passwordText}@`; }