| ... | @@ -342,7 +342,7 @@ pub fn format( | ... | @@ -342,7 +342,7 @@ pub fn format( |
| 342 | /// The return value will contain unescaped strings pointing into the | 342 | /// The return value will contain unescaped strings pointing into the |
| 343 | /// original `text`. Each component that is provided, will be non-`null`. | 343 | /// original `text`. Each component that is provided, will be non-`null`. |
| 344 | pub fn parse(text: []const u8) ParseError!Uri { | 344 | pub fn parse(text: []const u8) ParseError!Uri { |
| 345 | var reader = SliceReader{ .slice = text }; | 345 | var reader: SliceReader = .{ .slice = text }; |
| 346 | const scheme = reader.readWhile(isSchemeChar); | 346 | const scheme = reader.readWhile(isSchemeChar); |
| 347 | | 347 | |
| 348 | // after the scheme, a ':' must appear | 348 | // after the scheme, a ':' must appear |
| ... | @@ -359,111 +359,145 @@ pub fn parse(text: []const u8) ParseError!Uri { | ... | @@ -359,111 +359,145 @@ pub fn parse(text: []const u8) ParseError!Uri { |
| 359 | return uri; | 359 | return uri; |
| 360 | } | 360 | } |
| 361 | | 361 | |
| 362 | /// Implementation of RFC 3986, Section 5.2.4. Removes dot segments from a URI path. | 362 | pub const ResolveInplaceError = ParseError || error{OutOfMemory}; |
| 363 | /// | | |
| 364 | /// `std.fs.path.resolvePosix` is not sufficient here because it may return relative paths and does not preserve trailing slashes. | | |
| 365 | fn removeDotSegments(allocator: Allocator, paths: []const []const u8) Allocator.Error![]const u8 { | | |
| 366 | var result = std.ArrayList(u8).init(allocator); | | |
| 367 | defer result.deinit(); | | |
| 368 | | | |
| 369 | for (paths) |p| { | | |
| 370 | var it = std.mem.tokenizeScalar(u8, p, '/'); | | |
| 371 | while (it.next()) |component| { | | |
| 372 | if (std.mem.eql(u8, component, ".")) { | | |
| 373 | continue; | | |
| 374 | } else if (std.mem.eql(u8, component, "..")) { | | |
| 375 | if (result.items.len == 0) | | |
| 376 | continue; | | |
| 377 | | 363 | |
| 378 | while (true) { | 364 | /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. |
| 379 | const ends_with_slash = result.items[result.items.len - 1] == '/'; | 365 | /// Copies `new` to the beginning of `aux_buf`, allowing the slices to overlap, |
| 380 | result.items.len -= 1; | 366 | /// then parses `new` as a URI, and then resolves the path in place. |
| 381 | if (ends_with_slash or result.items.len == 0) break; | 367 | /// If a merge needs to take place, the newly constructed path will be stored |
| 382 | } | 368 | /// in `aux_buf` just after the copied `new`. |
| 383 | } else { | 369 | pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplaceError!Uri { |
| 384 | try result.ensureUnusedCapacity(1 + component.len); | 370 | std.mem.copyBackwards(u8, aux_buf, new); |
| 385 | result.appendAssumeCapacity('/'); | 371 | // At this point, new is an invalid pointer. |
| 386 | result.appendSliceAssumeCapacity(component); | 372 | const new_mut = aux_buf[0..new.len]; |
| 387 | } | 373 | |
| 388 | } | 374 | const new_parsed, const has_scheme = p: { |
| 389 | } | 375 | break :p .{ |
| | 376 | parse(new_mut) catch |first_err| { |
| | 377 | break :p .{ |
| | 378 | parseWithoutScheme(new_mut) catch return first_err, |
| | 379 | false, |
| | 380 | }; |
| | 381 | }, |
| | 382 | true, |
| | 383 | }; |
| | 384 | }; |
| 390 | | 385 | |
| 391 | // ensure a trailing slash is kept | 386 | // As you can see above, `new_mut` is not a const pointer. |
| 392 | const last_path = paths[paths.len - 1]; | 387 | const new_path: []u8 = @constCast(new_parsed.path); |
| 393 | if (last_path.len > 0 and last_path[last_path.len - 1] == '/') { | 388 | |
| 394 | try result.append('/'); | 389 | if (has_scheme) return .{ |
| 395 | } | 390 | .scheme = new_parsed.scheme, |
| | 391 | .user = new_parsed.user, |
| | 392 | .host = new_parsed.host, |
| | 393 | .port = new_parsed.port, |
| | 394 | .path = remove_dot_segments(new_path), |
| | 395 | .query = new_parsed.query, |
| | 396 | .fragment = new_parsed.fragment, |
| | 397 | }; |
| 396 | | 398 | |
| 397 | return result.toOwnedSlice(); | 399 | if (new_parsed.host) |host| return .{ |
| 398 | } | 400 | .scheme = base.scheme, |
| | 401 | .user = new_parsed.user, |
| | 402 | .host = host, |
| | 403 | .port = new_parsed.port, |
| | 404 | .path = remove_dot_segments(new_path), |
| | 405 | .query = new_parsed.query, |
| | 406 | .fragment = new_parsed.fragment, |
| | 407 | }; |
| 399 | | 408 | |
| 400 | /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. | 409 | const path, const query = b: { |
| 401 | /// | 410 | if (new_path.len == 0) |
| 402 | /// Assumes `arena` owns all memory in `base` and `ref`. `arena` will own all memory in the returned URI. | 411 | break :b .{ |
| 403 | pub fn resolve(base: Uri, ref: Uri, strict: bool, arena: Allocator) Allocator.Error!Uri { | 412 | base.path, |
| 404 | var target: Uri = Uri{ | 413 | new_parsed.query orelse base.query, |
| 405 | .scheme = "", | 414 | }; |
| 406 | .user = null, | 415 | |
| 407 | .password = null, | 416 | if (new_path[0] == '/') |
| 408 | .host = null, | 417 | break :b .{ |
| 409 | .port = null, | 418 | remove_dot_segments(new_path), |
| 410 | .path = "", | 419 | new_parsed.query, |
| 411 | .query = null, | 420 | }; |
| 412 | .fragment = null, | 421 | |
| | 422 | break :b .{ |
| | 423 | try merge_paths(base.path, new_path, aux_buf[new_mut.len..]), |
| | 424 | new_parsed.query, |
| | 425 | }; |
| 413 | }; | 426 | }; |
| 414 | | 427 | |
| 415 | if (ref.scheme.len > 0 and (strict or !std.mem.eql(u8, ref.scheme, base.scheme))) { | 428 | return .{ |
| 416 | target.scheme = ref.scheme; | 429 | .scheme = base.scheme, |
| 417 | target.user = ref.user; | 430 | .user = base.user, |
| 418 | target.host = ref.host; | 431 | .host = base.host, |
| 419 | target.port = ref.port; | 432 | .port = base.port, |
| 420 | target.path = try removeDotSegments(arena, &.{ref.path}); | 433 | .path = path, |
| 421 | target.query = ref.query; | 434 | .query = query, |
| 422 | } else { | 435 | .fragment = new_parsed.fragment, |
| 423 | target.scheme = base.scheme; | 436 | }; |
| 424 | if (ref.host) |host| { | 437 | } |
| 425 | target.user = ref.user; | 438 | |
| 426 | target.host = host; | 439 | /// In-place implementation of RFC 3986, Section 5.2.4. |
| 427 | target.port = ref.port; | 440 | fn remove_dot_segments(path: []u8) []u8 { |
| 428 | target.path = ref.path; | 441 | var in_i: usize = 0; |
| 429 | target.path = try removeDotSegments(arena, &.{ref.path}); | 442 | var out_i: usize = 0; |
| 430 | target.query = ref.query; | 443 | while (in_i < path.len) { |
| | 444 | if (std.mem.startsWith(u8, path[in_i..], "./")) { |
| | 445 | in_i += 2; |
| | 446 | } else if (std.mem.startsWith(u8, path[in_i..], "../")) { |
| | 447 | in_i += 3; |
| | 448 | } else if (std.mem.startsWith(u8, path[in_i..], "/./")) { |
| | 449 | in_i += 2; |
| | 450 | } else if (std.mem.eql(u8, path[in_i..], "/.")) { |
| | 451 | in_i += 1; |
| | 452 | path[in_i] = '/'; |
| | 453 | } else if (std.mem.startsWith(u8, path[in_i..], "/../")) { |
| | 454 | in_i += 3; |
| | 455 | while (out_i > 0) { |
| | 456 | out_i -= 1; |
| | 457 | if (path[out_i] == '/') break; |
| | 458 | } |
| | 459 | } else if (std.mem.eql(u8, path[in_i..], "/..")) { |
| | 460 | in_i += 2; |
| | 461 | path[in_i] = '/'; |
| | 462 | while (out_i > 0) { |
| | 463 | out_i -= 1; |
| | 464 | if (path[out_i] == '/') break; |
| | 465 | } |
| | 466 | } else if (std.mem.eql(u8, path[in_i..], ".")) { |
| | 467 | in_i += 1; |
| | 468 | } else if (std.mem.eql(u8, path[in_i..], "..")) { |
| | 469 | in_i += 2; |
| 431 | } else { | 470 | } else { |
| 432 | if (ref.path.len == 0) { | 471 | while (true) { |
| 433 | target.path = base.path; | 472 | path[out_i] = path[in_i]; |
| 434 | target.query = ref.query orelse base.query; | 473 | out_i += 1; |
| 435 | } else { | 474 | in_i += 1; |
| 436 | if (ref.path[0] == '/') { | 475 | if (in_i >= path.len or path[in_i] == '/') break; |
| 437 | target.path = try removeDotSegments(arena, &.{ref.path}); | | |
| 438 | } else { | | |
| 439 | target.path = try removeDotSegments(arena, &.{ std.fs.path.dirnamePosix(base.path) orelse "", ref.path }); | | |
| 440 | } | | |
| 441 | target.query = ref.query; | | |
| 442 | } | 476 | } |
| 443 | | | |
| 444 | target.user = base.user; | | |
| 445 | target.host = base.host; | | |
| 446 | target.port = base.port; | | |
| 447 | } | 477 | } |
| 448 | } | 478 | } |
| 449 | | 479 | return path[0..out_i]; |
| 450 | target.fragment = ref.fragment; | | |
| 451 | | | |
| 452 | return target; | | |
| 453 | } | 480 | } |
| 454 | | 481 | |
| 455 | test resolve { | 482 | test remove_dot_segments { |
| 456 | const base = try parse("http://a/b/c/d;p?q"); | 483 | { |
| 457 | | 484 | var buffer = "/a/b/c/./../../g".*; |
| 458 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 485 | try std.testing.expectEqualStrings("/a/g", remove_dot_segments(&buffer)); |
| 459 | defer arena.deinit(); | 486 | } |
| | 487 | } |
| 460 | | 488 | |
| 461 | try std.testing.expectEqualDeep(try parse("http://a/b/c/blog/"), try base.resolve(try parseWithoutScheme("blog/"), true, arena.allocator())); | 489 | /// 5.2.3. Merge Paths |
| 462 | try std.testing.expectEqualDeep(try parse("http://a/b/c/blog/?k"), try base.resolve(try parseWithoutScheme("blog/?k"), true, arena.allocator())); | 490 | fn merge_paths(base: []const u8, new: []u8, aux: []u8) error{OutOfMemory}![]u8 { |
| 463 | try std.testing.expectEqualDeep(try parse("http://a/b/blog/"), try base.resolve(try parseWithoutScheme("../blog/"), true, arena.allocator())); | 491 | if (aux.len < base.len + 1 + new.len) return error.OutOfMemory; |
| 464 | try std.testing.expectEqualDeep(try parse("http://a/b/blog"), try base.resolve(try parseWithoutScheme("../blog"), true, arena.allocator())); | 492 | if (base.len == 0) { |
| 465 | try std.testing.expectEqualDeep(try parse("http://e"), try base.resolve(try parseWithoutScheme("//e"), true, arena.allocator())); | 493 | aux[0] = '/'; |
| 466 | try std.testing.expectEqualDeep(try parse("https://a:1/"), try base.resolve(try parse("https://a:1/"), true, arena.allocator())); | 494 | @memcpy(aux[1..][0..new.len], new); |
| | 495 | return remove_dot_segments(aux[0 .. new.len + 1]); |
| | 496 | } |
| | 497 | const pos = std.mem.lastIndexOfScalar(u8, base, '/') orelse return remove_dot_segments(new); |
| | 498 | @memcpy(aux[0 .. pos + 1], base[0 .. pos + 1]); |
| | 499 | @memcpy(aux[pos + 1 ..][0..new.len], new); |
| | 500 | return remove_dot_segments(aux[0 .. pos + 1 + new.len]); |
| 467 | } | 501 | } |
| 468 | | 502 | |
| 469 | const SliceReader = struct { | 503 | const SliceReader = struct { |