diff options
author | 2022-02-02 23:33:37 -0800 | |
---|---|---|
committer | 2022-02-02 23:33:37 -0800 | |
commit | a52a948a706724895979604e780a6a5d5db92f95 (patch) | |
tree | c72e88af48b3cb78f542f1ed9ccb412e72cc9a04 | |
parent | 94cbfa45792342105fb1a5b69cafc3a8f4a1ba7a (diff) | |
download | bun-a52a948a706724895979604e780a6a5d5db92f95.tar.gz bun-a52a948a706724895979604e780a6a5d5db92f95.tar.zst bun-a52a948a706724895979604e780a6a5d5db92f95.zip |
`path.relative` passes Node's tests (which also fixed bugs)
-rw-r--r-- | integration/bunjs-only-snippets/path.test.js | 455 | ||||
-rw-r--r-- | src/javascript/jsc/node/types.zig | 7 | ||||
-rw-r--r-- | src/javascript/jsc/path-posix.exports.js | 35 | ||||
-rw-r--r-- | src/javascript/jsc/path-win32.exports.js | 35 | ||||
-rw-r--r-- | src/javascript/jsc/path.exports.js | 18 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 288 | ||||
-rw-r--r-- | src/string_immutable.zig | 2 | ||||
-rw-r--r-- | src/test/tester.zig | 14 |
8 files changed, 571 insertions, 283 deletions
diff --git a/integration/bunjs-only-snippets/path.test.js b/integration/bunjs-only-snippets/path.test.js index 7f4708b31..292a79f5e 100644 --- a/integration/bunjs-only-snippets/path.test.js +++ b/integration/bunjs-only-snippets/path.test.js @@ -1,184 +1,307 @@ +const { file } = import.meta; + import { describe, it, expect } from "bun:test"; import * as path from "node:path"; +import assert from "assert"; -const __filename = import.meta.file; +const strictEqual = (...args) => { + assert.strictEqual(...args); + expect(true).toBe(true); +}; -describe("path.basename", () => { - it("basics", () => { - expect(path.basename(__filename)).toBe("path.test.js"); - expect(path.basename(__filename, ".js")).toBe("path.test"); - expect(path.basename(".js", ".js")).toBe(""); - expect(path.basename("")).toBe(""); - expect(path.basename("/dir/basename.ext")).toBe("basename.ext"); - expect(path.basename("/basename.ext")).toBe("basename.ext"); - expect(path.basename("basename.ext")).toBe("basename.ext"); - expect(path.basename("basename.ext/")).toBe("basename.ext"); - expect(path.basename("basename.ext//")).toBe("basename.ext"); - expect(path.basename("aaa/bbb", "/bbb")).toBe("bbb"); - expect(path.basename("aaa/bbb", "a/bbb")).toBe("bbb"); - expect(path.basename("aaa/bbb", "bbb")).toBe("bbb"); - expect(path.basename("aaa/bbb//", "bbb")).toBe("bbb"); - expect(path.basename("aaa/bbb", "bb")).toBe("b"); - expect(path.basename("aaa/bbb", "b")).toBe("bb"); - expect(path.basename("/aaa/bbb", "/bbb")).toBe("bbb"); - expect(path.basename("/aaa/bbb", "a/bbb")).toBe("bbb"); - expect(path.basename("/aaa/bbb", "bbb")).toBe("bbb"); - expect(path.basename("/aaa/bbb//", "bbb")).toBe("bbb"); - expect(path.basename("/aaa/bbb", "bb")).toBe("b"); - expect(path.basename("/aaa/bbb", "b")).toBe("bb"); - expect(path.basename("/aaa/bbb")).toBe("bbb"); - expect(path.basename("/aaa/")).toBe("aaa"); - expect(path.basename("/aaa/b")).toBe("b"); - expect(path.basename("/a/b")).toBe("b"); - expect(path.basename("//a")).toBe("a"); - }); +it("path.basename", () => { + strictEqual(path.basename(file), "path.test.js"); + strictEqual(path.basename(file, ".js"), "path.test"); + strictEqual(path.basename(".js", ".js"), ""); + strictEqual(path.basename(""), ""); + strictEqual(path.basename("/dir/basename.ext"), "basename.ext"); + strictEqual(path.basename("/basename.ext"), "basename.ext"); + strictEqual(path.basename("basename.ext"), "basename.ext"); + strictEqual(path.basename("basename.ext/"), "basename.ext"); + strictEqual(path.basename("basename.ext//"), "basename.ext"); + strictEqual(path.basename("aaa/bbb", "/bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "a/bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb//", "bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "bb"), "b"); + strictEqual(path.basename("aaa/bbb", "b"), "bb"); + strictEqual(path.basename("/aaa/bbb", "/bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "a/bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb//", "bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "bb"), "b"); + strictEqual(path.basename("/aaa/bbb", "b"), "bb"); + strictEqual(path.basename("/aaa/bbb"), "bbb"); + strictEqual(path.basename("/aaa/"), "aaa"); + strictEqual(path.basename("/aaa/b"), "b"); + strictEqual(path.basename("/a/b"), "b"); + strictEqual(path.basename("//a"), "a"); + strictEqual(path.basename("a", "a"), ""); - it("On unix a backslash is just treated as any other character.", () => { - expect(path.posix.basename("\\dir\\basename.ext")).toBe( - "\\dir\\basename.ext" - ); - expect(path.posix.basename("\\basename.ext")).toBe("\\basename.ext"); - expect(path.posix.basename("basename.ext")).toBe("basename.ext"); - expect(path.posix.basename("basename.ext\\")).toBe("basename.ext\\"); - expect(path.posix.basename("basename.ext\\\\")).toBe("basename.ext\\\\"); - expect(path.posix.basename("foo")).toBe("foo"); - }); + // On Windows a backslash acts as a path separator. + strictEqual(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext\\"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext\\\\"), "basename.ext"); + strictEqual(path.win32.basename("foo"), "foo"); + strictEqual(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "bb"), "b"); + strictEqual(path.win32.basename("aaa\\bbb", "b"), "bb"); + strictEqual(path.win32.basename("C:"), ""); + strictEqual(path.win32.basename("C:."), "."); + strictEqual(path.win32.basename("C:\\"), ""); + strictEqual(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); + strictEqual(path.win32.basename("C:\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext\\"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); + strictEqual(path.win32.basename("C:foo"), "foo"); + strictEqual(path.win32.basename("file:stream"), "file:stream"); + strictEqual(path.win32.basename("a", "a"), ""); - it("POSIX filenames may include control characters", () => { - // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html - var controlCharFilename = "Icon" + String.fromCharCode(13); - expect(path.posix.basename("/a/b/" + controlCharFilename)).toBe( - controlCharFilename - ); - }); + // On unix a backslash is just treated as any other character. + strictEqual( + path.posix.basename("\\dir\\basename.ext"), + "\\dir\\basename.ext" + ); + strictEqual(path.posix.basename("\\basename.ext"), "\\basename.ext"); + strictEqual(path.posix.basename("basename.ext"), "basename.ext"); + strictEqual(path.posix.basename("basename.ext\\"), "basename.ext\\"); + strictEqual(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); + strictEqual(path.posix.basename("foo"), "foo"); + + // POSIX filenames may include control characters + // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html + const controlCharFilename = `Icon${String.fromCharCode(13)}`; + strictEqual( + path.posix.basename(`/a/b/${controlCharFilename}`), + controlCharFilename + ); }); -it("path.posix.extname", () => { - var pairs = [ - [__filename, ".js"], - ["", ""], - ["/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.//", "."], +it("path.join", () => { + const failures = []; + const backslashRE = /\\/g; + + const joinTests = [ + [ + [path.posix.join], + // 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"], + ], + ], ]; - pairs.forEach(function (p) { - var input = p[0]; - var expected = p[1]; - expect(expected).toBe(path.posix.extname(input)); + // Windows-specific join tests + // joinTests.push([ + // path.win32.join, + // joinTests[0][1].slice(0).concat([ + // // 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'], + // ]), + // ]); + joinTests.forEach((test) => { + if (!Array.isArray(test[0])) test[0] = [test[0]]; + test[0].forEach((join) => { + test[1].forEach((test) => { + const actual = join.apply(null, test[0]); + const expected = test[1]; + // For non-Windows specific tests with the Windows join(), we need to try + // replacing the slashes since the non-Windows specific tests' `expected` + // use forward slashes + let actualAlt; + let os; + if (join === path.win32.join) { + actualAlt = actual.replace(backslashRE, "/"); + os = "win32"; + } else { + os = "posix"; + } + if (actual !== expected && actualAlt !== expected) { + const delimiter = test[0].map(JSON.stringify).join(","); + const message = `path.${os}.join(${delimiter})\n expect=${JSON.stringify( + expected + )}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); }); + strictEqual(failures.length, 0, failures.join("")); }); -it("path.posix.dirname", function (t) { - expect(path.posix.dirname("/a/b/")).toBe("a"); - expect(path.posix.dirname("/a/b")).toBe("a"); - expect(path.posix.dirname("/a")).toBe("/"); - expect(path.posix.dirname("")).toBe("."); - expect(path.posix.dirname("/")).toBe("/"); - expect(path.posix.dirname("//a")).toBe("//"); - expect(path.posix.dirname("foo")).toBe("."); -}); - -it("path.posix.isAbsolute", () => { - expect(path.posix.isAbsolute("/home/foo")).toBe(true); - expect(path.posix.isAbsolute("/home/foo/..")).toBe(true); - expect(path.posix.isAbsolute("bar/")).toBe(false); - expect(path.posix.isAbsolute("./baz")).toBe(false); -}); +it("path.relative", () => { + const failures = []; -tape("path.posix.join", () => { - var joinTests = - // arguments result + const relativeTests = [ + // [ + // path.win32.relative, + // // 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"], + // ], + // ], [ - [[".", "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"], - ]; + path.posix.relative, + // 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"], + ["/page1/page2/foo", "/", "../../.."], + ], + ], + ]; - joinTests.forEach(() => { - var actual = path.posix.join.apply(null, p[0]); - expect(actual).toBe(p[1]); + relativeTests.forEach((test) => { + const relative = test[0]; + test[1].forEach((test) => { + const actual = relative(test[0], test[1]); + const expected = test[2]; + if (actual !== expected) { + const os = relative === path.win32.relative ? "win32" : "posix"; + const message = `path.${os}.relative(${test + .slice(0, 2) + .map(JSON.stringify) + .join(",")})\n expect=${JSON.stringify( + expected + )}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); }); + + assert.strictEqual(failures.length, 0, failures.join("")); + expect(true).toBe(true); }); diff --git a/src/javascript/jsc/node/types.zig b/src/javascript/jsc/node/types.zig index 9238c3970..bbdbba30c 100644 --- a/src/javascript/jsc/node/types.zig +++ b/src/javascript/jsc/node/types.zig @@ -2168,6 +2168,7 @@ pub const Path = struct { } pub fn join(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + if (args_len == 0) return JSC.ZigString.init("").toValue(globalThis); var stack_fallback_allocator = std.heap.stackFallback( (32 * @sizeOf(string)), @@ -2263,7 +2264,7 @@ pub const Path = struct { var arguments = args_ptr[0..args_len]; if (args_len > 1 and JSC.JSValue.eqlValue(args_ptr[0], args_ptr[1])) - return JSC.ZigString.init(".").toValue(globalThis); + return JSC.ZigString.init("").toValue(globalThis); var from_slice: JSC.ZigString.Slice = if (args_len > 0) arguments[0].toSlice(globalThis, heap_allocator) else JSC.ZigString.Slice.empty; defer from_slice.deinit(); @@ -2274,9 +2275,9 @@ pub const Path = struct { var to = to_slice.slice(); var out = if (!isWindows) - PathHandler.relativeNormalized(from, to, .posix, false) + PathHandler.relativePlatform(from, to, .posix, true) else - PathHandler.relativeNormalized(from, to, .windows, false); + PathHandler.relativePlatform(from, to, .windows, true); var out_str = JSC.ZigString.init(out); out_str.detectEncoding(); diff --git a/src/javascript/jsc/path-posix.exports.js b/src/javascript/jsc/path-posix.exports.js new file mode 100644 index 000000000..b3f61c1a2 --- /dev/null +++ b/src/javascript/jsc/path-posix.exports.js @@ -0,0 +1,35 @@ +function bound(obj) { + return { + basename: obj.basename.bind(obj), + dirname: obj.dirname.bind(obj), + extname: obj.extname.bind(obj), + format: obj.format.bind(obj), + isAbsolute: obj.isAbsolute.bind(obj), + join: obj.join.bind(obj), + normalize: obj.normalize.bind(obj), + parse: obj.parse.bind(obj), + relative: obj.relative.bind(obj), + resolve: obj.resolve.bind(obj), + toNamespacedPath: obj.toNamespacedPath.bind(obj), + sep: obj.sep, + delimiter: obj.delimiter, + }; +} +var path = bound(Bun._Path(false)); + +export var { + basename, + dirname, + extname, + format, + isAbsolute, + join, + normalize, + parse, + relative, + resolve, + toNamespacedPath, + sep, + delimiter, +} = path; +export default path; diff --git a/src/javascript/jsc/path-win32.exports.js b/src/javascript/jsc/path-win32.exports.js new file mode 100644 index 000000000..932cc8960 --- /dev/null +++ b/src/javascript/jsc/path-win32.exports.js @@ -0,0 +1,35 @@ +function bound(obj) { + return { + basename: obj.basename.bind(obj), + dirname: obj.dirname.bind(obj), + extname: obj.extname.bind(obj), + format: obj.format.bind(obj), + isAbsolute: obj.isAbsolute.bind(obj), + join: obj.join.bind(obj), + normalize: obj.normalize.bind(obj), + parse: obj.parse.bind(obj), + relative: obj.relative.bind(obj), + resolve: obj.resolve.bind(obj), + toNamespacedPath: obj.toNamespacedPath.bind(obj), + sep: obj.sep, + delimiter: obj.delimiter, + }; +} +var path = bound(Bun._Path(true)); + +export var { + basename, + dirname, + extname, + format, + isAbsolute, + join, + normalize, + parse, + relative, + resolve, + toNamespacedPath, + sep, + delimiter, +} = path; +export default path; diff --git a/src/javascript/jsc/path.exports.js b/src/javascript/jsc/path.exports.js index 6fa7af88d..98f685fc1 100644 --- a/src/javascript/jsc/path.exports.js +++ b/src/javascript/jsc/path.exports.js @@ -21,7 +21,7 @@ path.posix = posix; export var posix = bound(Bun._Path(false)); export var win32 = bound(Bun._Path(true)); -var { +export var { basename, dirname, extname, @@ -37,20 +37,4 @@ var { delimiter, } = path; -export { - basename, - dirname, - extname, - format, - isAbsolute, - join, - normalize, - parse, - relative, - resolve, - toNamespacedPath, - sep, - delimiter, -}; - export default path; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index d77825211..30da31018 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1,16 +1,17 @@ const tester = @import("../test/tester.zig"); const std = @import("std"); +const strings = @import("../string_immutable.zig"); const FeatureFlags = @import("../feature_flags.zig"); const default_allocator = @import("../memory_allocator.zig").c_allocator; -threadlocal var parser_join_input_buffer: [1024]u8 = undefined; +threadlocal var parser_join_input_buffer: [4096]u8 = undefined; threadlocal var parser_buffer: [1024]u8 = undefined; -inline fn nqlAtIndex(comptime string_count: comptime_int, index: usize, strings: []const []const u8) bool { +inline fn nqlAtIndex(comptime string_count: comptime_int, index: usize, input: []const []const u8) bool { comptime var string_index = 1; inline while (string_index < string_count) : (string_index += 1) { - if (strings[0][index] != strings[string_index][index]) { + if (input[0][index] != input[string_index][index]) { return true; } } @@ -19,13 +20,14 @@ inline fn nqlAtIndex(comptime string_count: comptime_int, index: usize, strings: } const IsSeparatorFunc = fn (char: u8) bool; +const LastSeparatorFunction = fn (slice: []const u8) ?usize; // TODO: is it faster to determine longest_common_separator in the while loop // or as an extra step at the end? // only boether to check if this function appears in benchmarking -pub fn longestCommonPathGeneric(strings: []const []const u8, comptime separator: u8, comptime isPathSeparator: IsSeparatorFunc) []const u8 { +pub fn longestCommonPathGeneric(input: []const []const u8, comptime separator: u8, comptime isPathSeparator: IsSeparatorFunc) []const u8 { var min_length: usize = std.math.maxInt(usize); - for (strings) |str| { + for (input) |str| { min_length = @minimum(str.len, min_length); } @@ -33,79 +35,79 @@ pub fn longestCommonPathGeneric(strings: []const []const u8, comptime separator: var last_common_separator: usize = 0; // try to use an unrolled version of this loop - switch (strings.len) { + switch (input.len) { 0 => { return ""; }, 1 => { - return strings[0]; + return input[0]; }, 2 => { while (index < min_length) : (index += 1) { - if (strings[0][index] != strings[1][index]) { + if (input[0][index] != input[1][index]) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 3 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(3, index, strings)) { + if (nqlAtIndex(3, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 4 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(4, index, strings)) { + if (nqlAtIndex(4, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 5 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(5, index, strings)) { + if (nqlAtIndex(5, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 6 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(6, index, strings)) { + if (nqlAtIndex(6, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 7 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(7, index, strings)) { + if (nqlAtIndex(7, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, 8 => { while (index < min_length) : (index += 1) { - if (nqlAtIndex(8, index, strings)) { + if (nqlAtIndex(8, index, input)) { break; } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } @@ -113,12 +115,12 @@ pub fn longestCommonPathGeneric(strings: []const []const u8, comptime separator: else => { var string_index: usize = 1; while (index < min_length) : (index += 1) { - while (string_index < strings.len) : (string_index += 1) { - if (strings[0][index] != strings[index][string_index]) { + while (string_index < input.len) : (string_index += 1) { + if (input[0][index] != input[index][string_index]) { break; } } - if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{strings[0][index]})) { + if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } @@ -138,7 +140,7 @@ pub fn longestCommonPathGeneric(strings: []const []const u8, comptime separator: // /app/public/ // To detect /app/public is actually a folder, we do one more loop through the strings // and say, "do one of you have a path separator after what we thought was the end?" - for (strings) |str| { + for (input) |str| { if (str.len > index + 1) { if (@call(.{ .modifier = .always_inline }, isPathSeparator, .{str[index]})) { return str[0 .. index + 2]; @@ -146,19 +148,19 @@ pub fn longestCommonPathGeneric(strings: []const []const u8, comptime separator: } } - return strings[0][0 .. last_common_separator + 1]; + return input[0][0 .. last_common_separator + 1]; } -pub fn longestCommonPath(strings: []const []const u8) []const u8 { - return longestCommonPathGeneric(strings, '/', isSepAny); +pub fn longestCommonPath(input: []const []const u8) []const u8 { + return longestCommonPathGeneric(input, '/', isSepAny); } -pub fn longestCommonPathWindows(strings: []const []const u8) []const u8 { - return longestCommonPathGeneric(strings, std.fs.path.sep_windows, isSepWin32); +pub fn longestCommonPathWindows(input: []const []const u8) []const u8 { + return longestCommonPathGeneric(input, std.fs.path.sep_windows, isSepWin32); } -pub fn longestCommonPathPosix(strings: []const []const u8) []const u8 { - return longestCommonPathGeneric(strings, std.fs.path.sep_posix, isSepPosix); +pub fn longestCommonPathPosix(input: []const []const u8) []const u8 { + return longestCommonPathGeneric(input, std.fs.path.sep_posix, isSepPosix); } threadlocal var relative_to_common_path_buf: [4096]u8 = undefined; @@ -178,9 +180,9 @@ pub fn relativeToCommonPath( const common_path = if (has_leading_separator) _common_path[1..] else _common_path; - var shortest = @minimum(normalized_from.len, normalized_to.len); + const shortest = @minimum(normalized_from.len, normalized_to.len); - var last_common_separator = @maximum(common_path.len, 1) - 1; + var last_common_separator = std.mem.lastIndexOfScalar(u8, _common_path, separator) orelse 0; if (shortest == common_path.len) { if (normalized_to.len > normalized_from.len) { @@ -209,20 +211,6 @@ pub fn relativeToCommonPath( } } } - - if (normalized_from.len > normalized_to.len) { - // We get here if `to` is the exact base path for `from`. - // For example: from='/foo/bar/baz'; to='/foo/bar' - if (normalized_from[common_path.len - 1] == separator) { - last_common_separator = common_path.len - 1; - } else if (normalized_from[common_path.len] == separator) { - last_common_separator = common_path.len; - } else if (common_path.len == 0) { - // We get here if `to` is the root. - // For example: from='/foo/bar'; to='/' - last_common_separator = 0; - } - } } // Generate the relative path based on the path difference between `to` @@ -237,25 +225,27 @@ pub fn relativeToCommonPath( if (i == normalized_from.len or (normalized_from[i] == separator and i + 1 < normalized_from.len)) { if (out_slice.len == 0) { out_slice = buf[0 .. out_slice.len + 2]; - out_slice[0] = '.'; - out_slice[1] = '.'; + out_slice[0..2].* = "..".*; } else { - var old_len = out_slice.len; - out_slice = buf[0 .. out_slice.len + 3]; - out_slice[old_len] = separator; - old_len += 1; - out_slice[old_len] = '.'; - old_len += 1; - out_slice[old_len] = '.'; + const old_len = out_slice.len; + out_slice.len += 3; + out_slice[old_len..][0..3].* = "/..".*; } } } } if (normalized_to.len > last_common_separator + 1) { - const tail = normalized_to[last_common_separator..]; - const insert_leading_slash = last_common_separator > 0 and normalized_to[last_common_separator - 1] != separator and tail[0] != separator; + var tail = normalized_to[last_common_separator..]; + if (normalized_from.len > 0 and (last_common_separator == normalized_from.len or (last_common_separator == normalized_from.len - 1))) { + if (tail[0] == separator) { + tail = tail[1..]; + } + } + const insert_leading_slash = last_common_separator > 0 and normalized_to[last_common_separator] != separator and tail[0] != separator; + + // avoid making non-absolute paths absolute if (insert_leading_slash) { buf[out_slice.len] = separator; out_slice = buf[0 .. out_slice.len + 1]; @@ -273,8 +263,13 @@ pub fn relativeToCommonPath( } pub fn relativeNormalized(from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { + if (from.len == to.len and strings.eqlLong(from, to, true)) { + return ""; + } + const two = [_][]const u8{ from, to }; const common_path = longestCommonPathGeneric(&two, comptime platform.separator(), comptime platform.getSeparatorFunc()); + return relativeToCommonPath(common_path, from, to, &relative_to_common_path_buf, comptime platform.separator(), always_copy); } @@ -308,8 +303,27 @@ pub fn relative(from: []const u8, to: []const u8) []const u8 { } pub fn relativePlatform(from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { - const normalized_from = normalizeStringBuf(from, &relative_from_buf, true, platform, true); - const normalized_to = normalizeStringBuf(to, &relative_to_buf, true, platform, true); + const from_allow_above_root = (from.len > 1 and from[0] == platform.separator()); + const to_allow_above_root = (to.len > 1 and to[0] == platform.separator()); + var normalized_from = + if (!from_allow_above_root) + normalizeStringBuf(from, relative_from_buf[1..], false, platform, true) + else + normalizeStringBuf(from, relative_from_buf[1..], true, platform, true); + var normalized_to = + if (!to_allow_above_root) + normalizeStringBuf(to, relative_to_buf[1..], false, platform, true) + else + normalizeStringBuf(to, relative_to_buf[1..], true, platform, true); + + if (from_allow_above_root == to_allow_above_root and from_allow_above_root) { + relative_from_buf[0] = platform.separator(); + normalized_from = relative_from_buf[0 .. normalized_from.len + 1]; + + relative_to_buf[0] = platform.separator(); + normalized_to = relative_to_buf[0 .. normalized_to.len + 1]; + } + // return relativeNormalized(normalized_from, normalized_to, platform, always_copy); } @@ -348,7 +362,7 @@ pub fn normalizeStringGeneric(str: []const u8, buf: []u8, comptime allow_above_r if (last_slash == @intCast(i32, i) - 1 or dots == 1) { // NOOP } else if (dots == 2) { - if (written_len < 2 or last_segment_length != 2 or buf[written_len - 1] != '.' or buf[written_len - 2] != '.') { + if (written_len < 2 or last_segment_length != 2 or @bitCast(u16, buf[written_len - 2 ..][0..2].*) != std.mem.readIntNative(u16, "..")) { if (written_len > 2) { if (lastIndexOfSeparator(buf[0..written_len])) |last_slash_index| { written_len = last_slash_index; @@ -373,10 +387,8 @@ pub fn normalizeStringGeneric(str: []const u8, buf: []u8, comptime allow_above_r written_len += 1; } - buf[written_len] = '.'; - written_len += 1; - buf[written_len] = '.'; - written_len += 1; + buf[written_len .. written_len + 2][0..2].* = "..".*; + written_len += 2; last_segment_length = 2; } @@ -428,6 +440,14 @@ pub const Platform = enum { }; } + pub fn separatorString(comptime platform: Platform) []const u8 { + return comptime switch (platform) { + .auto => platform.resolve().separatorString(), + .loose, .posix => std.fs.path.sep_str_posix, + .windows => std.fs.path.sep_str_windows, + }; + } + pub const current: Platform = switch (@import("builtin").target.os.tag) { .windows => Platform.windows, else => Platform.posix, @@ -448,6 +468,21 @@ pub const Platform = enum { } } + pub fn getLastSeparatorFunc(comptime _platform: Platform) LastSeparatorFunction { + switch (comptime _platform.resolve()) { + .auto => unreachable, + .loose => { + return lastIndexOfSeparatorLoose; + }, + .windows => { + return lastIndexOfSeparatorWindows; + }, + .posix => { + return lastIndexOfSeparatorPosix; + }, + } + } + pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { switch (comptime _platform.resolve()) { .auto => unreachable, @@ -630,12 +665,14 @@ pub fn joinStringBuf(buf: []u8, _parts: anytype, comptime _platform: Platform) [ var written: usize = 0; const platform = comptime _platform.resolve(); + parser_join_input_buffer[0] = 0; + for (_parts) |part| { - if (part.len == 0 or (part.len == 1 and part[0] == '.')) { + if (part.len == 0) { continue; } - if (!platform.isSeparator(part[part.len - 1])) { + if (written > 0) { parser_join_input_buffer[written] = platform.separator(); written += 1; } @@ -648,22 +685,12 @@ pub fn joinStringBuf(buf: []u8, _parts: anytype, comptime _platform: Platform) [ written += part.len; } - // Preserve leading separator - if (_parts[0].len > 0 and _parts[0][0] == _platform.separator()) { - const out = switch (comptime platform) { - // .loose => - // .windows => @compileError("Not implemented yet"), - else => normalizeStringLooseBuf(parser_join_input_buffer[0..written], buf[1..], false, false), - }; - buf[0] = _platform.separator(); - - return buf[0 .. out.len + 1]; - } else { - return switch (platform) { - else => normalizeStringLooseBuf(parser_join_input_buffer[0..written], buf[0..], false, false), - // .windows => @compileError("Not implemented yet"), - }; + if (written == 0) { + buf[0] = '.'; + return buf[0..1]; } + + return normalizeStringNode(parser_join_input_buffer[0..written], buf, platform); } pub fn joinAbsStringBuf(_cwd: []const u8, buf: []u8, _parts: anytype, comptime _platform: Platform) []const u8 { @@ -791,7 +818,12 @@ pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { return std.mem.lastIndexOfAny(u8, slice, "/\\"); } -pub fn normalizeStringLooseBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime preserve_trailing_slash: bool) []u8 { +pub fn normalizeStringLooseBuf( + str: []const u8, + buf: []u8, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []u8 { return normalizeStringGeneric( str, buf, @@ -803,6 +835,70 @@ pub fn normalizeStringLooseBuf(str: []const u8, buf: []u8, comptime allow_above_ ); } +pub fn normalizeStringNode( + str: []const u8, + buf: []u8, + comptime platform: Platform, +) []u8 { + if (str.len == 0) { + buf[0] = '.'; + return buf[0..1]; + } + + const is_absolute = platform.isSeparator(str[0]); + const trailing_separator = platform.isSeparator(str[str.len - 1]); + var buf_ = buf[1..]; + + var out = if (!is_absolute) normalizeStringGeneric( + str, + buf_, + true, + comptime platform.resolve().separator(), + comptime platform.getSeparatorFunc(), + comptime platform.getLastSeparatorFunc(), + false, + ) else normalizeStringGeneric( + str, + buf_, + false, + comptime platform.resolve().separator(), + comptime platform.getSeparatorFunc(), + comptime platform.getLastSeparatorFunc(), + false, + ); + + if (out.len == 0) { + if (is_absolute) { + buf[0] = '/'; + return buf[0..1]; + } + + if (trailing_separator) { + buf[0..2].* = "./".*; + return buf[0..2]; + } + + buf[0] = '.'; + return buf[0..1]; + } + + if (trailing_separator) { + if (!platform.isSeparator(out[out.len - 1])) { + buf_[out.len] = platform.separator(); + out = buf_[0 .. out.len + 1]; + } + } + + if (is_absolute) { + std.debug.assert(!platform.isSeparator(out[0])); + + buf[0] = platform.separator(); + out = buf[0 .. out.len + 1]; + } + + return out; +} + test "joinAbsStringPosix" { var t = tester.Tester.t(default_allocator); defer t.report(@src()); @@ -984,7 +1080,29 @@ test "relative" { var t = tester.Tester.t(default_allocator); defer t.report(@src()); - _ = t.expect("var/foo", try relativeAlloc(default_allocator, "/", "/var/foo/"), @src()); + const fixtures = .{ + .{ "/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" }, + .{ "/page1/page2/foo", "/", "../../.." }, + }; + + inline for (fixtures) |fixture| { + const from = fixture[0]; + const to = fixture[1]; + const expected = fixture[2]; + _ = t.expect(expected, try relativeAlloc(default_allocator, from, to), @src()); + } + _ = t.expect("index.js", try relativeAlloc(default_allocator, "/app/public/", "/app/public/index.js"), @src()); _ = t.expect("..", try relativeAlloc(default_allocator, "/app/public/index.js", "/app/public/"), @src()); _ = t.expect("../../src/bacon.ts", try relativeAlloc(default_allocator, "/app/public/index.html", "/app/src/bacon.ts"), @src()); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 160242fbb..207b6634d 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1,8 +1,6 @@ const std = @import("std"); const expect = std.testing.expect; -const JavascriptString = @import("ast/base.zig").JavascriptString; - const string = @import("string_types.zig").string; const stringZ = @import("string_types.zig").stringZ; const CodePoint = @import("string_types.zig").CodePoint; diff --git a/src/test/tester.zig b/src/test/tester.zig index 84be7ddbb..2c53acc81 100644 --- a/src/test/tester.zig +++ b/src/test/tester.zig @@ -64,19 +64,13 @@ pub const Tester = struct { stderr.writeAll(RESET) catch unreachable; stderr.writeAll("\n") catch unreachable; } - + const strings = @import("../string_immutable.zig"); pub fn evaluate_outcome(self: *const @This()) Outcome { - if (self.expected.len > self.result.len) { + if (strings.eql(self.expected, self.result)) { + return .pass; + } else { return .fail; } - - for (self.expected) |char, i| { - if (char != self.result[i]) { - return Outcome.fail; - } - } - - return Outcome.pass; } }; |