diff --git a/std/node/_utils.ts b/std/node/_utils.ts index 352179f4ab..b8c4fa00e0 100644 --- a/std/node/_utils.ts +++ b/std/node/_utils.ts @@ -39,3 +39,8 @@ export function intoCallbackAPIWithIntercept( .then((value) => cb && cb(null, interceptor(value))) .catch((err) => cb && cb(err, null)); } + +export function spliceOne(list: string[], index: number): void { + for (; index + 1 < list.length; index++) list[index] = list[index + 1]; + list.pop(); +} diff --git a/std/node/querystring.ts b/std/node/querystring.ts index 427183bf02..bed3273377 100644 --- a/std/node/querystring.ts +++ b/std/node/querystring.ts @@ -4,6 +4,9 @@ interface ParseOptions { decodeURIComponent?: (string: string) => string; maxKeys?: number; } +export const hexTable = new Array(256); +for (let i = 0; i < 256; ++i) + hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(); export function parse( str: string, @@ -43,6 +46,67 @@ interface StringifyOptions { encodeURIComponent?: (string: string) => string; } +export function encodeStr( + str: string, + noEscapeTable: number[], + hexTable: string[] +): string { + const len = str.length; + if (len === 0) return ""; + + let out = ""; + let lastPos = 0; + + for (let i = 0; i < len; i++) { + let c = str.charCodeAt(i); + // ASCII + if (c < 0x80) { + if (noEscapeTable[c] === 1) continue; + if (lastPos < i) out += str.slice(lastPos, i); + lastPos = i + 1; + out += hexTable[c]; + continue; + } + + if (lastPos < i) out += str.slice(lastPos, i); + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)]; + continue; + } + if (c < 0xd800 || c >= 0xe000) { + lastPos = i + 1; + out += + hexTable[0xe0 | (c >> 12)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; + continue; + } + // Surrogate pair + ++i; + + // This branch should never happen because all URLSearchParams entries + // should already be converted to USVString. But, included for + // completion's sake anyway. + if (i >= len) throw new Deno.errors.InvalidData("invalid URI"); + + const c2 = str.charCodeAt(i) & 0x3ff; + + lastPos = i + 1; + c = 0x10000 + (((c & 0x3ff) << 10) | c2); + out += + hexTable[0xf0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3f)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; + } + if (lastPos === 0) return str; + if (lastPos < len) return out + str.slice(lastPos); + return out; +} + export function stringify( obj: object, sep = "&", diff --git a/std/node/url.ts b/std/node/url.ts new file mode 100644 index 0000000000..bbf0ff5d00 --- /dev/null +++ b/std/node/url.ts @@ -0,0 +1,127 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { + CHAR_LOWERCASE_A, + CHAR_LOWERCASE_Z, + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH, +} from "../path/_constants.ts"; +import * as path from "./path.ts"; + +const isWindows = Deno.build.os === "windows"; + +const forwardSlashRegEx = /\//g; +const percentRegEx = /%/g; +const backslashRegEx = /\\/g; +const newlineRegEx = /\n/g; +const carriageReturnRegEx = /\r/g; +const tabRegEx = /\t/g; + +export function fileURLToPath(path: string | URL): string { + if (typeof path === "string") path = new URL(path); + else if (!(path instanceof URL)) + throw new Deno.errors.InvalidData( + "invalid argument path , must be a string or URL" + ); + if (path.protocol !== "file:") + throw new Deno.errors.InvalidData("invalid url scheme"); + return isWindows ? getPathFromURLWin(path) : getPathFromURLPosix(path); +} + +function getPathFromURLWin(url: URL): string { + const hostname = url.hostname; + let pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === "%") { + const third = pathname.codePointAt(n + 2) || 0x20; + if ( + (pathname[n + 1] === "2" && third === 102) || // 2f 2F / + (pathname[n + 1] === "5" && third === 99) + ) { + // 5c 5C \ + throw new Deno.errors.InvalidData( + "must not include encoded \\ or / characters" + ); + } + } + } + + pathname = pathname.replace(forwardSlashRegEx, "\\"); + pathname = decodeURIComponent(pathname); + if (hostname !== "") { + //TODO add support for punycode encodings + return `\\\\${hostname}${pathname}`; + } else { + // Otherwise, it's a local path that requires a drive letter + const letter = pathname.codePointAt(1) || 0x20; + const sep = pathname[2]; + if ( + letter < CHAR_LOWERCASE_A || + letter > CHAR_LOWERCASE_Z || // a..z A..Z + sep !== ":" + ) { + throw new Deno.errors.InvalidData("file url path must be absolute"); + } + return pathname.slice(1); + } +} + +function getPathFromURLPosix(url: URL): string { + if (url.hostname !== "") { + throw new Deno.errors.InvalidData("invalid file url hostname"); + } + const pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === "%") { + const third = pathname.codePointAt(n + 2) || 0x20; + if (pathname[n + 1] === "2" && third === 102) { + throw new Deno.errors.InvalidData( + "must not include encoded / characters" + ); + } + } + } + return decodeURIComponent(pathname); +} + +export function pathToFileURL(filepath: string): URL { + let resolved = path.resolve(filepath); + // path.resolve strips trailing slashes so we must add them back + const filePathLast = filepath.charCodeAt(filepath.length - 1); + if ( + (filePathLast === CHAR_FORWARD_SLASH || + (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && + resolved[resolved.length - 1] !== path.sep + ) + resolved += "/"; + const outURL = new URL("file://"); + if (resolved.includes("%")) resolved = resolved.replace(percentRegEx, "%25"); + // In posix, "/" is a valid character in paths + if (!isWindows && resolved.includes("\\")) + resolved = resolved.replace(backslashRegEx, "%5C"); + if (resolved.includes("\n")) resolved = resolved.replace(newlineRegEx, "%0A"); + if (resolved.includes("\r")) + resolved = resolved.replace(carriageReturnRegEx, "%0D"); + if (resolved.includes("\t")) resolved = resolved.replace(tabRegEx, "%09"); + outURL.pathname = resolved; + return outURL; +}