diff options
Diffstat (limited to 'std/fs/path')
m--------- | std | 0 | ||||
-rw-r--r-- | std/fs/path/README.md | 7 | ||||
-rw-r--r-- | std/fs/path/basename_test.ts | 76 | ||||
-rw-r--r-- | std/fs/path/constants.ts | 54 | ||||
-rw-r--r-- | std/fs/path/dirname_test.ts | 62 | ||||
-rw-r--r-- | std/fs/path/extname_test.ts | 90 | ||||
-rw-r--r-- | std/fs/path/interface.ts | 27 | ||||
-rw-r--r-- | std/fs/path/isabsolute_test.ts | 34 | ||||
-rw-r--r-- | std/fs/path/join_test.ts | 128 | ||||
-rw-r--r-- | std/fs/path/mod.ts | 25 | ||||
-rw-r--r-- | std/fs/path/parse_format_test.ts | 180 | ||||
-rw-r--r-- | std/fs/path/posix.ts | 422 | ||||
-rw-r--r-- | std/fs/path/relative_test.ts | 73 | ||||
-rw-r--r-- | std/fs/path/resolve_test.ts | 52 | ||||
-rw-r--r-- | std/fs/path/test.ts | 9 | ||||
-rw-r--r-- | std/fs/path/utils.ts | 116 | ||||
-rw-r--r-- | std/fs/path/win32.ts | 896 | ||||
-rw-r--r-- | std/fs/path/zero_length_strings_test.ts | 49 |
18 files changed, 2300 insertions, 0 deletions
diff --git a/std b/std deleted file mode 160000 -Subproject 43aafbf33285753e7b42230f0eb7969b300f71c diff --git a/std/fs/path/README.md b/std/fs/path/README.md new file mode 100644 index 000000000..93d268aa7 --- /dev/null +++ b/std/fs/path/README.md @@ -0,0 +1,7 @@ +# Deno Path Manipulation Libraries + +Usage: + +```ts +import * as path from "https://deno.land/std/fs/path.ts"; +``` diff --git a/std/fs/path/basename_test.ts b/std/fs/path/basename_test.ts new file mode 100644 index 000000000..f7770d1ca --- /dev/null +++ b/std/fs/path/basename_test.ts @@ -0,0 +1,76 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function basename() { + assertEquals(path.basename(".js", ".js"), ""); + assertEquals(path.basename(""), ""); + assertEquals(path.basename("/dir/basename.ext"), "basename.ext"); + assertEquals(path.basename("/basename.ext"), "basename.ext"); + assertEquals(path.basename("basename.ext"), "basename.ext"); + assertEquals(path.basename("basename.ext/"), "basename.ext"); + assertEquals(path.basename("basename.ext//"), "basename.ext"); + assertEquals(path.basename("aaa/bbb", "/bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "a/bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb//", "bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "bb"), "b"); + assertEquals(path.basename("aaa/bbb", "b"), "bb"); + assertEquals(path.basename("/aaa/bbb", "/bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "a/bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb//", "bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "bb"), "b"); + assertEquals(path.basename("/aaa/bbb", "b"), "bb"); + assertEquals(path.basename("/aaa/bbb"), "bbb"); + assertEquals(path.basename("/aaa/"), "aaa"); + assertEquals(path.basename("/aaa/b"), "b"); + assertEquals(path.basename("/a/b"), "b"); + assertEquals(path.basename("//a"), "a"); + + // On unix a backslash is just treated as any other character. + assertEquals( + path.posix.basename("\\dir\\basename.ext"), + "\\dir\\basename.ext" + ); + assertEquals(path.posix.basename("\\basename.ext"), "\\basename.ext"); + assertEquals(path.posix.basename("basename.ext"), "basename.ext"); + assertEquals(path.posix.basename("basename.ext\\"), "basename.ext\\"); + assertEquals(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); + assertEquals(path.posix.basename("foo"), "foo"); + + // POSIX filenames may include control characters + const controlCharFilename = "Icon" + String.fromCharCode(13); + assertEquals( + path.posix.basename("/a/b/" + controlCharFilename), + controlCharFilename + ); +}); + +test(function basenameWin32() { + assertEquals(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext\\"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext\\\\"), "basename.ext"); + assertEquals(path.win32.basename("foo"), "foo"); + assertEquals(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "bb"), "b"); + assertEquals(path.win32.basename("aaa\\bbb", "b"), "bb"); + assertEquals(path.win32.basename("C:"), ""); + assertEquals(path.win32.basename("C:."), "."); + assertEquals(path.win32.basename("C:\\"), ""); + assertEquals(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); + assertEquals(path.win32.basename("C:\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext\\"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); + assertEquals(path.win32.basename("C:foo"), "foo"); + assertEquals(path.win32.basename("file:stream"), "file:stream"); +}); diff --git a/std/fs/path/constants.ts b/std/fs/path/constants.ts new file mode 100644 index 000000000..1e1eeeb49 --- /dev/null +++ b/std/fs/path/constants.ts @@ -0,0 +1,54 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { build } = Deno; + +// Alphabet chars. +export const CHAR_UPPERCASE_A = 65; /* A */ +export const CHAR_LOWERCASE_A = 97; /* a */ +export const CHAR_UPPERCASE_Z = 90; /* Z */ +export const CHAR_LOWERCASE_Z = 122; /* z */ + +// Non-alphabetic chars. +export const CHAR_DOT = 46; /* . */ +export const CHAR_FORWARD_SLASH = 47; /* / */ +export const CHAR_BACKWARD_SLASH = 92; /* \ */ +export const CHAR_VERTICAL_LINE = 124; /* | */ +export const CHAR_COLON = 58; /* : */ +export const CHAR_QUESTION_MARK = 63; /* ? */ +export const CHAR_UNDERSCORE = 95; /* _ */ +export const CHAR_LINE_FEED = 10; /* \n */ +export const CHAR_CARRIAGE_RETURN = 13; /* \r */ +export const CHAR_TAB = 9; /* \t */ +export const CHAR_FORM_FEED = 12; /* \f */ +export const CHAR_EXCLAMATION_MARK = 33; /* ! */ +export const CHAR_HASH = 35; /* # */ +export const CHAR_SPACE = 32; /* */ +export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */ +export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */ +export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */ +export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */ +export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */ +export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */ +export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */ +export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */ +export const CHAR_HYPHEN_MINUS = 45; /* - */ +export const CHAR_PLUS = 43; /* + */ +export const CHAR_DOUBLE_QUOTE = 34; /* " */ +export const CHAR_SINGLE_QUOTE = 39; /* ' */ +export const CHAR_PERCENT = 37; /* % */ +export const CHAR_SEMICOLON = 59; /* ; */ +export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */ +export const CHAR_GRAVE_ACCENT = 96; /* ` */ +export const CHAR_AT = 64; /* @ */ +export const CHAR_AMPERSAND = 38; /* & */ +export const CHAR_EQUAL = 61; /* = */ + +// Digits +export const CHAR_0 = 48; /* 0 */ +export const CHAR_9 = 57; /* 9 */ + +export const isWindows = build.os === "win"; +export const EOL = isWindows ? "\r\n" : "\n"; +export const SEP = isWindows ? "\\" : "/"; +export const SEP_PATTERN = isWindows ? /[\\/]+/ : /\/+/; diff --git a/std/fs/path/dirname_test.ts b/std/fs/path/dirname_test.ts new file mode 100644 index 000000000..047d4859b --- /dev/null +++ b/std/fs/path/dirname_test.ts @@ -0,0 +1,62 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function dirname() { + assertEquals(path.posix.dirname("/a/b/"), "/a"); + assertEquals(path.posix.dirname("/a/b"), "/a"); + assertEquals(path.posix.dirname("/a"), "/"); + assertEquals(path.posix.dirname(""), "."); + assertEquals(path.posix.dirname("/"), "/"); + assertEquals(path.posix.dirname("////"), "/"); + assertEquals(path.posix.dirname("//a"), "//"); + assertEquals(path.posix.dirname("foo"), "."); +}); + +test(function dirnameWin32() { + assertEquals(path.win32.dirname("c:\\"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo\\"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo\\bar"), "c:\\foo"); + assertEquals(path.win32.dirname("c:\\foo\\bar\\"), "c:\\foo"); + assertEquals(path.win32.dirname("c:\\foo\\bar\\baz"), "c:\\foo\\bar"); + assertEquals(path.win32.dirname("\\"), "\\"); + assertEquals(path.win32.dirname("\\foo"), "\\"); + assertEquals(path.win32.dirname("\\foo\\"), "\\"); + assertEquals(path.win32.dirname("\\foo\\bar"), "\\foo"); + assertEquals(path.win32.dirname("\\foo\\bar\\"), "\\foo"); + assertEquals(path.win32.dirname("\\foo\\bar\\baz"), "\\foo\\bar"); + assertEquals(path.win32.dirname("c:"), "c:"); + assertEquals(path.win32.dirname("c:foo"), "c:"); + assertEquals(path.win32.dirname("c:foo\\"), "c:"); + assertEquals(path.win32.dirname("c:foo\\bar"), "c:foo"); + assertEquals(path.win32.dirname("c:foo\\bar\\"), "c:foo"); + assertEquals(path.win32.dirname("c:foo\\bar\\baz"), "c:foo\\bar"); + assertEquals(path.win32.dirname("file:stream"), "."); + assertEquals(path.win32.dirname("dir\\file:stream"), "dir"); + assertEquals(path.win32.dirname("\\\\unc\\share"), "\\\\unc\\share"); + assertEquals(path.win32.dirname("\\\\unc\\share\\foo"), "\\\\unc\\share\\"); + assertEquals(path.win32.dirname("\\\\unc\\share\\foo\\"), "\\\\unc\\share\\"); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar"), + "\\\\unc\\share\\foo" + ); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar\\"), + "\\\\unc\\share\\foo" + ); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar\\baz"), + "\\\\unc\\share\\foo\\bar" + ); + assertEquals(path.win32.dirname("/a/b/"), "/a"); + assertEquals(path.win32.dirname("/a/b"), "/a"); + assertEquals(path.win32.dirname("/a"), "/"); + assertEquals(path.win32.dirname(""), "."); + assertEquals(path.win32.dirname("/"), "/"); + assertEquals(path.win32.dirname("////"), "/"); + assertEquals(path.win32.dirname("foo"), "."); +}); diff --git a/std/fs/path/extname_test.ts b/std/fs/path/extname_test.ts new file mode 100644 index 000000000..336d6b0b2 --- /dev/null +++ b/std/fs/path/extname_test.ts @@ -0,0 +1,90 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const slashRE = /\//g; + +const pairs = [ + ["", ""], + ["/path/to/file", ""], + ["/path/to/file.ext", ".ext"], + ["/path.to/file.ext", ".ext"], + ["/path.to/file", ""], + ["/path.to/.file", ""], + ["/path.to/.file.ext", ".ext"], + ["/path/to/f.ext", ".ext"], + ["/path/to/..ext", ".ext"], + ["/path/to/..", ""], + ["file", ""], + ["file.ext", ".ext"], + [".file", ""], + [".file.ext", ".ext"], + ["/file", ""], + ["/file.ext", ".ext"], + ["/.file", ""], + ["/.file.ext", ".ext"], + [".path/file.ext", ".ext"], + ["file.ext.ext", ".ext"], + ["file.", "."], + [".", ""], + ["./", ""], + [".file.ext", ".ext"], + [".file", ""], + [".file.", "."], + [".file..", "."], + ["..", ""], + ["../", ""], + ["..file.ext", ".ext"], + ["..file", ".file"], + ["..file.", "."], + ["..file..", "."], + ["...", "."], + ["...ext", ".ext"], + ["....", "."], + ["file.ext/", ".ext"], + ["file.ext//", ".ext"], + ["file/", ""], + ["file//", ""], + ["file./", "."], + ["file.//", "."] +]; + +test(function extname() { + pairs.forEach(function(p) { + const input = p[0]; + const expected = p[1]; + assertEquals(expected, path.posix.extname(input)); + }); + + // On *nix, backslash is a valid name component like any other character. + assertEquals(path.posix.extname(".\\"), ""); + assertEquals(path.posix.extname("..\\"), ".\\"); + assertEquals(path.posix.extname("file.ext\\"), ".ext\\"); + assertEquals(path.posix.extname("file.ext\\\\"), ".ext\\\\"); + assertEquals(path.posix.extname("file\\"), ""); + assertEquals(path.posix.extname("file\\\\"), ""); + assertEquals(path.posix.extname("file.\\"), ".\\"); + assertEquals(path.posix.extname("file.\\\\"), ".\\\\"); +}); + +test(function extnameWin32() { + pairs.forEach(function(p) { + const input = p[0].replace(slashRE, "\\"); + const expected = p[1]; + assertEquals(expected, path.win32.extname(input)); + assertEquals(expected, path.win32.extname("C:" + input)); + }); + + // On Windows, backslash is a path separator. + assertEquals(path.win32.extname(".\\"), ""); + assertEquals(path.win32.extname("..\\"), ""); + assertEquals(path.win32.extname("file.ext\\"), ".ext"); + assertEquals(path.win32.extname("file.ext\\\\"), ".ext"); + assertEquals(path.win32.extname("file\\"), ""); + assertEquals(path.win32.extname("file\\\\"), ""); + assertEquals(path.win32.extname("file.\\"), "."); + assertEquals(path.win32.extname("file.\\\\"), "."); +}); diff --git a/std/fs/path/interface.ts b/std/fs/path/interface.ts new file mode 100644 index 000000000..b31c89ea7 --- /dev/null +++ b/std/fs/path/interface.ts @@ -0,0 +1,27 @@ +/** + * A parsed path object generated by path.parse() or consumed by path.format(). + */ +export interface ParsedPath { + /** + * The root of the path such as '/' or 'c:\' + */ + root: string; + /** + * The full directory path such as '/home/user/dir' or 'c:\path\dir' + */ + dir: string; + /** + * The file name including extension (if any) such as 'index.html' + */ + base: string; + /** + * The file extension (if any) such as '.html' + */ + ext: string; + /** + * The file name without extension (if any) such as 'index' + */ + name: string; +} + +export type FormatInputPathObject = Partial<ParsedPath>; diff --git a/std/fs/path/isabsolute_test.ts b/std/fs/path/isabsolute_test.ts new file mode 100644 index 000000000..87218a185 --- /dev/null +++ b/std/fs/path/isabsolute_test.ts @@ -0,0 +1,34 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function isAbsolute() { + assertEquals(path.posix.isAbsolute("/home/foo"), true); + assertEquals(path.posix.isAbsolute("/home/foo/.."), true); + assertEquals(path.posix.isAbsolute("bar/"), false); + assertEquals(path.posix.isAbsolute("./baz"), false); +}); + +test(function isAbsoluteWin32() { + assertEquals(path.win32.isAbsolute("/"), true); + assertEquals(path.win32.isAbsolute("//"), true); + assertEquals(path.win32.isAbsolute("//server"), true); + assertEquals(path.win32.isAbsolute("//server/file"), true); + assertEquals(path.win32.isAbsolute("\\\\server\\file"), true); + assertEquals(path.win32.isAbsolute("\\\\server"), true); + assertEquals(path.win32.isAbsolute("\\\\"), true); + assertEquals(path.win32.isAbsolute("c"), false); + assertEquals(path.win32.isAbsolute("c:"), false); + assertEquals(path.win32.isAbsolute("c:\\"), true); + assertEquals(path.win32.isAbsolute("c:/"), true); + assertEquals(path.win32.isAbsolute("c://"), true); + assertEquals(path.win32.isAbsolute("C:/Users/"), true); + assertEquals(path.win32.isAbsolute("C:\\Users\\"), true); + assertEquals(path.win32.isAbsolute("C:cwd/another"), false); + assertEquals(path.win32.isAbsolute("C:cwd\\another"), false); + assertEquals(path.win32.isAbsolute("directory/directory"), false); + assertEquals(path.win32.isAbsolute("directory\\directory"), false); +}); diff --git a/std/fs/path/join_test.ts b/std/fs/path/join_test.ts new file mode 100644 index 000000000..2c0f35e48 --- /dev/null +++ b/std/fs/path/join_test.ts @@ -0,0 +1,128 @@ +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const backslashRE = /\\/g; + +const joinTests = + // arguments result + [ + [[".", "x/b", "..", "/b/c.js"], "x/b/c.js"], + [[], "."], + [["/.", "x/b", "..", "/b/c.js"], "/x/b/c.js"], + [["/foo", "../../../bar"], "/bar"], + [["foo", "../../../bar"], "../../bar"], + [["foo/", "../../../bar"], "../../bar"], + [["foo/x", "../../../bar"], "../bar"], + [["foo/x", "./bar"], "foo/x/bar"], + [["foo/x/", "./bar"], "foo/x/bar"], + [["foo/x/", ".", "bar"], "foo/x/bar"], + [["./"], "./"], + [[".", "./"], "./"], + [[".", ".", "."], "."], + [[".", "./", "."], "."], + [[".", "/./", "."], "."], + [[".", "/////./", "."], "."], + [["."], "."], + [["", "."], "."], + [["", "foo"], "foo"], + [["foo", "/bar"], "foo/bar"], + [["", "/foo"], "/foo"], + [["", "", "/foo"], "/foo"], + [["", "", "foo"], "foo"], + [["foo", ""], "foo"], + [["foo/", ""], "foo/"], + [["foo", "", "/bar"], "foo/bar"], + [["./", "..", "/foo"], "../foo"], + [["./", "..", "..", "/foo"], "../../foo"], + [[".", "..", "..", "/foo"], "../../foo"], + [["", "..", "..", "/foo"], "../../foo"], + [["/"], "/"], + [["/", "."], "/"], + [["/", ".."], "/"], + [["/", "..", ".."], "/"], + [[""], "."], + [["", ""], "."], + [[" /foo"], " /foo"], + [[" ", "foo"], " /foo"], + [[" ", "."], " "], + [[" ", "/"], " /"], + [[" ", ""], " "], + [["/", "foo"], "/foo"], + [["/", "/foo"], "/foo"], + [["/", "//foo"], "/foo"], + [["/", "", "/foo"], "/foo"], + [["", "/", "foo"], "/foo"], + [["", "/", "/foo"], "/foo"] + ]; + +// Windows-specific join tests +const windowsJoinTests = [ + // arguments result + // UNC path expected + [["//foo/bar"], "\\\\foo\\bar\\"], + [["\\/foo/bar"], "\\\\foo\\bar\\"], + [["\\\\foo/bar"], "\\\\foo\\bar\\"], + // UNC path expected - server and share separate + [["//foo", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "bar"], "\\\\foo\\bar\\"], + [["//foo", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - questionable + [["//foo", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - even more questionable + [["", "//foo", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "/bar"], "\\\\foo\\bar\\"], + // No UNC path expected (no double slash in first component) + [["\\", "foo/bar"], "\\foo\\bar"], + [["\\", "/foo/bar"], "\\foo\\bar"], + [["", "/", "/foo/bar"], "\\foo\\bar"], + // No UNC path expected (no non-slashes in first component - + // questionable) + [["//", "foo/bar"], "\\foo\\bar"], + [["//", "/foo/bar"], "\\foo\\bar"], + [["\\\\", "/", "/foo/bar"], "\\foo\\bar"], + [["//"], "\\"], + // No UNC path expected (share name missing - questionable). + [["//foo"], "\\foo"], + [["//foo/"], "\\foo\\"], + [["//foo", "/"], "\\foo\\"], + [["//foo", "", "/"], "\\foo\\"], + // No UNC path expected (too many leading slashes - questionable) + [["///foo/bar"], "\\foo\\bar"], + [["////foo", "bar"], "\\foo\\bar"], + [["\\\\\\/foo/bar"], "\\foo\\bar"], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [["c:"], "c:."], + [["c:."], "c:."], + [["c:", ""], "c:."], + [["", "c:"], "c:."], + [["c:.", "/"], "c:.\\"], + [["c:.", "file"], "c:file"], + [["c:", "/"], "c:\\"], + [["c:", "file"], "c:\\file"] +]; + +test(function join() { + joinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.posix.join.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); + +test(function joinWin32() { + joinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.join.apply(null, _p).replace(backslashRE, "/"); + assertEquals(actual, p[1]); + }); + windowsJoinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.join.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); diff --git a/std/fs/path/mod.ts b/std/fs/path/mod.ts new file mode 100644 index 000000000..660c061d6 --- /dev/null +++ b/std/fs/path/mod.ts @@ -0,0 +1,25 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import * as _win32 from "./win32.ts"; +import * as _posix from "./posix.ts"; + +import { isWindows } from "./constants.ts"; + +const path = isWindows ? _win32 : _posix; + +export const win32 = _win32; +export const posix = _posix; +export const resolve = path.resolve; +export const normalize = path.normalize; +export const isAbsolute = path.isAbsolute; +export const join = path.join; +export const relative = path.relative; +export const toNamespacedPath = path.toNamespacedPath; +export const dirname = path.dirname; +export const basename = path.basename; +export const extname = path.extname; +export const format = path.format; +export const parse = path.parse; +export const sep = path.sep; +export const delimiter = path.delimiter; diff --git a/std/fs/path/parse_format_test.ts b/std/fs/path/parse_format_test.ts new file mode 100644 index 000000000..db83c3354 --- /dev/null +++ b/std/fs/path/parse_format_test.ts @@ -0,0 +1,180 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// TODO(kt3k): fix any types in this file + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const winPaths = [ + // [path, root] + ["C:\\path\\dir\\index.html", "C:\\"], + ["C:\\another_path\\DIR\\1\\2\\33\\\\index", "C:\\"], + ["another_path\\DIR with spaces\\1\\2\\33\\index", ""], + ["\\", "\\"], + ["\\foo\\C:", "\\"], + ["file", ""], + ["file:stream", ""], + [".\\file", ""], + ["C:", "C:"], + ["C:.", "C:"], + ["C:..", "C:"], + ["C:abc", "C:"], + ["C:\\", "C:\\"], + ["C:\\abc", "C:\\"], + ["", ""], + + // unc + ["\\\\server\\share\\file_path", "\\\\server\\share\\"], + [ + "\\\\server two\\shared folder\\file path.zip", + "\\\\server two\\shared folder\\" + ], + ["\\\\teela\\admin$\\system32", "\\\\teela\\admin$\\"], + ["\\\\?\\UNC\\server\\share", "\\\\?\\UNC\\"] +]; + +const winSpecialCaseParseTests = [["/foo/bar", { root: "/" }]]; + +const winSpecialCaseFormatTests = [ + [{ dir: "some\\dir" }, "some\\dir\\"], + [{ base: "index.html" }, "index.html"], + [{ root: "C:\\" }, "C:\\"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some\\dir", name: "index", ext: ".html" }, "some\\dir\\index.html"], + [{ root: "C:\\", name: "index", ext: ".html" }, "C:\\index.html"], + [{}, ""] +]; + +const unixPaths = [ + // [path, root] + ["/home/user/dir/file.txt", "/"], + ["/home/user/a dir/another File.zip", "/"], + ["/home/user/a dir//another&File.", "/"], + ["/home/user/a$$$dir//another File.zip", "/"], + ["user/dir/another File.zip", ""], + ["file", ""], + [".\\file", ""], + ["./file", ""], + ["C:\\foo", ""], + ["/", "/"], + ["", ""], + [".", ""], + ["..", ""], + ["/foo", "/"], + ["/foo.", "/"], + ["/foo.bar", "/"], + ["/.", "/"], + ["/.foo", "/"], + ["/.foo.bar", "/"], + ["/foo/bar.baz", "/"] +]; + +const unixSpecialCaseFormatTests = [ + [{ dir: "some/dir" }, "some/dir/"], + [{ base: "index.html" }, "index.html"], + [{ root: "/" }, "/"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some/dir", name: "index", ext: ".html" }, "some/dir/index.html"], + [{ root: "/", name: "index", ext: ".html" }, "/index.html"], + [{}, ""] +]; + +function checkParseFormat(path: any, paths: any): void { + paths.forEach(function(p: Array<Record<string, unknown>>) { + const element = p[0]; + const output = path.parse(element); + assertEquals(typeof output.root, "string"); + assertEquals(typeof output.dir, "string"); + assertEquals(typeof output.base, "string"); + assertEquals(typeof output.ext, "string"); + assertEquals(typeof output.name, "string"); + assertEquals(path.format(output), element); + assertEquals(output.rooroot, undefined); + assertEquals(output.dir, output.dir ? path.dirname(element) : ""); + assertEquals(output.base, path.basename(element)); + }); +} + +function checkSpecialCaseParseFormat(path: any, testCases: any): void { + testCases.forEach(function(testCase: Array<Record<string, unknown>>) { + const element = testCase[0]; + const expect = testCase[1]; + const output = path.parse(element); + Object.keys(expect).forEach(function(key) { + assertEquals(output[key], expect[key]); + }); + }); +} + +function checkFormat(path: any, testCases: unknown[][]): void { + testCases.forEach(function(testCase) { + assertEquals(path.format(testCase[0]), testCase[1]); + }); +} + +test(function parseWin32() { + checkParseFormat(path.win32, winPaths); + checkSpecialCaseParseFormat(path.win32, winSpecialCaseParseTests); +}); + +test(function parse() { + checkParseFormat(path.posix, unixPaths); +}); + +test(function formatWin32() { + checkFormat(path.win32, winSpecialCaseFormatTests); +}); + +test(function format() { + checkFormat(path.posix, unixSpecialCaseFormatTests); +}); + +// Test removal of trailing path separators +const windowsTrailingTests = [ + [".\\", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + [ + "c:\\foo\\\\\\", + { root: "c:\\", dir: "c:\\", base: "foo", ext: "", name: "foo" } + ], + [ + "D:\\foo\\\\\\bar.baz", + { + root: "D:\\", + dir: "D:\\foo\\\\", + base: "bar.baz", + ext: ".baz", + name: "bar" + } + ] +]; + +const posixTrailingTests = [ + ["./", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["//", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["///", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["/foo///", { root: "/", dir: "/", base: "foo", ext: "", name: "foo" }], + [ + "/foo///bar.baz", + { root: "/", dir: "/foo//", base: "bar.baz", ext: ".baz", name: "bar" } + ] +]; + +test(function parseTrailingWin32() { + windowsTrailingTests.forEach(function(p) { + const actual = path.win32.parse(p[0] as string); + const expected = p[1]; + assertEquals(actual, expected); + }); +}); + +test(function parseTrailing() { + posixTrailingTests.forEach(function(p) { + const actual = path.posix.parse(p[0] as string); + const expected = p[1]; + assertEquals(actual, expected); + }); +}); diff --git a/std/fs/path/posix.ts b/std/fs/path/posix.ts new file mode 100644 index 000000000..4377fd542 --- /dev/null +++ b/std/fs/path/posix.ts @@ -0,0 +1,422 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { FormatInputPathObject, ParsedPath } from "./interface.ts"; +import { CHAR_DOT, CHAR_FORWARD_SLASH } from "./constants.ts"; + +import { + assertPath, + normalizeString, + isPosixPathSeparator, + _format +} from "./utils.ts"; + +export const sep = "/"; +export const delimiter = ":"; + +// path.resolve([from ...], to) +export function resolve(...pathSegments: string[]): string { + let resolvedPath = ""; + let resolvedAbsolute = false; + + for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + let path: string; + + if (i >= 0) path = pathSegments[i]; + else path = cwd(); + + assertPath(path); + + // Skip empty entries + if (path.length === 0) { + continue; + } + + resolvedPath = `${path}/${resolvedPath}`; + resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeString( + resolvedPath, + !resolvedAbsolute, + "/", + isPosixPathSeparator + ); + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) return `/${resolvedPath}`; + else return "/"; + } else if (resolvedPath.length > 0) return resolvedPath; + else return "."; +} + +export function normalize(path: string): string { + assertPath(path); + + if (path.length === 0) return "."; + + const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + const trailingSeparator = + path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH; + + // Normalize the path + path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator); + + if (path.length === 0 && !isAbsolute) path = "."; + if (path.length > 0 && trailingSeparator) path += "/"; + + if (isAbsolute) return `/${path}`; + return path; +} + +export function isAbsolute(path: string): boolean { + assertPath(path); + return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH; +} + +export function join(...paths: string[]): string { + if (paths.length === 0) return "."; + let joined: string | undefined; + for (let i = 0, len = paths.length; i < len; ++i) { + const path = paths[i]; + assertPath(path); + if (path.length > 0) { + if (!joined) joined = path; + else joined += `/${path}`; + } + } + if (!joined) return "."; + return normalize(joined); +} + +export function relative(from: string, to: string): string { + assertPath(from); + assertPath(to); + + if (from === to) return ""; + + from = resolve(from); + to = resolve(to); + + if (from === to) return ""; + + // Trim any leading backslashes + let fromStart = 1; + const fromEnd = from.length; + for (; fromStart < fromEnd; ++fromStart) { + if (from.charCodeAt(fromStart) !== CHAR_FORWARD_SLASH) break; + } + const fromLen = fromEnd - fromStart; + + // Trim any leading backslashes + let toStart = 1; + const toEnd = to.length; + for (; toStart < toEnd; ++toStart) { + if (to.charCodeAt(toStart) !== CHAR_FORWARD_SLASH) break; + } + const toLen = toEnd - toStart; + + // Compare paths to find the longest common path from root + const length = fromLen < toLen ? fromLen : toLen; + let lastCommonSep = -1; + let i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(toStart + i + 1); + } else if (i === 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return to.slice(toStart + i); + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = i; + } else if (i === 0) { + // We get here if `to` is the root. + // For example: from='/foo'; to='/' + lastCommonSep = 0; + } + } + break; + } + const fromCode = from.charCodeAt(fromStart + i); + const toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) break; + else if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i; + } + + let out = ""; + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) { + if (out.length === 0) out += ".."; + else out += "/.."; + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === CHAR_FORWARD_SLASH) ++toStart; + return to.slice(toStart); + } +} + +export function toNamespacedPath(path: string): string { + // Non-op on posix systems + return path; +} + +export function dirname(path: string): string { + assertPath(path); + if (path.length === 0) return "."; + const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + let end = -1; + let matchedSlash = true; + for (let i = path.length - 1; i >= 1; --i) { + if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) return hasRoot ? "/" : "."; + if (hasRoot && end === 1) return "//"; + return path.slice(0, end); +} + +export function basename(path: string, ext = ""): string { + if (ext !== undefined && typeof ext !== "string") + throw new TypeError('"ext" argument must be a string'); + assertPath(path); + + let start = 0; + let end = -1; + let matchedSlash = true; + let i: number; + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return ""; + let extIdx = ext.length - 1; + let firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) return ""; + return path.slice(start, end); + } +} + +export function extname(path: string): string { + assertPath(path); + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ""; + } + return path.slice(startDot, end); +} + +export function format(pathObject: FormatInputPathObject): string { + /* eslint-disable max-len */ + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError( + `The "pathObject" argument must be of type Object. Received type ${typeof pathObject}` + ); + } + return _format("/", pathObject); +} + +export function parse(path: string): ParsedPath { + assertPath(path); + + const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" }; + if (path.length === 0) return ret; + const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + let start: number; + if (isAbsolute) { + ret.root = "/"; + start = 1; + } else { + start = 0; + } + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + let i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Get non-dir info + for (; i >= start; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) { + ret.base = ret.name = path.slice(1, end); + } else { + ret.base = ret.name = path.slice(startPart, end); + } + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot); + ret.base = path.slice(1, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); + } + + if (startPart > 0) ret.dir = path.slice(0, startPart - 1); + else if (isAbsolute) ret.dir = "/"; + + return ret; +} diff --git a/std/fs/path/relative_test.ts b/std/fs/path/relative_test.ts new file mode 100644 index 000000000..0188b5368 --- /dev/null +++ b/std/fs/path/relative_test.ts @@ -0,0 +1,73 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const relativeTests = { + win32: + // arguments result + [ + ["c:/blah\\blah", "d:/games", "d:\\games"], + ["c:/aaaa/bbbb", "c:/aaaa", ".."], + ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], + ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], + ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], + ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], + ["c:/aaaa/bbbb", "d:\\", "d:\\"], + ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], + ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], + [ + "C:\\foo\\test", + "C:\\foo\\test\\bar\\package.json", + "bar\\package.json" + ], + ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], + ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], + ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["C:\\baz-quux", "C:\\baz", "..\\baz"], + ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], + ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], + ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], + ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"] + ], + posix: + // arguments result + [ + ["/var/lib", "/var", ".."], + ["/var/lib", "/bin", "../../bin"], + ["/var/lib", "/var/lib", ""], + ["/var/lib", "/var/apache", "../apache"], + ["/var/", "/var/lib", "lib"], + ["/", "/var/lib", "var/lib"], + ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], + ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], + ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], + ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], + ["/baz-quux", "/baz", "../baz"], + ["/baz", "/baz-quux", "../baz-quux"] + ] +}; + +test(function relative() { + relativeTests.posix.forEach(function(p) { + const expected = p[2]; + const actual = path.posix.relative(p[0], p[1]); + assertEquals(actual, expected); + }); +}); + +test(function relativeWin32() { + relativeTests.win32.forEach(function(p) { + const expected = p[2]; + const actual = path.win32.relative(p[0], p[1]); + assertEquals(actual, expected); + }); +}); diff --git a/std/fs/path/resolve_test.ts b/std/fs/path/resolve_test.ts new file mode 100644 index 000000000..606570aad --- /dev/null +++ b/std/fs/path/resolve_test.ts @@ -0,0 +1,52 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const windowsTests = + // arguments result + [ + [["c:/blah\\blah", "d:/games", "c:../a"], "c:\\blah\\a"], + [["c:/ignore", "d:\\a/b\\c/d", "\\e.exe"], "d:\\e.exe"], + [["c:/ignore", "c:/some/file"], "c:\\some\\file"], + [["d:/ignore", "d:some/dir//"], "d:\\ignore\\some\\dir"], + [["//server/share", "..", "relative\\"], "\\\\server\\share\\relative"], + [["c:/", "//"], "c:\\"], + [["c:/", "//dir"], "c:\\dir"], + [["c:/", "//server/share"], "\\\\server\\share\\"], + [["c:/", "//server//share"], "\\\\server\\share\\"], + [["c:/", "///some//dir"], "c:\\some\\dir"], + [ + ["C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"], + "C:\\foo\\tmp.3\\cycles\\root.js" + ] + ]; +const posixTests = + // arguments result + [ + [["/var/lib", "../", "file/"], "/var/file"], + [["/var/lib", "/../", "file/"], "/file"], + [["a/b/c/", "../../.."], cwd()], + [["."], cwd()], + [["/some/dir", ".", "/absolute/"], "/absolute"], + [["/foo/tmp.3/", "../tmp.3/cycles/root.js"], "/foo/tmp.3/cycles/root.js"] + ]; + +test(function resolve() { + posixTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.posix.resolve.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); + +test(function resolveWin32() { + windowsTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.resolve.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); diff --git a/std/fs/path/test.ts b/std/fs/path/test.ts new file mode 100644 index 000000000..3664ae5f1 --- /dev/null +++ b/std/fs/path/test.ts @@ -0,0 +1,9 @@ +import "./basename_test.ts"; +import "./dirname_test.ts"; +import "./extname_test.ts"; +import "./isabsolute_test.ts"; +import "./join_test.ts"; +import "./parse_format_test.ts"; +import "./relative_test.ts"; +import "./resolve_test.ts"; +import "./zero_length_strings_test.ts"; diff --git a/std/fs/path/utils.ts b/std/fs/path/utils.ts new file mode 100644 index 000000000..7a4cd7299 --- /dev/null +++ b/std/fs/path/utils.ts @@ -0,0 +1,116 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { FormatInputPathObject } from "./interface.ts"; +import { + CHAR_UPPERCASE_A, + CHAR_LOWERCASE_A, + CHAR_UPPERCASE_Z, + CHAR_LOWERCASE_Z, + CHAR_DOT, + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH +} from "./constants.ts"; + +export function assertPath(path: string): void { + if (typeof path !== "string") { + throw new TypeError( + `Path must be a string. Received ${JSON.stringify(path)}` + ); + } +} + +export function isPosixPathSeparator(code: number): boolean { + return code === CHAR_FORWARD_SLASH; +} + +export function isPathSeparator(code: number): boolean { + return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH; +} + +export function isWindowsDeviceRoot(code: number): boolean { + return ( + (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) || + (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) + ); +} + +// Resolves . and .. elements in a path with directory names +export function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code: number) => boolean +): string { + let res = ""; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code: number; + for (let i = 0, len = path.length; i <= len; ++i) { + if (i < len) code = path.charCodeAt(i); + else if (isPathSeparator(code!)) break; + else code = CHAR_FORWARD_SLASH; + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CHAR_DOT || + res.charCodeAt(res.length - 2) !== CHAR_DOT + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length === 2 || res.length === 1) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) res += `${separator}..`; + else res = ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); + else res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CHAR_DOT && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + +export function _format( + sep: string, + pathObject: FormatInputPathObject +): string { + const dir: string | undefined = pathObject.dir || pathObject.root; + const base: string = + pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); + if (!dir) return base; + if (dir === pathObject.root) return dir + base; + return dir + sep + base; +} diff --git a/std/fs/path/win32.ts b/std/fs/path/win32.ts new file mode 100644 index 000000000..79e04ea6e --- /dev/null +++ b/std/fs/path/win32.ts @@ -0,0 +1,896 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd, env } = Deno; +import { FormatInputPathObject, ParsedPath } from "./interface.ts"; +import { + CHAR_DOT, + CHAR_BACKWARD_SLASH, + CHAR_COLON, + CHAR_QUESTION_MARK +} from "./constants.ts"; + +import { + assertPath, + isPathSeparator, + isWindowsDeviceRoot, + normalizeString, + _format +} from "./utils.ts"; + +export const sep = "\\"; +export const delimiter = ";"; + +export function resolve(...pathSegments: string[]): string { + let resolvedDevice = ""; + let resolvedTail = ""; + let resolvedAbsolute = false; + + for (let i = pathSegments.length - 1; i >= -1; i--) { + let path: string; + if (i >= 0) { + path = pathSegments[i]; + } else if (!resolvedDevice) { + path = cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive, or the process cwd if + // the drive cwd is not available. We're sure the device is not + // a UNC path at this points, because UNC paths are always absolute. + path = env()[`=${resolvedDevice}`] || cwd(); + + // Verify that a cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if ( + path === undefined || + path.slice(0, 3).toLowerCase() !== `${resolvedDevice.toLowerCase()}\\` + ) { + path = `${resolvedDevice}\\`; + } + } + + assertPath(path); + + const len = path.length; + + // Skip empty entries + if (len === 0) continue; + + let rootEnd = 0; + let device = ""; + let isAbsolute = false; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an + // absolute path of some kind (UNC or otherwise) + isAbsolute = true; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + device = `\\\\${firstPart}\\${path.slice(last)}`; + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = `\\\\${firstPart}\\${path.slice(last, j)}`; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator + rootEnd = 1; + isAbsolute = true; + } + + if ( + device.length > 0 && + resolvedDevice.length > 0 && + device.toLowerCase() !== resolvedDevice.toLowerCase() + ) { + // This path points to another device so it is not applicable + continue; + } + + if (resolvedDevice.length === 0 && device.length > 0) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`; + resolvedAbsolute = isAbsolute; + } + + if (resolvedAbsolute && resolvedDevice.length > 0) break; + } + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + resolvedTail = normalizeString( + resolvedTail, + !resolvedAbsolute, + "\\", + isPathSeparator + ); + + return resolvedDevice + (resolvedAbsolute ? "\\" : "") + resolvedTail || "."; +} + +export function normalize(path: string): string { + assertPath(path); + const len = path.length; + if (len === 0) return "."; + let rootEnd = 0; + let device: string | undefined; + let isAbsolute = false; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an absolute + // path of some kind (UNC or otherwise) + isAbsolute = true; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + // Return the normalized version of the UNC root since there + // is nothing left to process + + return `\\\\${firstPart}\\${path.slice(last)}\\`; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = `\\\\${firstPart}\\${path.slice(last, j)}`; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid unnecessary + // work + return "\\"; + } + + let tail: string; + if (rootEnd < len) { + tail = normalizeString( + path.slice(rootEnd), + !isAbsolute, + "\\", + isPathSeparator + ); + } else { + tail = ""; + } + if (tail.length === 0 && !isAbsolute) tail = "."; + if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) + tail += "\\"; + if (device === undefined) { + if (isAbsolute) { + if (tail.length > 0) return `\\${tail}`; + else return "\\"; + } else if (tail.length > 0) { + return tail; + } else { + return ""; + } + } else if (isAbsolute) { + if (tail.length > 0) return `${device}\\${tail}`; + else return `${device}\\`; + } else if (tail.length > 0) { + return device + tail; + } else { + return device; + } +} + +export function isAbsolute(path: string): boolean { + assertPath(path); + const len = path.length; + if (len === 0) return false; + + const code = path.charCodeAt(0); + if (isPathSeparator(code)) { + return true; + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (len > 2 && path.charCodeAt(1) === CHAR_COLON) { + if (isPathSeparator(path.charCodeAt(2))) return true; + } + } + return false; +} + +export function join(...paths: string[]): string { + const pathsCount = paths.length; + if (pathsCount === 0) return "."; + + let joined: string | undefined; + let firstPart: string; + for (let i = 0; i < pathsCount; ++i) { + const path = paths[i]; + assertPath(path); + if (path.length > 0) { + if (joined === undefined) joined = firstPart = path; + else joined += `\\${path}`; + } + } + + if (joined === undefined) return "."; + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for an UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at an UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as an UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\\') + let needsReplace = true; + let slashCount = 0; + firstPart = firstPart!; + if (isPathSeparator(firstPart.charCodeAt(0))) { + ++slashCount; + const firstLen = firstPart.length; + if (firstLen > 1) { + if (isPathSeparator(firstPart.charCodeAt(1))) { + ++slashCount; + if (firstLen > 2) { + if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount; + else { + // We matched a UNC path in the first part + needsReplace = false; + } + } + } + } + } + if (needsReplace) { + // Find any more consecutive slashes we need to replace + for (; slashCount < joined.length; ++slashCount) { + if (!isPathSeparator(joined.charCodeAt(slashCount))) break; + } + + // Replace the slashes if needed + if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`; + } + + return normalize(joined); +} + +// It will solve the relative path from `from` to `to`, for instance: +// from = 'C:\\orandea\\test\\aaa' +// to = 'C:\\orandea\\impl\\bbb' +// The output of the function should be: '..\\..\\impl\\bbb' +export function relative(from: string, to: string): string { + assertPath(from); + assertPath(to); + + if (from === to) return ""; + + const fromOrig = resolve(from); + const toOrig = resolve(to); + + if (fromOrig === toOrig) return ""; + + from = fromOrig.toLowerCase(); + to = toOrig.toLowerCase(); + + if (from === to) return ""; + + // Trim any leading backslashes + let fromStart = 0; + let fromEnd = from.length; + for (; fromStart < fromEnd; ++fromStart) { + if (from.charCodeAt(fromStart) !== CHAR_BACKWARD_SLASH) break; + } + // Trim trailing backslashes (applicable to UNC paths only) + for (; fromEnd - 1 > fromStart; --fromEnd) { + if (from.charCodeAt(fromEnd - 1) !== CHAR_BACKWARD_SLASH) break; + } + const fromLen = fromEnd - fromStart; + + // Trim any leading backslashes + let toStart = 0; + let toEnd = to.length; + for (; toStart < toEnd; ++toStart) { + if (to.charCodeAt(toStart) !== CHAR_BACKWARD_SLASH) break; + } + // Trim trailing backslashes (applicable to UNC paths only) + for (; toEnd - 1 > toStart; --toEnd) { + if (to.charCodeAt(toEnd - 1) !== CHAR_BACKWARD_SLASH) break; + } + const toLen = toEnd - toStart; + + // Compare paths to find the longest common path from root + const length = fromLen < toLen ? fromLen : toLen; + let lastCommonSep = -1; + let i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' + return toOrig.slice(toStart + i + 1); + } else if (i === 2) { + // We get here if `from` is the device root. + // For example: from='C:\\'; to='C:\\foo' + return toOrig.slice(toStart + i); + } + } + if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='C:\\foo\\bar'; to='C:\\foo' + lastCommonSep = i; + } else if (i === 2) { + // We get here if `to` is the device root. + // For example: from='C:\\foo\\bar'; to='C:\\' + lastCommonSep = 3; + } + } + break; + } + const fromCode = from.charCodeAt(fromStart + i); + const toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) break; + else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i; + } + + // We found a mismatch before the first common path separator was seen, so + // return the original `to`. + if (i !== length && lastCommonSep === -1) { + return toOrig; + } + + let out = ""; + if (lastCommonSep === -1) lastCommonSep = 0; + // Generate the relative path based on the path difference between `to` and + // `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) { + if (out.length === 0) out += ".."; + else out += "\\.."; + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) return out + toOrig.slice(toStart + lastCommonSep, toEnd); + else { + toStart += lastCommonSep; + if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart; + return toOrig.slice(toStart, toEnd); + } +} + +export function toNamespacedPath(path: string): string { + // Note: this will *probably* throw somewhere. + if (typeof path !== "string") return path; + if (path.length === 0) return ""; + + const resolvedPath = resolve(path); + + if (resolvedPath.length >= 3) { + if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) { + // Possible UNC root + + if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) { + const code = resolvedPath.charCodeAt(2); + if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) { + // Matched non-long UNC root, convert the path to a long UNC path + return `\\\\?\\UNC\\${resolvedPath.slice(2)}`; + } + } + } else if (isWindowsDeviceRoot(resolvedPath.charCodeAt(0))) { + // Possible device root + + if ( + resolvedPath.charCodeAt(1) === CHAR_COLON && + resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH + ) { + // Matched device root, convert the path to a long UNC path + return `\\\\?\\${resolvedPath}`; + } + } + } + + return path; +} + +export function dirname(path: string): string { + assertPath(path); + const len = path.length; + if (len === 0) return "."; + let rootEnd = -1; + let end = -1; + let matchedSlash = true; + let offset = 0; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + rootEnd = offset = 1; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + return path; + } + if (j !== last) { + // We matched a UNC root with leftovers + + // Offset by 1 to include the separator after the UNC root to + // treat it as a "normal root" on top of a (UNC) root + rootEnd = offset = j + 1; + } + } + } + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + rootEnd = offset = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3; + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + return path; + } + + for (let i = len - 1; i >= offset; --i) { + if (isPathSeparator(path.charCodeAt(i))) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) { + if (rootEnd === -1) return "."; + else end = rootEnd; + } + return path.slice(0, end); +} + +export function basename(path: string, ext = ""): string { + if (ext !== undefined && typeof ext !== "string") + throw new TypeError('"ext" argument must be a string'); + + assertPath(path); + + let start = 0; + let end = -1; + let matchedSlash = true; + let i: number; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + if (path.length >= 2) { + const drive = path.charCodeAt(0); + if (isWindowsDeviceRoot(drive)) { + if (path.charCodeAt(1) === CHAR_COLON) start = 2; + } + } + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return ""; + let extIdx = ext.length - 1; + let firstNonSlashEnd = -1; + for (i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= start; --i) { + if (isPathSeparator(path.charCodeAt(i))) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) return ""; + return path.slice(start, end); + } +} + +export function extname(path: string): string { + assertPath(path); + let start = 0; + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + + if ( + path.length >= 2 && + path.charCodeAt(1) === CHAR_COLON && + isWindowsDeviceRoot(path.charCodeAt(0)) + ) { + start = startPart = 2; + } + + for (let i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ""; + } + return path.slice(startDot, end); +} + +export function format(pathObject: FormatInputPathObject): string { + /* eslint-disable max-len */ + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError( + `The "pathObject" argument must be of type Object. Received type ${typeof pathObject}` + ); + } + return _format("\\", pathObject); +} + +export function parse(path: string): ParsedPath { + assertPath(path); + + const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" }; + + const len = path.length; + if (len === 0) return ret; + + let rootEnd = 0; + let code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + rootEnd = 1; + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + rootEnd = j + 1; + } + } + } + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + if (len === 3) { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + rootEnd = 3; + } + } else { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + + if (rootEnd > 0) ret.root = path.slice(0, rootEnd); + + let startDot = -1; + let startPart = rootEnd; + let end = -1; + let matchedSlash = true; + let i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Get non-dir info + for (; i >= rootEnd; --i) { + code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + if (end !== -1) { + ret.base = ret.name = path.slice(startPart, end); + } + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + ret.ext = path.slice(startDot, end); + } + + // If the directory is the root, use the entire root as the `dir` including + // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the + // trailing slash (`C:\abc\def` -> `C:\abc`). + if (startPart > 0 && startPart !== rootEnd) { + ret.dir = path.slice(0, startPart - 1); + } else ret.dir = ret.root; + + return ret; +} diff --git a/std/fs/path/zero_length_strings_test.ts b/std/fs/path/zero_length_strings_test.ts new file mode 100644 index 000000000..744e97735 --- /dev/null +++ b/std/fs/path/zero_length_strings_test.ts @@ -0,0 +1,49 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const pwd = cwd(); + +test(function joinZeroLength() { + // join will internally ignore all the zero-length strings and it will return + // '.' if the joined string is a zero-length string. + assertEquals(path.posix.join(""), "."); + assertEquals(path.posix.join("", ""), "."); + if (path.win32) assertEquals(path.win32.join(""), "."); + if (path.win32) assertEquals(path.win32.join("", ""), "."); + assertEquals(path.join(pwd), pwd); + assertEquals(path.join(pwd, ""), pwd); +}); + +test(function normalizeZeroLength() { + // normalize will return '.' if the input is a zero-length string + assertEquals(path.posix.normalize(""), "."); + if (path.win32) assertEquals(path.win32.normalize(""), "."); + assertEquals(path.normalize(pwd), pwd); +}); + +test(function isAbsoluteZeroLength() { + // Since '' is not a valid path in any of the common environments, + // return false + assertEquals(path.posix.isAbsolute(""), false); + if (path.win32) assertEquals(path.win32.isAbsolute(""), false); +}); + +test(function resolveZeroLength() { + // resolve, internally ignores all the zero-length strings and returns the + // current working directory + assertEquals(path.resolve(""), pwd); + assertEquals(path.resolve("", ""), pwd); +}); + +test(function relativeZeroLength() { + // relative, internally calls resolve. So, '' is actually the current + // directory + assertEquals(path.relative("", pwd), ""); + assertEquals(path.relative(pwd, ""), ""); + assertEquals(path.relative(pwd, pwd), ""); +}); |