diff options
author | 2021-05-16 23:25:12 -0700 | |
---|---|---|
committer | 2021-05-16 23:25:12 -0700 | |
commit | 154e049638753abc10ed0eca2012685fe3b831be (patch) | |
tree | bdeb6b0bf8137ee36df0aab436ac50713ddeb5ef /src/resolver/resolve_path.zig | |
parent | e80f865974df7aae5e2f6abb966b36497da693c6 (diff) | |
download | bun-154e049638753abc10ed0eca2012685fe3b831be.tar.gz bun-154e049638753abc10ed0eca2012685fe3b831be.tar.zst bun-154e049638753abc10ed0eca2012685fe3b831be.zip |
lots
Former-commit-id: 9ccb4dd082afbc4f94982bf092360487232d8b60
Diffstat (limited to 'src/resolver/resolve_path.zig')
-rw-r--r-- | src/resolver/resolve_path.zig | 521 |
1 files changed, 468 insertions, 53 deletions
diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index f639bff1b..9cb3e635c 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1,83 +1,498 @@ -// https://github.com/MasterQ32/ftz/blob/3183b582211f8e38c1c3363c56753026ca45c11f/src/main.zig#L431-L509 -// Thanks, Felix! We should get this into std perhaps. +const tester = @import("../test/tester.zig"); const std = @import("std"); -/// Resolves a unix-like path and removes all "." and ".." from it. Will not escape the root and can be used to sanitize inputs. -pub fn resolvePath(buffer: []u8, src_path: []const u8) ?[]u8 { - var end: usize = 0; - buffer[0] = '.'; +threadlocal var parser_join_input_buffer: [1024]u8 = undefined; +threadlocal var parser_buffer: [1024]u8 = undefined; - var iter = std.mem.tokenize(src_path, "/"); - while (iter.next()) |segment| { - if (end >= buffer.len) break; +// This function is based on Node.js' path.normalize function. +// https://github.com/nodejs/node/blob/36bb31be5f0b85a0f6cbcb36b64feb3a12c60984/lib/path.js#L66 +pub fn normalizeStringGeneric(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime separator: u8, comptime isPathSeparator: anytype, lastIndexOfSeparator: anytype) []u8 { + var i: usize = 0; + var last_segment_length: i32 = 0; + var last_slash: i32 = -1; + var dots: i32 = 0; + var code: u8 = 0; - if (std.mem.eql(u8, segment, ".")) { - continue; - } else if (std.mem.eql(u8, segment, "..")) { - while (true) { - if (end == 0) - break; - if (buffer[end] == '/') { - break; + var written_len: usize = 0; + const stop_len = str.len; + + while (i <= stop_len) : (i += 1) { + if (i < stop_len) { + code = str[i]; + } else if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) { + break; + } else { + code = separator; + } + + if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) { + 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) { + if (lastIndexOfSeparator(buf[0..written_len])) |last_slash_index| { + written_len = last_slash_index; + last_segment_length = @intCast(i32, written_len - 1 - (lastIndexOfSeparator(buf[0..written_len]) orelse 0)); + } else { + written_len = 0; + } + last_slash = @intCast(i32, i); + dots = 0; + continue; + } else if (written_len != 0) { + written_len = 0; + last_segment_length = 0; + last_slash = @intCast(i32, i); + dots = 0; + continue; + } + + if (allow_above_root) { + if (written_len > 0) { + buf[written_len] = separator; + written_len += 1; + } + + buf[written_len] = '.'; + written_len += 1; + buf[written_len] = '.'; + written_len += 1; + + last_segment_length = 2; + } + } + } else { + if (written_len > 0) { + buf[written_len] = separator; + written_len += 1; } - end -= 1; + + const slice = str[@intCast(usize, @intCast(usize, last_slash + 1))..i]; + std.mem.copy(u8, buf[written_len .. written_len + slice.len], slice); + written_len += slice.len; + last_segment_length = @intCast(i32, i) - last_slash - 1; } + + last_slash = @intCast(i32, i); + dots = 0; + } else if (code == '.' and dots != -1) { + dots += 1; } else { - if (end + segment.len + 1 > buffer.len) + dots = -1; + } + } + + return buf[0..written_len]; +} + +pub const Platform = enum { + auto, + loose, + windows, + posix, + + pub fn isSeparator(comptime _platform: Platform, char: u8) bool { + const platform = _platform.resolve(); + switch (platform) { + .auto => unreachable, + .loose => { + return isSepAny(char); + }, + .windows => { + return isSepWin32(char); + }, + .posix => { + return isSepPosix(char); + }, + } + } + + pub fn leadingSeparatorIndex(comptime _platform: Platform, path: anytype) ?usize { + switch (_platform.resolve()) { + .windows => { + if (path.len < 1) + return null; + + if (path[0] == '/') + return 0; + + if (path[0] == '\\') + return 0; + + if (path.len < 3) + return null; + + // C:\ + // C:/ + if (path[0] >= 'A' and path[0] <= 'Z' and path[1] == ':') { + if (path[2] == '/') + return 2; + if (path[2] == '\\') + return 2; + } + return null; + }, + .posix => { + if (path.len > 0 and path[0] == '/') { + return 0; + } else { + return null; + } + }, + else => { + return leadingSeparatorIndex(.windows, path) orelse leadingSeparatorIndex(.posix, path); + }, + } + } + + pub fn resolve(comptime _platform: Platform) Platform { + if (_platform == .auto) { + switch (std.Target.current.os.tag) { + .windows => { + return .windows; + }, + + .freestanding, .emscripten, .other => { + return .loose; + }, - const start = end; - buffer[end] = '/'; - end += segment.len + 1; - std.mem.copy(u8, buffer[start + 1 .. end], segment); + else => { + return .posix; + }, + } } + + return _platform; + } +}; + +pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 { + return normalizeStringBuf(str, &parser_buffer, allow_above_root, _platform); +} + +pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 { + comptime const platform = _platform.resolve(); + + switch (platform) { + .auto => unreachable, + + .windows => { + return normalizeStringWindowsBuf(str, buf, allow_above_root); + }, + .posix => { + return normalizeStringPosixBuf(str, buf, allow_above_root); + }, + + .loose => { + return normalizeStringLooseBuf(str, buf, allow_above_root); + }, + } +} + +pub fn normalizeStringAlloc(allocator: *std.mem.Allocator, str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) ![]const u8 { + return try allocator.dupe(u8, normalizeString(str, allow_above_root, _platform)); +} + +pub fn normalizeAndJoin2(_cwd: []const u8, comptime _platform: Platform, part: anytype, part2: anytype) []const u8 { + const parts = [_][]const u8{ part, part2 }; + const slice = normalizeAndJoinString(_cwd, &parts, _platform); + return slice; +} + +pub fn normalizeAndJoin(_cwd: []const u8, comptime _platform: Platform, part: anytype) []const u8 { + const parts = [_][]const u8{ + part, + }; + const slice = normalizeAndJoinString(_cwd, &parts, _platform); + return slice; +} + +// Convert parts of potentially invalid file paths into a single valid filpeath +// without querying the filesystem +// This is the equivalent of +pub fn normalizeAndJoinString(_cwd: []const u8, parts: anytype, comptime _platform: Platform) []const u8 { + return normalizeAndJoinStringBuf(_cwd, &parser_join_input_buffer, parts, _platform); +} + +pub fn normalizeAndJoinStringBuf(_cwd: []const u8, buf: []u8, parts: anytype, comptime _platform: Platform) []const u8 { + if (parts.len == 0) { + return _cwd; + } + + if ((_platform == .loose or _platform == .posix) and parts.len == 1 and parts[0].len == 1 and parts[0] == std.fs.path.sep_posix) { + return "/"; + } + + var cwd = _cwd; + var out: usize = 0; + // When parts[0] is absolute, we treat that as, effectively, the cwd + var ignore_cwd = cwd.len == 0; + + // Windows leading separators can be a lot of things... + // So we need to do this instead of just checking the first char. + var leading_separator: []const u8 = ""; + if (_platform.leadingSeparatorIndex(parts[0])) |leading_separator_i| { + leading_separator = parts[0][0 .. leading_separator_i + 1]; + ignore_cwd = true; } - const result = if (end == 0) - buffer[0 .. end + 1] - else - buffer[0..end]; + if (!ignore_cwd) { + leading_separator = cwd[0 .. 1 + (_platform.leadingSeparatorIndex(_cwd) orelse unreachable)]; // cwd must be absolute + cwd = _cwd[leading_separator.len..cwd.len]; + out = cwd.len; + std.debug.assert(out < buf.len); + std.mem.copy(u8, buf[0..out], cwd); + } + + for (parts) |part, i| { + // This never returns leading separators. + var normalized_part = normalizeString(part, true, _platform); + if (normalized_part.len == 0) { + continue; + } + switch (_platform.resolve()) { + .windows => { + buf[out] = std.fs.path.sep_windows; + }, + else => { + buf[out] = std.fs.path.sep_posix; + }, + } + + out += 1; - if (std.mem.eql(u8, result, src_path)) { - return null; + const start = out; + out += normalized_part.len; + std.debug.assert(out < buf.len); + std.mem.copy(u8, buf[start..out], normalized_part); } - return result; + // One last normalization, to remove any ../ added + const result = normalizeStringBuf(buf[0..out], parser_buffer[leading_separator.len..parser_buffer.len], false, _platform); + std.mem.copy(u8, buf[0..leading_separator.len], leading_separator); + std.mem.copy(u8, buf[leading_separator.len .. result.len + leading_separator.len], result); + + return buf[0 .. result.len + leading_separator.len]; +} + +pub fn isSepPosix(char: u8) bool { + return char == std.fs.path.sep_posix; +} + +pub fn isSepWin32(char: u8) bool { + return char == std.fs.path.sep_windows; +} + +pub fn isSepAny(char: u8) bool { + return @call(.{ .modifier = .always_inline }, isSepPosix, .{char}) or @call(.{ .modifier = .always_inline }, isSepWin32, .{char}); } -fn testResolve(expected: []const u8, input: []const u8) !void { - var buffer: [1024]u8 = undefined; +pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { + return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_windows); +} - const actual = try resolvePath(&buffer, input); - std.testing.expectEqualStrings(expected, actual); +pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { + return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix); } -test "resolvePath" { - try testResolve("/", ""); - try testResolve("/", "/"); - try testResolve("/", "////////////"); +pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { + return std.mem.lastIndexOfAny(u8, slice, "/\\"); +} - try testResolve("/a", "a"); - try testResolve("/a", "/a"); - try testResolve("/a", "////////////a"); - try testResolve("/a", "////////////a///"); +pub fn normalizeStringPosix(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix); +} - try testResolve("/a/b/c/d", "/a/b/c/d"); +pub fn normalizeStringPosixBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix); +} - try testResolve("/a/b/d", "/a/b/c/../d"); +pub fn normalizeStringWindows(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows); +} + +pub fn normalizeStringWindowsBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows); +} + +pub fn normalizeStringLoose(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose); +} + +pub fn normalizeStringLooseBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose); +} + +test "normalizeAndJoinStringPosix" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); + const string = []const u8; + const cwd = "/Users/jarredsumner/Code/app"; + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .posix), + @src(), + ); +} + +test "normalizeAndJoinStringLoose" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); + const string = []const u8; + const cwd = "/Users/jarredsumner/Code/app"; + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ ".\\.\\.\\.\\foo", "././././bar././././", "..\\file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); +} - try testResolve("/", ".."); - try testResolve("/", "/.."); - try testResolve("/", "/../../../.."); - try testResolve("/a/b/c", "a/b/c/"); +test "normalizeStringPosix" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); - try testResolve("/new/date.txt", "/new/../../new/date.txt"); + // Don't mess up strings that + _ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", true, .posix), @src()); + _ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", true, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/././foo/././././././bar/../bar/../bar", true, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar//////", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar//////", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar", false, .posix), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "/////", false, .posix), @src()); + _ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "../boom/../", true, .posix), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "./", true, .posix), @src()); } -test "resolvePath overflow" { - var buf: [1]u8 = undefined; +test "normalizeStringWindows" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); - std.testing.expectEqualStrings("/", try resolvePath(&buf, "/")); - std.testing.expectError(error.BufferTooSmall, resolvePath(&buf, "a")); // will resolve to "/a" + // Don't mess up strings that + _ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", true, .windows), @src()); + _ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", true, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\.\\.\\foo\\.\\.\\.\\.\\.\\.\\bar\\..\\bar\\..\\bar", true, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar\\\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar\\\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar", false, .windows), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "..\\boom\\..\\", true, .windows), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, ".\\", true, .windows), @src()); } |