authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2024-04-09 22:22:08-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-04-10 02:11:54-07:00
logc4587dc9f46e15d4fb875a7675bc1aa22138c1ab
treefff3520b8a9ba7492b7ac1d35a5290c535c50ba5
parent215de3ee67f75e2405c177b262cb5c1cd8c8e343

Uri: propagate per-component encoding

This allows `std.Uri.resolve_inplace` to properly preserve the fact that `new` is already escaped but `base` may not be. I originally tried just moving `raw_uri` around, but it made uri resolution unmanagably complicated, so I instead added per-component information to `Uri` which allows extra allocations to be avoided when constructing uris with components from different sources, and in some cases, deferring the work all the way to when the uri is printed, where an allocator may not even be needed. Closes #19587

7 files changed, 520 insertions(+), 510 deletions(-)

lib/std/Uri.zig+309-334
...@@ -1,156 +1,157 @@...@@ -1,156 +1,157 @@
1//! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>.1//! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>.
2//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild.2//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild.
33
4const Uri = @This();
5const std = @import("std.zig");
6const testing = std.testing;
7const Allocator = std.mem.Allocator;
8
9scheme: []const u8,4scheme: []const u8,
10user: ?[]const u8 = null,5user: ?Component = null,
11password: ?[]const u8 = null,6password: ?Component = null,
12host: ?[]const u8 = null,7host: ?Component = null,
13port: ?u16 = null,8port: ?u16 = null,
14path: []const u8,9path: Component = Component.empty,
15query: ?[]const u8 = null,10query: ?Component = null,
16fragment: ?[]const u8 = null,11fragment: ?Component = null,
1712
18/// Applies URI encoding and replaces all reserved characters with their respective %XX code.13pub const Component = union(enum) {
19pub fn escapeString(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {14 /// Invalid characters in this component must be percent encoded
20 return escapeStringWithFn(allocator, input, isUnreserved);15 /// before being printed as part of a URI.
21}16 raw: []const u8,
2217 /// This component is already percent-encoded, it can be printed
23pub fn escapePath(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {18 /// directly as part of a URI.
24 return escapeStringWithFn(allocator, input, isPathChar);19 percent_encoded: []const u8,
25}20
2621 pub const empty: Component = .{ .percent_encoded = "" };
27pub fn escapeQuery(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {22
28 return escapeStringWithFn(allocator, input, isQueryChar);23 pub fn isEmpty(component: Component) bool {
29}24 return switch (component) {
3025 .raw, .percent_encoded => |string| string.len == 0,
31pub fn writeEscapedString(writer: anytype, input: []const u8) !void {26 };
32 return writeEscapedStringWithFn(writer, input, isUnreserved);
33}
34
35pub fn writeEscapedPath(writer: anytype, input: []const u8) !void {
36 return writeEscapedStringWithFn(writer, input, isPathChar);
37}
38
39pub fn writeEscapedQuery(writer: anytype, input: []const u8) !void {
40 return writeEscapedStringWithFn(writer, input, isQueryChar);
41}
42
43pub fn escapeStringWithFn(allocator: Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) Allocator.Error![]u8 {
44 var outsize: usize = 0;
45 for (input) |c| {
46 outsize += if (keepUnescaped(c)) @as(usize, 1) else 3;
47 }27 }
48 var output = try allocator.alloc(u8, outsize);
49 var outptr: usize = 0;
5028
51 for (input) |c| {29 /// Allocates the result with `arena` only if needed, so the result should not be freed.
52 if (keepUnescaped(c)) {30 pub fn toRawMaybeAlloc(
53 output[outptr] = c;31 component: Component,
54 outptr += 1;32 arena: std.mem.Allocator,
55 } else {33 ) std.mem.Allocator.Error![]const u8 {
56 var buf: [2]u8 = undefined;34 return switch (component) {
57 _ = std.fmt.bufPrint(&buf, "{X:0>2}", .{c}) catch unreachable;35 .raw => |raw| raw,
5836 .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_|
59 output[outptr + 0] = '%';37 try std.fmt.allocPrint(arena, "{raw}", .{component})
60 output[outptr + 1] = buf[0];38 else
61 output[outptr + 2] = buf[1];39 percent_encoded,
62 outptr += 3;40 };
63 }
64 }41 }
65 return output;
66}
6742
68pub fn writeEscapedStringWithFn(writer: anytype, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) @TypeOf(writer).Error!void {43 pub fn format(
69 for (input) |c| {44 component: Component,
70 if (keepUnescaped(c)) {45 comptime fmt_str: []const u8,
71 try writer.writeByte(c);46 _: std.fmt.FormatOptions,
72 } else {47 writer: anytype,
73 try writer.print("%{X:0>2}", .{c});48 ) @TypeOf(writer).Error!void {
74 }49 if (fmt_str.len == 0) {
50 try writer.print("std.Uri.Component{{ .{s} = \"{}\" }}", .{
51 @tagName(component),
52 std.zig.fmtEscapes(switch (component) {
53 .raw, .percent_encoded => |string| string,
54 }),
55 });
56 } else if (comptime std.mem.eql(u8, fmt_str, "raw")) switch (component) {
57 .raw => |raw| try writer.writeAll(raw),
58 .percent_encoded => |percent_encoded| {
59 var start: usize = 0;
60 var index: usize = 0;
61 while (std.mem.indexOfScalarPos(u8, percent_encoded, index, '%')) |percent| {
62 index = percent + 1;
63 if (percent_encoded.len - index < 2) continue;
64 const percent_encoded_char =
65 std.fmt.parseInt(u8, percent_encoded[index..][0..2], 16) catch continue;
66 try writer.print("{s}{c}", .{
67 percent_encoded[start..percent],
68 percent_encoded_char,
69 });
70 start = percent + 3;
71 index = percent + 3;
72 }
73 try writer.writeAll(percent_encoded[start..]);
74 },
75 } else if (comptime std.mem.eql(u8, fmt_str, "%")) switch (component) {
76 .raw => |raw| try percentEncode(writer, raw, isUnreserved),
77 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
78 } else if (comptime std.mem.eql(u8, fmt_str, "user")) switch (component) {
79 .raw => |raw| try percentEncode(writer, raw, isUserChar),
80 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
81 } else if (comptime std.mem.eql(u8, fmt_str, "password")) switch (component) {
82 .raw => |raw| try percentEncode(writer, raw, isPasswordChar),
83 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
84 } else if (comptime std.mem.eql(u8, fmt_str, "host")) switch (component) {
85 .raw => |raw| try percentEncode(writer, raw, isHostChar),
86 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
87 } else if (comptime std.mem.eql(u8, fmt_str, "path")) switch (component) {
88 .raw => |raw| try percentEncode(writer, raw, isPathChar),
89 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
90 } else if (comptime std.mem.eql(u8, fmt_str, "query")) switch (component) {
91 .raw => |raw| try percentEncode(writer, raw, isQueryChar),
92 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
93 } else if (comptime std.mem.eql(u8, fmt_str, "fragment")) switch (component) {
94 .raw => |raw| try percentEncode(writer, raw, isFragmentChar),
95 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
96 } else @compileError("invalid format string '" ++ fmt_str ++ "'");
75 }97 }
76}
7798
78/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies99 pub fn percentEncode(
79/// them to the output.100 writer: anytype,
80pub fn unescapeString(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {101 raw: []const u8,
81 var outsize: usize = 0;102 comptime isValidChar: fn (u8) bool,
82 var inptr: usize = 0;103 ) @TypeOf(writer).Error!void {
83 while (inptr < input.len) {104 var start: usize = 0;
84 if (input[inptr] == '%') {105 for (raw, 0..) |char, index| {
85 inptr += 1;106 if (isValidChar(char)) continue;
86 if (inptr + 2 <= input.len) {107 try writer.print("{s}%{X:0>2}", .{ raw[start..index], char });
87 _ = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {108 start = index + 1;
88 outsize += 3;
89 inptr += 2;
90 continue;
91 };
92 inptr += 2;
93 outsize += 1;
94 } else {
95 outsize += 1;
96 }
97 } else {
98 inptr += 1;
99 outsize += 1;
100 }109 }
110 try writer.writeAll(raw[start..]);
101 }111 }
112};
102113
103 var output = try allocator.alloc(u8, outsize);114/// Percent decodes all %XX where XX is a valid hex number.
104 var outptr: usize = 0;115/// `output` may alias `input` if `output.ptr <= input.ptr`.
105 inptr = 0;116/// Mutates and returns a subslice of `output`.
106 while (inptr < input.len) {117pub fn percentDecodeBackwards(output: []u8, input: []const u8) []u8 {
107 if (input[inptr] == '%') {118 var input_index = input.len;
108 inptr += 1;119 var output_index = output.len;
109 if (inptr + 2 <= input.len) {120 while (input_index > 0) {
110 const value = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {121 if (input_index >= 3) {
111 output[outptr + 0] = input[inptr + 0];122 const maybe_percent_encoded = input[input_index - 3 ..][0..3];
112 output[outptr + 1] = input[inptr + 1];123 if (maybe_percent_encoded[0] == '%') {
113 inptr += 2;124 if (std.fmt.parseInt(u8, maybe_percent_encoded[1..], 16)) |percent_encoded_char| {
114 outptr += 2;125 input_index -= maybe_percent_encoded.len;
126 output_index -= 1;
127 output[output_index] = percent_encoded_char;
115 continue;128 continue;
116 };129 } else |_| {}
117
118 output[outptr] = value;
119
120 inptr += 2;
121 outptr += 1;
122 } else {
123 output[outptr] = input[inptr - 1];
124 outptr += 1;
125 }130 }
126 } else {
127 output[outptr] = input[inptr];
128 inptr += 1;
129 outptr += 1;
130 }131 }
132 input_index -= 1;
133 output_index -= 1;
134 output[output_index] = input[input_index];
131 }135 }
132 return output;136 return output[output_index..];
137}
138
139/// Percent decodes all %XX where XX is a valid hex number.
140/// Mutates and returns a subslice of `buffer`.
141pub fn percentDecodeInPlace(buffer: []u8) []u8 {
142 return percentDecodeBackwards(buffer, buffer);
133}143}
134144
135pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };145pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };
136146
137/// Parses the URI or returns an error. This function is not compliant, but is required to parse147/// Parses the URI or returns an error. This function is not compliant, but is required to parse
138/// some forms of URIs in the wild, such as HTTP Location headers.148/// some forms of URIs in the wild, such as HTTP Location headers.
139/// The return value will contain unescaped strings pointing into the149/// The return value will contain strings pointing into the original `text`.
140/// original `text`. Each component that is provided, will be non-`null`.150/// Each component that is provided, will be non-`null`.
141pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {151pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri {
142 var reader = SliceReader{ .slice = text };152 var reader = SliceReader{ .slice = text };
143153
144 var uri = Uri{154 var uri: Uri = .{ .scheme = scheme, .path = undefined };
145 .scheme = "",
146 .user = null,
147 .password = null,
148 .host = null,
149 .port = null,
150 .path = "", // path is always set, but empty by default.
151 .query = null,
152 .fragment = null,
153 };
154155
155 if (reader.peekPrefix("//")) a: { // authority part156 if (reader.peekPrefix("//")) a: { // authority part
156 std.debug.assert(reader.get().? == '/');157 std.debug.assert(reader.get().? == '/');
...@@ -167,12 +168,12 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {...@@ -167,12 +168,12 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
167 const user_info = authority[0..index];168 const user_info = authority[0..index];
168169
169 if (std.mem.indexOf(u8, user_info, ":")) |idx| {170 if (std.mem.indexOf(u8, user_info, ":")) |idx| {
170 uri.user = user_info[0..idx];171 uri.user = .{ .percent_encoded = user_info[0..idx] };
171 if (idx < user_info.len - 1) { // empty password is also "no password"172 if (idx < user_info.len - 1) { // empty password is also "no password"
172 uri.password = user_info[idx + 1 ..];173 uri.password = .{ .percent_encoded = user_info[idx + 1 ..] };
173 }174 }
174 } else {175 } else {
175 uri.user = user_info;176 uri.user = .{ .percent_encoded = user_info };
176 uri.password = null;177 uri.password = null;
177 }178 }
178 }179 }
...@@ -205,19 +206,19 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {...@@ -205,19 +206,19 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
205 }206 }
206207
207 if (start_of_host >= end_of_host) return error.InvalidFormat;208 if (start_of_host >= end_of_host) return error.InvalidFormat;
208 uri.host = authority[start_of_host..end_of_host];209 uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] };
209 }210 }
210211
211 uri.path = reader.readUntil(isPathSeparator);212 uri.path = .{ .percent_encoded = reader.readUntil(isPathSeparator) };
212213
213 if ((reader.peek() orelse 0) == '?') { // query part214 if ((reader.peek() orelse 0) == '?') { // query part
214 std.debug.assert(reader.get().? == '?');215 std.debug.assert(reader.get().? == '?');
215 uri.query = reader.readUntil(isQuerySeparator);216 uri.query = .{ .percent_encoded = reader.readUntil(isQuerySeparator) };
216 }217 }
217218
218 if ((reader.peek() orelse 0) == '#') { // fragment part219 if ((reader.peek() orelse 0) == '#') { // fragment part
219 std.debug.assert(reader.get().? == '#');220 std.debug.assert(reader.get().? == '#');
220 uri.fragment = reader.readUntilEof();221 uri.fragment = .{ .percent_encoded = reader.readUntilEof() };
221 }222 }
222223
223 return uri;224 return uri;
...@@ -241,9 +242,6 @@ pub const WriteToStreamOptions = struct {...@@ -241,9 +242,6 @@ pub const WriteToStreamOptions = struct {
241242
242 /// When true, include the fragment part of the URI. Ignored when `path` is false.243 /// When true, include the fragment part of the URI. Ignored when `path` is false.
243 fragment: bool = false,244 fragment: bool = false,
244
245 /// When true, do not escape any part of the URI.
246 raw: bool = false,
247};245};
248246
249pub fn writeToStream(247pub fn writeToStream(
...@@ -252,80 +250,51 @@ pub fn writeToStream(...@@ -252,80 +250,51 @@ pub fn writeToStream(
252 writer: anytype,250 writer: anytype,
253) @TypeOf(writer).Error!void {251) @TypeOf(writer).Error!void {
254 if (options.scheme) {252 if (options.scheme) {
255 try writer.writeAll(uri.scheme);253 try writer.print("{s}:", .{uri.scheme});
256 try writer.writeAll(":");
257
258 if (options.authority and uri.host != null) {254 if (options.authority and uri.host != null) {
259 try writer.writeAll("//");255 try writer.writeAll("//");
260 }256 }
261 }257 }
262
263 if (options.authority) {258 if (options.authority) {
264 if (options.authentication and uri.host != null) {259 if (options.authentication and uri.host != null) {
265 if (uri.user) |user| {260 if (uri.user) |user| {
266 try writer.writeAll(user);261 try writer.print("{user}", .{user});
267 if (uri.password) |password| {262 if (uri.password) |password| {
268 try writer.writeAll(":");263 try writer.print(":{password}", .{password});
269 try writer.writeAll(password);
270 }264 }
271 try writer.writeAll("@");265 try writer.writeByte('@');
272 }266 }
273 }267 }
274
275 if (uri.host) |host| {268 if (uri.host) |host| {
276 try writer.writeAll(host);269 try writer.print("{host}", .{host});
277270 if (uri.port) |port| try writer.print(":{d}", .{port});
278 if (uri.port) |port| {
279 try writer.writeAll(":");
280 try std.fmt.formatInt(port, 10, .lower, .{}, writer);
281 }
282 }271 }
283 }272 }
284
285 if (options.path) {273 if (options.path) {
286 if (uri.path.len == 0) {274 try writer.print("{path}", .{
287 try writer.writeAll("/");275 if (uri.path.isEmpty()) Uri.Component{ .percent_encoded = "/" } else uri.path,
288 } else if (options.raw) {276 });
289 try writer.writeAll(uri.path);277 if (options.query) {
290 } else {278 if (uri.query) |query| try writer.print("?{query}", .{query});
291 try writeEscapedPath(writer, uri.path);279 }
280 if (options.fragment) {
281 if (uri.fragment) |fragment| try writer.print("#{fragment}", .{fragment});
292 }282 }
293
294 if (options.query) if (uri.query) |q| {
295 try writer.writeAll("?");
296 if (options.raw) {
297 try writer.writeAll(q);
298 } else {
299 try writeEscapedQuery(writer, q);
300 }
301 };
302
303 if (options.fragment) if (uri.fragment) |f| {
304 try writer.writeAll("#");
305 if (options.raw) {
306 try writer.writeAll(f);
307 } else {
308 try writeEscapedQuery(writer, f);
309 }
310 };
311 }283 }
312}284}
313285
314pub fn format(286pub fn format(
315 uri: Uri,287 uri: Uri,
316 comptime fmt: []const u8,288 comptime fmt_str: []const u8,
317 options: std.fmt.FormatOptions,289 _: std.fmt.FormatOptions,
318 writer: anytype,290 writer: anytype,
319) @TypeOf(writer).Error!void {291) @TypeOf(writer).Error!void {
320 _ = options;292 const scheme = comptime std.mem.indexOfScalar(u8, fmt_str, ';') != null or fmt_str.len == 0;
321293 const authentication = comptime std.mem.indexOfScalar(u8, fmt_str, '@') != null or fmt_str.len == 0;
322 const scheme = comptime std.mem.indexOf(u8, fmt, ";") != null or fmt.len == 0;294 const authority = comptime std.mem.indexOfScalar(u8, fmt_str, '+') != null or fmt_str.len == 0;
323 const authentication = comptime std.mem.indexOf(u8, fmt, "@") != null or fmt.len == 0;295 const path = comptime std.mem.indexOfScalar(u8, fmt_str, '/') != null or fmt_str.len == 0;
324 const authority = comptime std.mem.indexOf(u8, fmt, "+") != null or fmt.len == 0;296 const query = comptime std.mem.indexOfScalar(u8, fmt_str, '?') != null or fmt_str.len == 0;
325 const path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;297 const fragment = comptime std.mem.indexOfScalar(u8, fmt_str, '#') != null or fmt_str.len == 0;
326 const query = comptime std.mem.indexOf(u8, fmt, "?") != null or fmt.len == 0;
327 const fragment = comptime std.mem.indexOf(u8, fmt, "#") != null or fmt.len == 0;
328 const raw = comptime std.mem.indexOf(u8, fmt, "r") != null or fmt.len == 0;
329298
330 return writeToStream(uri, .{299 return writeToStream(uri, .{
331 .scheme = scheme,300 .scheme = scheme,
...@@ -334,12 +303,11 @@ pub fn format(...@@ -334,12 +303,11 @@ pub fn format(
334 .path = path,303 .path = path,
335 .query = query,304 .query = query,
336 .fragment = fragment,305 .fragment = fragment,
337 .raw = raw,
338 }, writer);306 }, writer);
339}307}
340308
341/// Parses the URI or returns an error.309/// Parses the URI or returns an error.
342/// The return value will contain unescaped strings pointing into the310/// The return value will contain strings pointing into the
343/// original `text`. Each component that is provided, will be non-`null`.311/// original `text`. Each component that is provided, will be non-`null`.
344pub fn parse(text: []const u8) ParseError!Uri {312pub fn parse(text: []const u8) ParseError!Uri {
345 var reader: SliceReader = .{ .slice = text };313 var reader: SliceReader = .{ .slice = text };
...@@ -353,42 +321,32 @@ pub fn parse(text: []const u8) ParseError!Uri {...@@ -353,42 +321,32 @@ pub fn parse(text: []const u8) ParseError!Uri {
353 return error.InvalidFormat;321 return error.InvalidFormat;
354 }322 }
355323
356 var uri = try parseWithoutScheme(reader.readUntilEof());324 return parseAfterScheme(scheme, reader.readUntilEof());
357 uri.scheme = scheme;
358
359 return uri;
360}325}
361326
362pub const ResolveInplaceError = ParseError || error{OutOfMemory};327pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft};
363328
364/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5.329/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5.
365/// Copies `new` to the beginning of `aux_buf`, allowing the slices to overlap,330/// Copies `new` to the beginning of `aux_buf.*`, allowing the slices to overlap,
366/// then parses `new` as a URI, and then resolves the path in place.331/// then parses `new` as a URI, and then resolves the path in place.
367/// If a merge needs to take place, the newly constructed path will be stored332/// If a merge needs to take place, the newly constructed path will be stored
368/// in `aux_buf` just after the copied `new`.333/// in `aux_buf.*` just after the copied `new`, and `aux_buf.*` will be modified
369pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplaceError!Uri {334/// to only contain the remaining unused space.
370 std.mem.copyForwards(u8, aux_buf, new);335pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri {
336 std.mem.copyForwards(u8, aux_buf.*, new);
371 // At this point, new is an invalid pointer.337 // At this point, new is an invalid pointer.
372 const new_mut = aux_buf[0..new.len];338 const new_mut = aux_buf.*[0..new.len];
373339 aux_buf.* = aux_buf.*[new.len..];
374 const new_parsed, const has_scheme = p: {
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 };
385340
341 const new_parsed = parse(new_mut) catch |err|
342 (parseAfterScheme("", new_mut) catch return err);
386 // As you can see above, `new_mut` is not a const pointer.343 // As you can see above, `new_mut` is not a const pointer.
387 const new_path: []u8 = @constCast(new_parsed.path);344 const new_path: []u8 = @constCast(new_parsed.path.percent_encoded);
388345
389 if (has_scheme) return .{346 if (new_parsed.scheme.len > 0) return .{
390 .scheme = new_parsed.scheme,347 .scheme = new_parsed.scheme,
391 .user = new_parsed.user,348 .user = new_parsed.user,
349 .password = new_parsed.password,
392 .host = new_parsed.host,350 .host = new_parsed.host,
393 .port = new_parsed.port,351 .port = new_parsed.port,
394 .path = remove_dot_segments(new_path),352 .path = remove_dot_segments(new_path),
...@@ -399,6 +357,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace...@@ -399,6 +357,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
399 if (new_parsed.host) |host| return .{357 if (new_parsed.host) |host| return .{
400 .scheme = base.scheme,358 .scheme = base.scheme,
401 .user = new_parsed.user,359 .user = new_parsed.user,
360 .password = new_parsed.password,
402 .host = host,361 .host = host,
403 .port = new_parsed.port,362 .port = new_parsed.port,
404 .path = remove_dot_segments(new_path),363 .path = remove_dot_segments(new_path),
...@@ -406,28 +365,21 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace...@@ -406,28 +365,21 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
406 .fragment = new_parsed.fragment,365 .fragment = new_parsed.fragment,
407 };366 };
408367
409 const path, const query = b: {368 const path, const query = if (new_path.len == 0) .{
410 if (new_path.len == 0)369 base.path,
411 break :b .{370 new_parsed.query orelse base.query,
412 base.path,371 } else if (new_path[0] == '/') .{
413 new_parsed.query orelse base.query,372 remove_dot_segments(new_path),
414 };373 new_parsed.query,
415374 } else .{
416 if (new_path[0] == '/')375 try merge_paths(base.path, new_path, aux_buf),
417 break :b .{376 new_parsed.query,
418 remove_dot_segments(new_path),
419 new_parsed.query,
420 };
421
422 break :b .{
423 try merge_paths(base.path, new_path, aux_buf[new_mut.len..]),
424 new_parsed.query,
425 };
426 };377 };
427378
428 return .{379 return .{
429 .scheme = base.scheme,380 .scheme = base.scheme,
430 .user = base.user,381 .user = base.user,
382 .password = base.password,
431 .host = base.host,383 .host = base.host,
432 .port = base.port,384 .port = base.port,
433 .path = path,385 .path = path,
...@@ -437,7 +389,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace...@@ -437,7 +389,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
437}389}
438390
439/// In-place implementation of RFC 3986, Section 5.2.4.391/// In-place implementation of RFC 3986, Section 5.2.4.
440fn remove_dot_segments(path: []u8) []u8 {392fn remove_dot_segments(path: []u8) Component {
441 var in_i: usize = 0;393 var in_i: usize = 0;
442 var out_i: usize = 0;394 var out_i: usize = 0;
443 while (in_i < path.len) {395 while (in_i < path.len) {
...@@ -476,28 +428,28 @@ fn remove_dot_segments(path: []u8) []u8 {...@@ -476,28 +428,28 @@ fn remove_dot_segments(path: []u8) []u8 {
476 }428 }
477 }429 }
478 }430 }
479 return path[0..out_i];431 return .{ .percent_encoded = path[0..out_i] };
480}432}
481433
482test remove_dot_segments {434test remove_dot_segments {
483 {435 {
484 var buffer = "/a/b/c/./../../g".*;436 var buffer = "/a/b/c/./../../g".*;
485 try std.testing.expectEqualStrings("/a/g", remove_dot_segments(&buffer));437 try std.testing.expectEqualStrings("/a/g", remove_dot_segments(&buffer).percent_encoded);
486 }438 }
487}439}
488440
489/// 5.2.3. Merge Paths441/// 5.2.3. Merge Paths
490fn merge_paths(base: []const u8, new: []u8, aux: []u8) error{OutOfMemory}![]u8 {442fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Component {
491 if (aux.len < base.len + 1 + new.len) return error.OutOfMemory;443 var aux = std.io.fixedBufferStream(aux_buf.*);
492 if (base.len == 0) {444 if (!base.isEmpty()) {
493 aux[0] = '/';445 try aux.writer().print("{path}", .{base});
494 @memcpy(aux[1..][0..new.len], new);446 aux.pos = std.mem.lastIndexOfScalar(u8, aux.getWritten(), '/') orelse
495 return remove_dot_segments(aux[0 .. new.len + 1]);447 return remove_dot_segments(new);
496 }448 }
497 const pos = std.mem.lastIndexOfScalar(u8, base, '/') orelse return remove_dot_segments(new);449 try aux.writer().print("/{s}", .{new});
498 @memcpy(aux[0 .. pos + 1], base[0 .. pos + 1]);450 const merged_path = remove_dot_segments(aux.getWritten());
499 @memcpy(aux[pos + 1 ..][0..new.len], new);451 aux_buf.* = aux_buf.*[merged_path.percent_encoded.len..];
500 return remove_dot_segments(aux[0 .. pos + 1 + new.len]);452 return merged_path;
501}453}
502454
503const SliceReader = struct {455const SliceReader = struct {
...@@ -561,13 +513,6 @@ fn isSchemeChar(c: u8) bool {...@@ -561,13 +513,6 @@ fn isSchemeChar(c: u8) bool {
561 };513 };
562}514}
563515
564fn isAuthoritySeparator(c: u8) bool {
565 return switch (c) {
566 '/', '?', '#' => true,
567 else => false,
568 };
569}
570
571/// reserved = gen-delims / sub-delims516/// reserved = gen-delims / sub-delims
572fn isReserved(c: u8) bool {517fn isReserved(c: u8) bool {
573 return isGenLimit(c) or isSubLimit(c);518 return isGenLimit(c) or isSubLimit(c);
...@@ -598,19 +543,40 @@ fn isUnreserved(c: u8) bool {...@@ -598,19 +543,40 @@ fn isUnreserved(c: u8) bool {
598 };543 };
599}544}
600545
601fn isPathSeparator(c: u8) bool {546fn isUserChar(c: u8) bool {
602 return switch (c) {547 return isUnreserved(c) or isSubLimit(c);
603 '?', '#' => true,548}
604 else => false,549
605 };550fn isPasswordChar(c: u8) bool {
551 return isUserChar(c) or c == ':';
552}
553
554fn isHostChar(c: u8) bool {
555 return isPasswordChar(c) or c == '[' or c == ']';
606}556}
607557
608fn isPathChar(c: u8) bool {558fn isPathChar(c: u8) bool {
609 return isUnreserved(c) or isSubLimit(c) or c == '/' or c == ':' or c == '@';559 return isUserChar(c) or c == '/' or c == ':' or c == '@';
610}560}
611561
612fn isQueryChar(c: u8) bool {562fn isQueryChar(c: u8) bool {
613 return isPathChar(c) or c == '?' or c == '%';563 return isPathChar(c) or c == '?';
564}
565
566const isFragmentChar = isQueryChar;
567
568fn isAuthoritySeparator(c: u8) bool {
569 return switch (c) {
570 '/', '?', '#' => true,
571 else => false,
572 };
573}
574
575fn isPathSeparator(c: u8) bool {
576 return switch (c) {
577 '?', '#' => true,
578 else => false,
579 };
614}580}
615581
616fn isQuerySeparator(c: u8) bool {582fn isQuerySeparator(c: u8) bool {
...@@ -623,92 +589,92 @@ fn isQuerySeparator(c: u8) bool {...@@ -623,92 +589,92 @@ fn isQuerySeparator(c: u8) bool {
623test "basic" {589test "basic" {
624 const parsed = try parse("https://ziglang.org/download");590 const parsed = try parse("https://ziglang.org/download");
625 try testing.expectEqualStrings("https", parsed.scheme);591 try testing.expectEqualStrings("https", parsed.scheme);
626 try testing.expectEqualStrings("ziglang.org", parsed.host orelse return error.UnexpectedNull);592 try testing.expectEqualStrings("ziglang.org", parsed.host.?.percent_encoded);
627 try testing.expectEqualStrings("/download", parsed.path);593 try testing.expectEqualStrings("/download", parsed.path.percent_encoded);
628 try testing.expectEqual(@as(?u16, null), parsed.port);594 try testing.expectEqual(@as(?u16, null), parsed.port);
629}595}
630596
631test "with port" {597test "with port" {
632 const parsed = try parse("http://example:1337/");598 const parsed = try parse("http://example:1337/");
633 try testing.expectEqualStrings("http", parsed.scheme);599 try testing.expectEqualStrings("http", parsed.scheme);
634 try testing.expectEqualStrings("example", parsed.host orelse return error.UnexpectedNull);600 try testing.expectEqualStrings("example", parsed.host.?.percent_encoded);
635 try testing.expectEqualStrings("/", parsed.path);601 try testing.expectEqualStrings("/", parsed.path.percent_encoded);
636 try testing.expectEqual(@as(?u16, 1337), parsed.port);602 try testing.expectEqual(@as(?u16, 1337), parsed.port);
637}603}
638604
639test "should fail gracefully" {605test "should fail gracefully" {
640 try std.testing.expectEqual(@as(ParseError!Uri, error.InvalidFormat), parse("foobar://"));606 try std.testing.expectError(error.InvalidFormat, parse("foobar://"));
641}607}
642608
643test "file" {609test "file" {
644 const parsed = try parse("file:///");610 const parsed = try parse("file:///");
645 try std.testing.expectEqualSlices(u8, "file", parsed.scheme);611 try std.testing.expectEqualStrings("file", parsed.scheme);
646 try std.testing.expectEqual(@as(?[]const u8, null), parsed.host);612 try std.testing.expectEqual(@as(?Component, null), parsed.host);
647 try std.testing.expectEqualSlices(u8, "/", parsed.path);613 try std.testing.expectEqualStrings("/", parsed.path.percent_encoded);
648614
649 const parsed2 = try parse("file:///an/absolute/path/to/something");615 const parsed2 = try parse("file:///an/absolute/path/to/something");
650 try std.testing.expectEqualSlices(u8, "file", parsed2.scheme);616 try std.testing.expectEqualStrings("file", parsed2.scheme);
651 try std.testing.expectEqual(@as(?[]const u8, null), parsed2.host);617 try std.testing.expectEqual(@as(?Component, null), parsed2.host);
652 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/something", parsed2.path);618 try std.testing.expectEqualStrings("/an/absolute/path/to/something", parsed2.path.percent_encoded);
653619
654 const parsed3 = try parse("file://localhost/an/absolute/path/to/another/thing/");620 const parsed3 = try parse("file://localhost/an/absolute/path/to/another/thing/");
655 try std.testing.expectEqualSlices(u8, "file", parsed3.scheme);621 try std.testing.expectEqualStrings("file", parsed3.scheme);
656 try std.testing.expectEqualSlices(u8, "localhost", parsed3.host.?);622 try std.testing.expectEqualStrings("localhost", parsed3.host.?.percent_encoded);
657 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/another/thing/", parsed3.path);623 try std.testing.expectEqualStrings("/an/absolute/path/to/another/thing/", parsed3.path.percent_encoded);
658}624}
659625
660test "scheme" {626test "scheme" {
661 try std.testing.expectEqualSlices(u8, "http", (try parse("http:_")).scheme);627 try std.testing.expectEqualStrings("http", (try parse("http:_")).scheme);
662 try std.testing.expectEqualSlices(u8, "scheme-mee", (try parse("scheme-mee:_")).scheme);628 try std.testing.expectEqualStrings("scheme-mee", (try parse("scheme-mee:_")).scheme);
663 try std.testing.expectEqualSlices(u8, "a.b.c", (try parse("a.b.c:_")).scheme);629 try std.testing.expectEqualStrings("a.b.c", (try parse("a.b.c:_")).scheme);
664 try std.testing.expectEqualSlices(u8, "ab+", (try parse("ab+:_")).scheme);630 try std.testing.expectEqualStrings("ab+", (try parse("ab+:_")).scheme);
665 try std.testing.expectEqualSlices(u8, "X+++", (try parse("X+++:_")).scheme);631 try std.testing.expectEqualStrings("X+++", (try parse("X+++:_")).scheme);
666 try std.testing.expectEqualSlices(u8, "Y+-.", (try parse("Y+-.:_")).scheme);632 try std.testing.expectEqualStrings("Y+-.", (try parse("Y+-.:_")).scheme);
667}633}
668634
669test "authority" {635test "authority" {
670 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname")).host.?);636 try std.testing.expectEqualStrings("hostname", (try parse("scheme://hostname")).host.?.percent_encoded);
671637
672 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname")).host.?);638 try std.testing.expectEqualStrings("hostname", (try parse("scheme://userinfo@hostname")).host.?.percent_encoded);
673 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname")).user.?);639 try std.testing.expectEqualStrings("userinfo", (try parse("scheme://userinfo@hostname")).user.?.percent_encoded);
674 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname")).password);640 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@hostname")).password);
675 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@")).host);641 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@")).host);
676642
677 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname")).host.?);643 try std.testing.expectEqualStrings("hostname", (try parse("scheme://user:password@hostname")).host.?.percent_encoded);
678 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname")).user.?);644 try std.testing.expectEqualStrings("user", (try parse("scheme://user:password@hostname")).user.?.percent_encoded);
679 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname")).password.?);645 try std.testing.expectEqualStrings("password", (try parse("scheme://user:password@hostname")).password.?.percent_encoded);
680646
681 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname:0")).host.?);647 try std.testing.expectEqualStrings("hostname", (try parse("scheme://hostname:0")).host.?.percent_encoded);
682 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://hostname:1234")).port.?);648 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://hostname:1234")).port.?);
683649
684 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname:1234")).host.?);650 try std.testing.expectEqualStrings("hostname", (try parse("scheme://userinfo@hostname:1234")).host.?.percent_encoded);
685 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://userinfo@hostname:1234")).port.?);651 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://userinfo@hostname:1234")).port.?);
686 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname:1234")).user.?);652 try std.testing.expectEqualStrings("userinfo", (try parse("scheme://userinfo@hostname:1234")).user.?.percent_encoded);
687 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname:1234")).password);653 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@hostname:1234")).password);
688654
689 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname:1234")).host.?);655 try std.testing.expectEqualStrings("hostname", (try parse("scheme://user:password@hostname:1234")).host.?.percent_encoded);
690 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://user:password@hostname:1234")).port.?);656 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://user:password@hostname:1234")).port.?);
691 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname:1234")).user.?);657 try std.testing.expectEqualStrings("user", (try parse("scheme://user:password@hostname:1234")).user.?.percent_encoded);
692 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname:1234")).password.?);658 try std.testing.expectEqualStrings("password", (try parse("scheme://user:password@hostname:1234")).password.?.percent_encoded);
693}659}
694660
695test "authority.password" {661test "authority.password" {
696 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username@a")).user.?);662 try std.testing.expectEqualStrings("username", (try parse("scheme://username@a")).user.?.percent_encoded);
697 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username@a")).password);663 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://username@a")).password);
698664
699 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:@a")).user.?);665 try std.testing.expectEqualStrings("username", (try parse("scheme://username:@a")).user.?.percent_encoded);
700 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username:@a")).password);666 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://username:@a")).password);
701667
702 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:password@a")).user.?);668 try std.testing.expectEqualStrings("username", (try parse("scheme://username:password@a")).user.?.percent_encoded);
703 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://username:password@a")).password.?);669 try std.testing.expectEqualStrings("password", (try parse("scheme://username:password@a")).password.?.percent_encoded);
704670
705 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username::@a")).user.?);671 try std.testing.expectEqualStrings("username", (try parse("scheme://username::@a")).user.?.percent_encoded);
706 try std.testing.expectEqualSlices(u8, ":", (try parse("scheme://username::@a")).password.?);672 try std.testing.expectEqualStrings(":", (try parse("scheme://username::@a")).password.?.percent_encoded);
707}673}
708674
709fn testAuthorityHost(comptime hostlist: anytype) !void {675fn testAuthorityHost(comptime hostlist: anytype) !void {
710 inline for (hostlist) |hostname| {676 inline for (hostlist) |hostname| {
711 try std.testing.expectEqualSlices(u8, hostname, (try parse("scheme://" ++ hostname)).host.?);677 try std.testing.expectEqualStrings(hostname, (try parse("scheme://" ++ hostname)).host.?.percent_encoded);
712 }678 }
713}679}
714680
...@@ -761,11 +727,11 @@ test "RFC example 1" {...@@ -761,11 +727,11 @@ test "RFC example 1" {
761 .scheme = uri[0..3],727 .scheme = uri[0..3],
762 .user = null,728 .user = null,
763 .password = null,729 .password = null,
764 .host = uri[6..17],730 .host = .{ .percent_encoded = uri[6..17] },
765 .port = 8042,731 .port = 8042,
766 .path = uri[22..33],732 .path = .{ .percent_encoded = uri[22..33] },
767 .query = uri[34..45],733 .query = .{ .percent_encoded = uri[34..45] },
768 .fragment = uri[46..50],734 .fragment = .{ .percent_encoded = uri[46..50] },
769 }, try parse(uri));735 }, try parse(uri));
770}736}
771737
...@@ -777,7 +743,7 @@ test "RFC example 2" {...@@ -777,7 +743,7 @@ test "RFC example 2" {
777 .password = null,743 .password = null,
778 .host = null,744 .host = null,
779 .port = null,745 .port = null,
780 .path = uri[4..],746 .path = .{ .percent_encoded = uri[4..] },
781 .query = null,747 .query = null,
782 .fragment = null,748 .fragment = null,
783 }, try parse(uri));749 }, try parse(uri));
...@@ -838,55 +804,60 @@ test "Special test" {...@@ -838,55 +804,60 @@ test "Special test" {
838 _ = try parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be&t=0");804 _ = try parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be&t=0");
839}805}
840806
841test "URI escaping" {807test "URI percent encoding" {
842 const input = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";808 try std.testing.expectFmt(
843 const expected = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";809 "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad",
810 "{%}",
811 .{Component{ .raw = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad" }},
812 );
813}
844814
845 const actual = try escapeString(std.testing.allocator, input);815test "URI percent decoding" {
846 defer std.testing.allocator.free(actual);816 {
817 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
818 var input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad".*;
847819
848 try std.testing.expectEqualSlices(u8, expected, actual);820 try std.testing.expectFmt(expected, "{raw}", .{Component{ .percent_encoded = &input }});
849}821
822 var output: [expected.len]u8 = undefined;
823 try std.testing.expectEqualStrings(percentDecodeBackwards(&output, &input), expected);
824
825 try std.testing.expectEqualStrings(expected, percentDecodeInPlace(&input));
826 }
850827
851test "URI unescaping" {828 {
852 const input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";829 const expected = "/abc%";
853 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";830 var input = expected.*;
854831
855 const actual = try unescapeString(std.testing.allocator, input);832 try std.testing.expectFmt(expected, "{raw}", .{Component{ .percent_encoded = &input }});
856 defer std.testing.allocator.free(actual);
857833
858 try std.testing.expectEqualSlices(u8, expected, actual);834 var output: [expected.len]u8 = undefined;
835 try std.testing.expectEqualStrings(percentDecodeBackwards(&output, &input), expected);
859836
860 const decoded = try unescapeString(std.testing.allocator, "/abc%");837 try std.testing.expectEqualStrings(expected, percentDecodeInPlace(&input));
861 defer std.testing.allocator.free(decoded);838 }
862 try std.testing.expectEqualStrings("/abc%", decoded);
863}839}
864840
865test "URI query escaping" {841test "URI query encoding" {
866 const address = "https://objects.githubusercontent.com/?response-content-type=application%2Foctet-stream";842 const address = "https://objects.githubusercontent.com/?response-content-type=application%2Foctet-stream";
867 const parsed = try Uri.parse(address);843 const parsed = try Uri.parse(address);
868844
869 // format the URI to escape it845 // format the URI to percent encode it
870 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{/?}", .{parsed});846 try std.testing.expectFmt("/?response-content-type=application%2Foctet-stream", "{/?}", .{parsed});
871 defer std.testing.allocator.free(formatted_uri);
872 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
873}847}
874848
875test "format" {849test "format" {
876 const uri = Uri{850 const uri: Uri = .{
877 .scheme = "file",851 .scheme = "file",
878 .user = null,852 .user = null,
879 .password = null,853 .password = null,
880 .host = null,854 .host = null,
881 .port = null,855 .port = null,
882 .path = "/foo/bar/baz",856 .path = .{ .raw = "/foo/bar/baz" },
883 .query = null,857 .query = null,
884 .fragment = null,858 .fragment = null,
885 };859 };
886 var buf = std.ArrayList(u8).init(std.testing.allocator);860 try std.testing.expectFmt("file:/foo/bar/baz", "{;/?#}", .{uri});
887 defer buf.deinit();
888 try buf.writer().print("{;/?#}", .{uri});
889 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
890}861}
891862
892test "URI malformed input" {863test "URI malformed input" {
...@@ -894,3 +865,7 @@ test "URI malformed input" {...@@ -894,3 +865,7 @@ test "URI malformed input" {
894 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@["));865 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@["));
895 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q"));866 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q"));
896}867}
868
869const std = @import("std.zig");
870const testing = std.testing;
871const Uri = @This();
lib/std/http/Client.zig+108-128
...@@ -771,17 +771,41 @@ pub const Request = struct {...@@ -771,17 +771,41 @@ pub const Request = struct {
771 req.client.connection_pool.release(req.client.allocator, req.connection.?);771 req.client.connection_pool.release(req.client.allocator, req.connection.?);
772 req.connection = null;772 req.connection = null;
773773
774 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;774 var server_header = std.heap.FixedBufferAllocator.init(req.response.parser.header_bytes_buffer);
775 defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..];
776 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
777
778 const new_host = valid_uri.host.?.raw;
779 const prev_host = req.uri.host.?.raw;
780 const keep_privileged_headers =
781 std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and
782 std.ascii.endsWithIgnoreCase(new_host, prev_host) and
783 (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.');
784 if (!keep_privileged_headers) {
785 // When redirecting to a different domain, strip privileged headers.
786 req.privileged_headers = &.{};
787 }
775788
776 const port: u16 = uri.port orelse switch (protocol) {789 if (switch (req.response.status) {
777 .plain => 80,790 .see_other => true,
778 .tls => 443,791 .moved_permanently, .found => req.method == .POST,
779 };792 else => false,
793 }) {
794 // A redirect to a GET must change the method and remove the body.
795 req.method = .GET;
796 req.transfer_encoding = .none;
797 req.headers.content_type = .omit;
798 }
780799
781 const host = uri.host orelse return error.UriMissingHost;800 if (req.transfer_encoding != .none) {
801 // The request body has already been sent. The request is
802 // still in a valid state, but the redirect must be handled
803 // manually.
804 return error.RedirectRequiresResend;
805 }
782806
783 req.uri = uri;807 req.uri = valid_uri;
784 req.connection = try req.client.connect(host, port, protocol);808 req.connection = try req.client.connect(new_host, valid_uri.port.?, protocol);
785 req.redirect_behavior.subtractOne();809 req.redirect_behavior.subtractOne();
786 req.response.parser.reset();810 req.response.parser.reset();
787811
...@@ -796,13 +820,8 @@ pub const Request = struct {...@@ -796,13 +820,8 @@ pub const Request = struct {
796820
797 pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };821 pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };
798822
799 pub const SendOptions = struct {
800 /// Specifies that the uri is already escaped.
801 raw_uri: bool = false,
802 };
803
804 /// Send the HTTP request headers to the server.823 /// Send the HTTP request headers to the server.
805 pub fn send(req: *Request, options: SendOptions) SendError!void {824 pub fn send(req: *Request) SendError!void {
806 if (!req.method.requestHasBody() and req.transfer_encoding != .none)825 if (!req.method.requestHasBody() and req.transfer_encoding != .none)
807 return error.UnsupportedTransferEncoding;826 return error.UnsupportedTransferEncoding;
808827
...@@ -821,7 +840,6 @@ pub const Request = struct {...@@ -821,7 +840,6 @@ pub const Request = struct {
821 .authority = connection.proxied,840 .authority = connection.proxied,
822 .path = true,841 .path = true,
823 .query = true,842 .query = true,
824 .raw = options.raw_uri,
825 }, w);843 }, w);
826 }844 }
827 try w.writeByte(' ');845 try w.writeByte(' ');
...@@ -1038,55 +1056,19 @@ pub const Request = struct {...@@ -1038,55 +1056,19 @@ pub const Request = struct {
1038 const location = req.response.location orelse1056 const location = req.response.location orelse
1039 return error.HttpRedirectLocationMissing;1057 return error.HttpRedirectLocationMissing;
10401058
1041 // This mutates the beginning of header_buffer and uses that1059 // This mutates the beginning of header_bytes_buffer and uses that
1042 // for the backing memory of the returned new_uri.1060 // for the backing memory of the returned Uri.
1043 const header_buffer = req.response.parser.header_bytes_buffer;1061 try req.redirect(req.uri.resolve_inplace(
1044 const new_uri = req.uri.resolve_inplace(location, header_buffer) catch1062 location,
1045 return error.HttpRedirectLocationInvalid;1063 &req.response.parser.header_bytes_buffer,
10461064 ) catch |err| switch (err) {
1047 // The new URI references the beginning of header_bytes_buffer memory.1065 error.UnexpectedCharacter,
1048 // That memory will be kept, but everything after it will be1066 error.InvalidFormat,
1049 // reused by the subsequent request. In other words,1067 error.InvalidPort,
1050 // header_bytes_buffer must be large enough to store all1068 => return error.HttpRedirectLocationInvalid,
1051 // redirect locations as well as the final request header.1069 error.NoSpaceLeft => return error.HttpHeadersOversize,
1052 const path_end = new_uri.path.ptr + new_uri.path.len;1070 });
1053 // https://github.com/ziglang/zig/issues/17381071 try req.send();
1054 const path_offset = @intFromPtr(path_end) - @intFromPtr(header_buffer.ptr);
1055 const end_offset = @max(path_offset, location.len);
1056 req.response.parser.header_bytes_buffer = header_buffer[end_offset..];
1057
1058 const is_same_domain_or_subdomain =
1059 std.ascii.endsWithIgnoreCase(new_uri.host.?, req.uri.host.?) and
1060 (new_uri.host.?.len == req.uri.host.?.len or
1061 new_uri.host.?[new_uri.host.?.len - req.uri.host.?.len - 1] == '.');
1062
1063 if (new_uri.host == null or !is_same_domain_or_subdomain or
1064 !std.ascii.eqlIgnoreCase(new_uri.scheme, req.uri.scheme))
1065 {
1066 // When redirecting to a different domain, strip privileged headers.
1067 req.privileged_headers = &.{};
1068 }
1069
1070 if (switch (req.response.status) {
1071 .see_other => true,
1072 .moved_permanently, .found => req.method == .POST,
1073 else => false,
1074 }) {
1075 // A redirect to a GET must change the method and remove the body.
1076 req.method = .GET;
1077 req.transfer_encoding = .none;
1078 req.headers.content_type = .omit;
1079 }
1080
1081 if (req.transfer_encoding != .none) {
1082 // The request body has already been sent. The request is
1083 // still in a valid state, but the redirect must be handled
1084 // manually.
1085 return error.RedirectRequiresResend;
1086 }
1087
1088 try req.redirect(new_uri);
1089 try req.send(.{});
1090 } else {1072 } else {
1091 req.response.skip = false;1073 req.response.skip = false;
1092 if (!req.response.parser.done) {1074 if (!req.response.parser.done) {
...@@ -1264,30 +1246,25 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?...@@ -1264,30 +1246,25 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?
1264 };1246 };
1265 } else return null;1247 } else return null;
12661248
1267 const uri = Uri.parse(content) catch try Uri.parseWithoutScheme(content);1249 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
12681250 const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) {
1269 const protocol = if (uri.scheme.len == 0)1251 error.UnsupportedUriScheme => return null,
1270 .plain // No scheme, assume http://1252 error.UriMissingHost => return error.HttpProxyMissingHost,
1271 else1253 error.OutOfMemory => |e| return e,
1272 protocol_map.get(uri.scheme) orelse return null; // Unknown scheme, ignore1254 };
1273
1274 const host = uri.host orelse return error.HttpProxyMissingHost;
12751255
1276 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {1256 const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: {
1277 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));1257 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri));
1278 assert(basic_authorization.value(uri, authorization).len == authorization.len);1258 assert(basic_authorization.value(valid_uri, authorization).len == authorization.len);
1279 break :a authorization;1259 break :a authorization;
1280 } else null;1260 } else null;
12811261
1282 const proxy = try arena.create(Proxy);1262 const proxy = try arena.create(Proxy);
1283 proxy.* = .{1263 proxy.* = .{
1284 .protocol = protocol,1264 .protocol = protocol,
1285 .host = host,1265 .host = valid_uri.host.?.raw,
1286 .authorization = authorization,1266 .authorization = authorization,
1287 .port = uri.port orelse switch (protocol) {1267 .port = valid_uri.port.?,
1288 .plain => 80,
1289 .tls => 443,
1290 },
1291 .supports_connect = true,1268 .supports_connect = true,
1292 };1269 };
1293 return proxy;1270 return proxy;
...@@ -1305,24 +1282,26 @@ pub const basic_authorization = struct {...@@ -1305,24 +1282,26 @@ pub const basic_authorization = struct {
1305 }1282 }
13061283
1307 pub fn valueLengthFromUri(uri: Uri) usize {1284 pub fn valueLengthFromUri(uri: Uri) usize {
1308 return valueLength(1285 var stream = std.io.countingWriter(std.io.null_writer);
1309 if (uri.user) |user| user.len else 0,1286 try stream.writer().print("{user}", .{uri.user orelse Uri.Component.empty});
1310 if (uri.password) |password| password.len else 0,1287 const user_len = stream.bytes_written;
1311 );1288 stream.bytes_written = 0;
1289 try stream.writer().print("{password}", .{uri.password orelse Uri.Component.empty});
1290 const password_len = stream.bytes_written;
1291 return valueLength(@intCast(user_len), @intCast(password_len));
1312 }1292 }
13131293
1314 pub fn value(uri: Uri, out: []u8) []u8 {1294 pub fn value(uri: Uri, out: []u8) []u8 {
1315 assert(uri.user == null or uri.user.?.len <= max_user_len);
1316 assert(uri.password == null or uri.password.?.len <= max_password_len);
1317
1318 @memcpy(out[0..prefix.len], prefix);
1319
1320 var buf: [max_user_len + ":".len + max_password_len]u8 = undefined;1295 var buf: [max_user_len + ":".len + max_password_len]u8 = undefined;
1321 const unencoded = std.fmt.bufPrint(&buf, "{s}:{s}", .{1296 var stream = std.io.fixedBufferStream(&buf);
1322 uri.user orelse "", uri.password orelse "",1297 stream.writer().print("{user}", .{uri.user orelse Uri.Component.empty}) catch
1323 }) catch unreachable;1298 unreachable;
1324 const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], unencoded);1299 assert(stream.pos <= max_user_len);
1300 stream.writer().print(":{password}", .{uri.password orelse Uri.Component.empty}) catch
1301 unreachable;
13251302
1303 @memcpy(out[0..prefix.len], prefix);
1304 const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], stream.getWritten());
1326 return out[0 .. prefix.len + base64.len];1305 return out[0 .. prefix.len + base64.len];
1327 }1306 }
1328};1307};
...@@ -1337,8 +1316,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec...@@ -1337,8 +1316,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
1337 .host = host,1316 .host = host,
1338 .port = port,1317 .port = port,
1339 .protocol = protocol,1318 .protocol = protocol,
1340 })) |node|1319 })) |node| return node;
1341 return node;
13421320
1343 if (disable_tls and protocol == .tls)1321 if (disable_tls and protocol == .tls)
1344 return error.TlsInitializationFailed;1322 return error.TlsInitializationFailed;
...@@ -1449,19 +1427,12 @@ pub fn connectTunnel(...@@ -1449,19 +1427,12 @@ pub fn connectTunnel(
1449 client.connection_pool.release(client.allocator, conn);1427 client.connection_pool.release(client.allocator, conn);
1450 }1428 }
14511429
1452 const uri: Uri = .{1430 var buffer: [8096]u8 = undefined;
1431 var req = client.open(.CONNECT, .{
1453 .scheme = "http",1432 .scheme = "http",
1454 .user = null,1433 .host = .{ .raw = tunnel_host },
1455 .password = null,
1456 .host = tunnel_host,
1457 .port = tunnel_port,1434 .port = tunnel_port,
1458 .path = "",1435 }, .{
1459 .query = null,
1460 .fragment = null,
1461 };
1462
1463 var buffer: [8096]u8 = undefined;
1464 var req = client.open(.CONNECT, uri, .{
1465 .redirect_behavior = .unhandled,1436 .redirect_behavior = .unhandled,
1466 .connection = conn,1437 .connection = conn,
1467 .server_header_buffer = &buffer,1438 .server_header_buffer = &buffer,
...@@ -1471,7 +1442,7 @@ pub fn connectTunnel(...@@ -1471,7 +1442,7 @@ pub fn connectTunnel(
1471 };1442 };
1472 defer req.deinit();1443 defer req.deinit();
14731444
1474 req.send(.{ .raw_uri = true }) catch |err| break :tunnel err;1445 req.send() catch |err| break :tunnel err;
1475 req.wait() catch |err| break :tunnel err;1446 req.wait() catch |err| break :tunnel err;
14761447
1477 if (req.response.status.class() == .server_error) {1448 if (req.response.status.class() == .server_error) {
...@@ -1500,7 +1471,7 @@ pub fn connectTunnel(...@@ -1500,7 +1471,7 @@ pub fn connectTunnel(
1500}1471}
15011472
1502// Prevents a dependency loop in open()1473// Prevents a dependency loop in open()
1503const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };1474const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUriScheme, ConnectionRefused };
1504pub const ConnectError = ConnectErrorPartial || RequestError;1475pub const ConnectError = ConnectErrorPartial || RequestError;
15051476
1506/// Connect to `host:port` using the specified protocol. This will reuse a1477/// Connect to `host:port` using the specified protocol. This will reuse a
...@@ -1548,7 +1519,7 @@ pub fn connect(...@@ -1548,7 +1519,7 @@ pub fn connect(
1548pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||1519pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
1549 std.fmt.ParseIntError || Connection.WriteError ||1520 std.fmt.ParseIntError || Connection.WriteError ||
1550 error{ // TODO: file a zig fmt issue for this bad indentation1521 error{ // TODO: file a zig fmt issue for this bad indentation
1551 UnsupportedUrlScheme,1522 UnsupportedUriScheme,
1552 UriMissingHost,1523 UriMissingHost,
15531524
1554 CertificateBundleLoadFailure,1525 CertificateBundleLoadFailure,
...@@ -1598,12 +1569,25 @@ pub const RequestOptions = struct {...@@ -1598,12 +1569,25 @@ pub const RequestOptions = struct {
1598 privileged_headers: []const http.Header = &.{},1569 privileged_headers: []const http.Header = &.{},
1599};1570};
16001571
1601pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{1572fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } {
1602 .{ "http", .plain },1573 const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1603 .{ "ws", .plain },1574 .{ "http", .plain },
1604 .{ "https", .tls },1575 .{ "ws", .plain },
1605 .{ "wss", .tls },1576 .{ "https", .tls },
1606});1577 .{ "wss", .tls },
1578 });
1579 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme;
1580 var valid_uri = uri;
1581 // The host is always going to be needed as a raw string for hostname resolution anyway.
1582 valid_uri.host = .{
1583 .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena),
1584 };
1585 valid_uri.port = uri.port orelse switch (protocol) {
1586 .plain => 80,
1587 .tls => 443,
1588 };
1589 return .{ protocol, valid_uri };
1590}
16071591
1608/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.1592/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
1609///1593///
...@@ -1633,14 +1617,8 @@ pub fn open(...@@ -1633,14 +1617,8 @@ pub fn open(
1633 }1617 }
1634 }1618 }
16351619
1636 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;1620 var server_header = std.heap.FixedBufferAllocator.init(options.server_header_buffer);
16371621 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
1638 const port: u16 = uri.port orelse switch (protocol) {
1639 .plain => 80,
1640 .tls => 443,
1641 };
1642
1643 const host = uri.host orelse return error.UriMissingHost;
16441622
1645 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {1623 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {
1646 if (disable_tls) unreachable;1624 if (disable_tls) unreachable;
...@@ -1649,15 +1627,17 @@ pub fn open(...@@ -1649,15 +1627,17 @@ pub fn open(
1649 defer client.ca_bundle_mutex.unlock();1627 defer client.ca_bundle_mutex.unlock();
16501628
1651 if (client.next_https_rescan_certs) {1629 if (client.next_https_rescan_certs) {
1652 client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure;1630 client.ca_bundle.rescan(client.allocator) catch
1631 return error.CertificateBundleLoadFailure;
1653 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);1632 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);
1654 }1633 }
1655 }1634 }
16561635
1657 const conn = options.connection orelse try client.connect(host, port, protocol);1636 const conn = options.connection orelse
1637 try client.connect(valid_uri.host.?.raw, valid_uri.port.?, protocol);
16581638
1659 var req: Request = .{1639 var req: Request = .{
1660 .uri = uri,1640 .uri = valid_uri,
1661 .client = client,1641 .client = client,
1662 .connection = conn,1642 .connection = conn,
1663 .keep_alive = options.keep_alive,1643 .keep_alive = options.keep_alive,
...@@ -1671,7 +1651,7 @@ pub fn open(...@@ -1671,7 +1651,7 @@ pub fn open(
1671 .status = undefined,1651 .status = undefined,
1672 .reason = undefined,1652 .reason = undefined,
1673 .keep_alive = undefined,1653 .keep_alive = undefined,
1674 .parser = proto.HeadersParser.init(options.server_header_buffer),1654 .parser = proto.HeadersParser.init(server_header.buffer[server_header.end_index..]),
1675 },1655 },
1676 .headers = options.headers,1656 .headers = options.headers,
1677 .extra_headers = options.extra_headers,1657 .extra_headers = options.extra_headers,
...@@ -1751,7 +1731,7 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {...@@ -1751,7 +1731,7 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {
17511731
1752 if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len };1732 if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len };
17531733
1754 try req.send(.{ .raw_uri = options.raw_uri });1734 try req.send();
17551735
1756 if (options.payload) |payload| try req.writeAll(payload);1736 if (options.payload) |payload| try req.writeAll(payload);
17571737
lib/std/http/test.zig+51-21
...@@ -64,7 +64,7 @@ test "trailers" {...@@ -64,7 +64,7 @@ test "trailers" {
64 });64 });
65 defer req.deinit();65 defer req.deinit();
6666
67 try req.send(.{});67 try req.send();
68 try req.wait();68 try req.wait();
6969
70 const body = try req.reader().readAllAlloc(gpa, 8192);70 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -474,6 +474,15 @@ test "general client/server API coverage" {...@@ -474,6 +474,15 @@ test "general client/server API coverage" {
474 .{ .name = "location", .value = "/redirect/3" },474 .{ .name = "location", .value = "/redirect/3" },
475 },475 },
476 });476 });
477 } else if (mem.eql(u8, request.head.target, "/redirect/5")) {
478 try request.respond("Hello, Redirected!\n", .{
479 .status = .found,
480 .extra_headers = &.{
481 .{ .name = "location", .value = "/%2525" },
482 },
483 });
484 } else if (mem.eql(u8, request.head.target, "/%2525")) {
485 try request.respond("Encoded redirect successful!\n", .{});
477 } else if (mem.eql(u8, request.head.target, "/redirect/invalid")) {486 } else if (mem.eql(u8, request.head.target, "/redirect/invalid")) {
478 const invalid_port = try getUnusedTcpPort();487 const invalid_port = try getUnusedTcpPort();
479 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});488 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
...@@ -529,7 +538,7 @@ test "general client/server API coverage" {...@@ -529,7 +538,7 @@ test "general client/server API coverage" {
529 });538 });
530 defer req.deinit();539 defer req.deinit();
531540
532 try req.send(.{});541 try req.send();
533 try req.wait();542 try req.wait();
534543
535 const body = try req.reader().readAllAlloc(gpa, 8192);544 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -554,7 +563,7 @@ test "general client/server API coverage" {...@@ -554,7 +563,7 @@ test "general client/server API coverage" {
554 });563 });
555 defer req.deinit();564 defer req.deinit();
556565
557 try req.send(.{});566 try req.send();
558 try req.wait();567 try req.wait();
559568
560 const body = try req.reader().readAllAlloc(gpa, 8192 * 1024);569 const body = try req.reader().readAllAlloc(gpa, 8192 * 1024);
...@@ -578,7 +587,7 @@ test "general client/server API coverage" {...@@ -578,7 +587,7 @@ test "general client/server API coverage" {
578 });587 });
579 defer req.deinit();588 defer req.deinit();
580589
581 try req.send(.{});590 try req.send();
582 try req.wait();591 try req.wait();
583592
584 const body = try req.reader().readAllAlloc(gpa, 8192);593 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -604,7 +613,7 @@ test "general client/server API coverage" {...@@ -604,7 +613,7 @@ test "general client/server API coverage" {
604 });613 });
605 defer req.deinit();614 defer req.deinit();
606615
607 try req.send(.{});616 try req.send();
608 try req.wait();617 try req.wait();
609618
610 const body = try req.reader().readAllAlloc(gpa, 8192);619 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -629,7 +638,7 @@ test "general client/server API coverage" {...@@ -629,7 +638,7 @@ test "general client/server API coverage" {
629 });638 });
630 defer req.deinit();639 defer req.deinit();
631640
632 try req.send(.{});641 try req.send();
633 try req.wait();642 try req.wait();
634643
635 const body = try req.reader().readAllAlloc(gpa, 8192);644 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -656,7 +665,7 @@ test "general client/server API coverage" {...@@ -656,7 +665,7 @@ test "general client/server API coverage" {
656 });665 });
657 defer req.deinit();666 defer req.deinit();
658667
659 try req.send(.{});668 try req.send();
660 try req.wait();669 try req.wait();
661670
662 const body = try req.reader().readAllAlloc(gpa, 8192);671 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -684,7 +693,7 @@ test "general client/server API coverage" {...@@ -684,7 +693,7 @@ test "general client/server API coverage" {
684 });693 });
685 defer req.deinit();694 defer req.deinit();
686695
687 try req.send(.{});696 try req.send();
688 try req.wait();697 try req.wait();
689698
690 try std.testing.expectEqual(.ok, req.response.status);699 try std.testing.expectEqual(.ok, req.response.status);
...@@ -725,7 +734,7 @@ test "general client/server API coverage" {...@@ -725,7 +734,7 @@ test "general client/server API coverage" {
725 });734 });
726 defer req.deinit();735 defer req.deinit();
727736
728 try req.send(.{});737 try req.send();
729 try req.wait();738 try req.wait();
730739
731 const body = try req.reader().readAllAlloc(gpa, 8192);740 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -749,7 +758,7 @@ test "general client/server API coverage" {...@@ -749,7 +758,7 @@ test "general client/server API coverage" {
749 });758 });
750 defer req.deinit();759 defer req.deinit();
751760
752 try req.send(.{});761 try req.send();
753 try req.wait();762 try req.wait();
754763
755 const body = try req.reader().readAllAlloc(gpa, 8192);764 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -773,7 +782,7 @@ test "general client/server API coverage" {...@@ -773,7 +782,7 @@ test "general client/server API coverage" {
773 });782 });
774 defer req.deinit();783 defer req.deinit();
775784
776 try req.send(.{});785 try req.send();
777 try req.wait();786 try req.wait();
778787
779 const body = try req.reader().readAllAlloc(gpa, 8192);788 const body = try req.reader().readAllAlloc(gpa, 8192);
...@@ -797,13 +806,34 @@ test "general client/server API coverage" {...@@ -797,13 +806,34 @@ test "general client/server API coverage" {
797 });806 });
798 defer req.deinit();807 defer req.deinit();
799808
800 try req.send(.{});809 try req.send();
801 req.wait() catch |err| switch (err) {810 req.wait() catch |err| switch (err) {
802 error.TooManyHttpRedirects => {},811 error.TooManyHttpRedirects => {},
803 else => return err,812 else => return err,
804 };813 };
805 }814 }
806815
816 { // redirect to encoded url
817 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
818 defer gpa.free(location);
819 const uri = try std.Uri.parse(location);
820
821 log.info("{s}", .{location});
822 var server_header_buffer: [1024]u8 = undefined;
823 var req = try client.open(.GET, uri, .{
824 .server_header_buffer = &server_header_buffer,
825 });
826 defer req.deinit();
827
828 try req.send();
829 try req.wait();
830
831 const body = try req.reader().readAllAlloc(gpa, 8192);
832 defer gpa.free(body);
833
834 try expectEqualStrings("Encoded redirect successful!\n", body);
835 }
836
807 // connection has been kept alive837 // connection has been kept alive
808 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);838 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
809839
...@@ -819,7 +849,7 @@ test "general client/server API coverage" {...@@ -819,7 +849,7 @@ test "general client/server API coverage" {
819 });849 });
820 defer req.deinit();850 defer req.deinit();
821851
822 try req.send(.{});852 try req.send();
823 const result = req.wait();853 const result = req.wait();
824854
825 // a proxy without an upstream is likely to return a 5xx status.855 // a proxy without an upstream is likely to return a 5xx status.
...@@ -913,16 +943,16 @@ test "Server streams both reading and writing" {...@@ -913,16 +943,16 @@ test "Server streams both reading and writing" {
913 var server_header_buffer: [555]u8 = undefined;943 var server_header_buffer: [555]u8 = undefined;
914 var req = try client.open(.POST, .{944 var req = try client.open(.POST, .{
915 .scheme = "http",945 .scheme = "http",
916 .host = "127.0.0.1",946 .host = .{ .raw = "127.0.0.1" },
917 .port = test_server.port(),947 .port = test_server.port(),
918 .path = "/",948 .path = .{ .percent_encoded = "/" },
919 }, .{949 }, .{
920 .server_header_buffer = &server_header_buffer,950 .server_header_buffer = &server_header_buffer,
921 });951 });
922 defer req.deinit();952 defer req.deinit();
923953
924 req.transfer_encoding = .chunked;954 req.transfer_encoding = .chunked;
925 try req.send(.{});955 try req.send();
926 try req.wait();956 try req.wait();
927957
928 try req.writeAll("one ");958 try req.writeAll("one ");
...@@ -956,7 +986,7 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -956,7 +986,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
956986
957 req.transfer_encoding = .{ .content_length = 14 };987 req.transfer_encoding = .{ .content_length = 14 };
958988
959 try req.send(.{});989 try req.send();
960 try req.writeAll("Hello, ");990 try req.writeAll("Hello, ");
961 try req.writeAll("World!\n");991 try req.writeAll("World!\n");
962 try req.finish();992 try req.finish();
...@@ -990,7 +1020,7 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -990,7 +1020,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
9901020
991 req.transfer_encoding = .chunked;1021 req.transfer_encoding = .chunked;
9921022
993 try req.send(.{});1023 try req.send();
994 try req.writeAll("Hello, ");1024 try req.writeAll("Hello, ");
995 try req.writeAll("World!\n");1025 try req.writeAll("World!\n");
996 try req.finish();1026 try req.finish();
...@@ -1044,7 +1074,7 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1044,7 +1074,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
10441074
1045 req.transfer_encoding = .chunked;1075 req.transfer_encoding = .chunked;
10461076
1047 try req.send(.{});1077 try req.send();
1048 try req.writeAll("Hello, ");1078 try req.writeAll("Hello, ");
1049 try req.writeAll("World!\n");1079 try req.writeAll("World!\n");
1050 try req.finish();1080 try req.finish();
...@@ -1075,7 +1105,7 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1075,7 +1105,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
10751105
1076 req.transfer_encoding = .chunked;1106 req.transfer_encoding = .chunked;
10771107
1078 try req.send(.{});1108 try req.send();
1079 try req.wait();1109 try req.wait();
1080 try expectEqual(.expectation_failed, req.response.status);1110 try expectEqual(.expectation_failed, req.response.status);
1081 }1111 }
...@@ -1180,7 +1210,7 @@ test "redirect to different connection" {...@@ -1180,7 +1210,7 @@ test "redirect to different connection" {
1180 });1210 });
1181 defer req.deinit();1211 defer req.deinit();
11821212
1183 try req.send(.{});1213 try req.send();
1184 try req.wait();1214 try req.wait();
11851215
1186 const body = try req.reader().readAllAlloc(gpa, 8192);1216 const body = try req.reader().readAllAlloc(gpa, 8192);
lib/std/io.zig+1-1
...@@ -413,7 +413,7 @@ pub const StreamSource = @import("io/stream_source.zig").StreamSource;...@@ -413,7 +413,7 @@ pub const StreamSource = @import("io/stream_source.zig").StreamSource;
413pub const tty = @import("io/tty.zig");413pub const tty = @import("io/tty.zig");
414414
415/// A Writer that doesn't write to anything.415/// A Writer that doesn't write to anything.
416pub const null_writer = @as(NullWriter, .{ .context = {} });416pub const null_writer: NullWriter = .{ .context = {} };
417417
418const NullWriter = Writer(void, error{}, dummyWrite);418const NullWriter = Writer(void, error{}, dummyWrite);
419fn dummyWrite(context: void, data: []const u8) error{}!usize {419fn dummyWrite(context: void, data: []const u8) error{}!usize {
src/Package/Fetch.zig+13-11
...@@ -339,12 +339,12 @@ pub fn run(f: *Fetch) RunError!void {...@@ -339,12 +339,12 @@ pub fn run(f: *Fetch) RunError!void {
339 .path_or_url => |path_or_url| {339 .path_or_url => |path_or_url| {
340 if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {340 if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {
341 var resource: Resource = .{ .dir = dir };341 var resource: Resource = .{ .dir = dir };
342 return runResource(f, path_or_url, &resource, null);342 return f.runResource(path_or_url, &resource, null);
343 } else |dir_err| {343 } else |dir_err| {
344 const file_err = if (dir_err == error.NotDir) e: {344 const file_err = if (dir_err == error.NotDir) e: {
345 if (fs.cwd().openFile(path_or_url, .{})) |file| {345 if (fs.cwd().openFile(path_or_url, .{})) |file| {
346 var resource: Resource = .{ .file = file };346 var resource: Resource = .{ .file = file };
347 return runResource(f, path_or_url, &resource, null);347 return f.runResource(path_or_url, &resource, null);
348 } else |err| break :e err;348 } else |err| break :e err;
349 } else dir_err;349 } else dir_err;
350350
...@@ -356,7 +356,7 @@ pub fn run(f: *Fetch) RunError!void {...@@ -356,7 +356,7 @@ pub fn run(f: *Fetch) RunError!void {
356 };356 };
357 var server_header_buffer: [header_buffer_size]u8 = undefined;357 var server_header_buffer: [header_buffer_size]u8 = undefined;
358 var resource = try f.initResource(uri, &server_header_buffer);358 var resource = try f.initResource(uri, &server_header_buffer);
359 return runResource(f, uri.path, &resource, null);359 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null);
360 }360 }
361 },361 },
362 };362 };
...@@ -418,7 +418,7 @@ pub fn run(f: *Fetch) RunError!void {...@@ -418,7 +418,7 @@ pub fn run(f: *Fetch) RunError!void {
418 );418 );
419 var server_header_buffer: [header_buffer_size]u8 = undefined;419 var server_header_buffer: [header_buffer_size]u8 = undefined;
420 var resource = try f.initResource(uri, &server_header_buffer);420 var resource = try f.initResource(uri, &server_header_buffer);
421 return runResource(f, uri.path, &resource, remote.hash);421 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash);
422}422}
423423
424pub fn deinit(f: *Fetch) void {424pub fn deinit(f: *Fetch) void {
...@@ -897,13 +897,14 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re...@@ -897,13 +897,14 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
897 const arena = f.arena.allocator();897 const arena = f.arena.allocator();
898 const eb = &f.error_bundle;898 const eb = &f.error_bundle;
899899
900 if (ascii.eqlIgnoreCase(uri.scheme, "file")) return .{900 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
901 .file = f.parent_package_root.openFile(uri.path, .{}) catch |err| {901 const path = try uri.path.toRawMaybeAlloc(arena);
902 return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| {
902 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{903 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{
903 f.parent_package_root, uri.path, @errorName(err),904 f.parent_package_root, path, @errorName(err),
904 }));905 }));
905 },906 } };
906 };907 }
907908
908 const http_client = f.job_queue.http_client;909 const http_client = f.job_queue.http_client;
909910
...@@ -920,7 +921,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re...@@ -920,7 +921,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
920 };921 };
921 errdefer req.deinit(); // releases more than memory922 errdefer req.deinit(); // releases more than memory
922923
923 req.send(.{}) catch |err| {924 req.send() catch |err| {
924 return f.fail(f.location_tok, try eb.printString(925 return f.fail(f.location_tok, try eb.printString(
925 "HTTP request failed: {s}",926 "HTTP request failed: {s}",
926 .{@errorName(err)},927 .{@errorName(err)},
...@@ -967,7 +968,8 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re...@@ -967,7 +968,8 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
967 };968 };
968969
969 const want_oid = want_oid: {970 const want_oid = want_oid: {
970 const want_ref = uri.fragment orelse "HEAD";971 const want_ref =
972 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
971 if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}973 if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}
972974
973 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});975 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
src/Package/Fetch/git.zig+26-12
...@@ -540,9 +540,13 @@ pub const Session = struct {...@@ -540,9 +540,13 @@ pub const Session = struct {
540 http_headers_buffer: []u8,540 http_headers_buffer: []u8,
541 ) !CapabilityIterator {541 ) !CapabilityIterator {
542 var info_refs_uri = session.uri;542 var info_refs_uri = session.uri;
543 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });543 {
544 defer allocator.free(info_refs_uri.path);544 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
545 info_refs_uri.query = "service=git-upload-pack";545 defer allocator.free(session_uri_path);
546 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "info/refs" }) };
547 }
548 defer allocator.free(info_refs_uri.path.percent_encoded);
549 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
546 info_refs_uri.fragment = null;550 info_refs_uri.fragment = null;
547551
548 const max_redirects = 3;552 const max_redirects = 3;
...@@ -554,16 +558,18 @@ pub const Session = struct {...@@ -554,16 +558,18 @@ pub const Session = struct {
554 },558 },
555 });559 });
556 errdefer request.deinit();560 errdefer request.deinit();
557 try request.send(.{});561 try request.send();
558 try request.finish();562 try request.finish();
559563
560 try request.wait();564 try request.wait();
561 if (request.response.status != .ok) return error.ProtocolError;565 if (request.response.status != .ok) return error.ProtocolError;
562 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;566 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
563 if (any_redirects_occurred) {567 if (any_redirects_occurred) {
564 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;568 const request_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{request.uri.path});
569 defer allocator.free(request_uri_path);
570 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
565 var new_uri = request.uri;571 var new_uri = request.uri;
566 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];572 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
567 new_uri.query = null;573 new_uri.query = null;
568 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});574 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
569 return error.Redirected;575 return error.Redirected;
...@@ -645,8 +651,12 @@ pub const Session = struct {...@@ -645,8 +651,12 @@ pub const Session = struct {
645 /// Returns an iterator over refs known to the server.651 /// Returns an iterator over refs known to the server.
646 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {652 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
647 var upload_pack_uri = session.uri;653 var upload_pack_uri = session.uri;
648 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });654 {
649 defer allocator.free(upload_pack_uri.path);655 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
656 defer allocator.free(session_uri_path);
657 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
658 }
659 defer allocator.free(upload_pack_uri.path.percent_encoded);
650 upload_pack_uri.query = null;660 upload_pack_uri.query = null;
651 upload_pack_uri.fragment = null;661 upload_pack_uri.fragment = null;
652662
...@@ -681,7 +691,7 @@ pub const Session = struct {...@@ -681,7 +691,7 @@ pub const Session = struct {
681 });691 });
682 errdefer request.deinit();692 errdefer request.deinit();
683 request.transfer_encoding = .{ .content_length = body.items.len };693 request.transfer_encoding = .{ .content_length = body.items.len };
684 try request.send(.{});694 try request.send();
685 try request.writeAll(body.items);695 try request.writeAll(body.items);
686 try request.finish();696 try request.finish();
687697
...@@ -748,8 +758,12 @@ pub const Session = struct {...@@ -748,8 +758,12 @@ pub const Session = struct {
748 http_headers_buffer: []u8,758 http_headers_buffer: []u8,
749 ) !FetchStream {759 ) !FetchStream {
750 var upload_pack_uri = session.uri;760 var upload_pack_uri = session.uri;
751 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });761 {
752 defer allocator.free(upload_pack_uri.path);762 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
763 defer allocator.free(session_uri_path);
764 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
765 }
766 defer allocator.free(upload_pack_uri.path.percent_encoded);
753 upload_pack_uri.query = null;767 upload_pack_uri.query = null;
754 upload_pack_uri.fragment = null;768 upload_pack_uri.fragment = null;
755769
...@@ -786,7 +800,7 @@ pub const Session = struct {...@@ -786,7 +800,7 @@ pub const Session = struct {
786 });800 });
787 errdefer request.deinit();801 errdefer request.deinit();
788 request.transfer_encoding = .{ .content_length = body.items.len };802 request.transfer_encoding = .{ .content_length = body.items.len };
789 try request.send(.{});803 try request.send();
790 try request.writeAll(body.items);804 try request.writeAll(body.items);
791 try request.finish();805 try request.finish();
792806
src/link/SpirV.zig+12-3
...@@ -233,9 +233,18 @@ pub fn flushModule(self: *SpirV, arena: Allocator, prog_node: *std.Progress.Node...@@ -233,9 +233,18 @@ pub fn flushModule(self: *SpirV, arena: Allocator, prog_node: *std.Progress.Node
233 // name if it contains no strange characters is nice for debugging. URI encoding fits the bill.233 // name if it contains no strange characters is nice for debugging. URI encoding fits the bill.
234 // We're using : as separator, which is a reserved character.234 // We're using : as separator, which is a reserved character.
235235
236 const escaped_name = try std.Uri.escapeString(gpa, name.toSlice(&mod.intern_pool));236 try std.Uri.Component.percentEncode(
237 defer gpa.free(escaped_name);237 error_info.writer(),
238 try error_info.writer().print(":{s}", .{escaped_name});238 name.toSlice(&mod.intern_pool),
239 struct {
240 fn isValidChar(c: u8) bool {
241 return switch (c) {
242 0, '%', ':' => false,
243 else => true,
244 };
245 }
246 }.isValidChar,
247 );
239 }248 }
240 try spv.sections.debug_strings.emit(gpa, .OpSourceExtension, .{249 try spv.sections.debug_strings.emit(gpa, .OpSourceExtension, .{
241 .extension = error_info.items,250 .extension = error_info.items,