diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 850e00e574..ff4f6fad8a 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -358,53 +358,111 @@ pub fn parse(text: []const u8) ParseError!Uri { return uri; } -/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. -/// arena owns any memory allocated by this function. -pub fn resolve(Base: Uri, R: Uri, strict: bool, arena: std.mem.Allocator) !Uri { - var T: Uri = undefined; +/// Implementation of RFC 3986, Section 5.2.4. Removes dot segments from a URI path. +/// +/// `std.fs.path.resolvePosix` is not sufficient here because it may return relative paths and does not preserve trailing slashes. +fn removeDotSegments(allocator: std.mem.Allocator, paths: []const []const u8) std.mem.Allocator.Error![]const u8 { + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); - if (R.scheme.len > 0 and !((!strict) and (std.mem.eql(u8, R.scheme, Base.scheme)))) { - T.scheme = R.scheme; - T.user = R.user; - T.host = R.host; - T.port = R.port; - T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path }); - T.query = R.query; - } else { - if (R.host) |host| { - T.user = R.user; - T.host = host; - T.port = R.port; - T.path = R.path; - T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path }); - T.query = R.query; - } else { - if (R.path.len == 0) { - T.path = Base.path; - if (R.query) |query| { - T.query = query; - } else { - T.query = Base.query; + for (paths) |p| { + var it = std.mem.tokenizeScalar(u8, p, '/'); + while (it.next()) |component| { + if (std.mem.eql(u8, component, ".")) { + continue; + } else if (std.mem.eql(u8, component, "..")) { + if (result.items.len == 0) + continue; + + while (true) { + const ends_with_slash = result.items[result.items.len - 1] == '/'; + result.items.len -= 1; + if (ends_with_slash or result.items.len == 0) break; } } else { - if (R.path[0] == '/') { - T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path }); - } else { - T.path = try std.fs.path.resolvePosix(arena, &.{ "/", Base.path, R.path }); - } - T.query = R.query; + try result.ensureUnusedCapacity(1 + component.len); + result.appendAssumeCapacity('/'); + result.appendSliceAssumeCapacity(component); } - - T.user = Base.user; - T.host = Base.host; - T.port = Base.port; } - T.scheme = Base.scheme; } - T.fragment = R.fragment; + // ensure a trailing slash is kept + const last_path = paths[paths.len - 1]; + if (last_path.len > 0 and last_path[last_path.len - 1] == '/') { + try result.append('/'); + } - return T; + return result.toOwnedSlice(); +} + +/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. +/// +/// Assumes `arena` owns all memory in `base` and `ref`. `arena` will own all memory in the returned URI. +pub fn resolve(base: Uri, ref: Uri, strict: bool, arena: std.mem.Allocator) std.mem.Allocator.Error!Uri { + var target: Uri = Uri{ + .scheme = "", + .user = null, + .password = null, + .host = null, + .port = null, + .path = "", + .query = null, + .fragment = null, + }; + + if (ref.scheme.len > 0 and (strict or !std.mem.eql(u8, ref.scheme, base.scheme))) { + target.scheme = ref.scheme; + target.user = ref.user; + target.host = ref.host; + target.port = ref.port; + target.path = try removeDotSegments(arena, &.{ref.path}); + target.query = ref.query; + } else { + target.scheme = base.scheme; + if (ref.host) |host| { + target.user = ref.user; + target.host = host; + target.port = ref.port; + target.path = ref.path; + target.path = try removeDotSegments(arena, &.{ref.path}); + target.query = ref.query; + } else { + if (ref.path.len == 0) { + target.path = base.path; + target.query = ref.query orelse base.query; + } else { + if (ref.path[0] == '/') { + target.path = try removeDotSegments(arena, &.{ref.path}); + } else { + target.path = try removeDotSegments(arena, &.{ std.fs.path.dirnamePosix(base.path) orelse "", ref.path }); + } + target.query = ref.query; + } + + target.user = base.user; + target.host = base.host; + target.port = base.port; + } + } + + target.fragment = ref.fragment; + + return target; +} + +test resolve { + const base = try parse("http://a/b/c/d;p?q"); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + try std.testing.expectEqualDeep(try parse("http://a/b/c/blog/"), try base.resolve(try parseWithoutScheme("blog/"), true, arena.allocator())); + try std.testing.expectEqualDeep(try parse("http://a/b/c/blog/?k"), try base.resolve(try parseWithoutScheme("blog/?k"), true, arena.allocator())); + try std.testing.expectEqualDeep(try parse("http://a/b/blog/"), try base.resolve(try parseWithoutScheme("../blog/"), true, arena.allocator())); + try std.testing.expectEqualDeep(try parse("http://a/b/blog"), try base.resolve(try parseWithoutScheme("../blog"), true, arena.allocator())); + try std.testing.expectEqualDeep(try parse("http://e"), try base.resolve(try parseWithoutScheme("//e"), true, arena.allocator())); + try std.testing.expectEqualDeep(try parse("https://a:1/"), try base.resolve(try parse("https://a:1/"), true, arena.allocator())); } const SliceReader = struct {