An HTTP/1.1 server for zig
0

Configure Feed

Select the types of activity you want to include in your feed.

http.zig / src / url.zig
8.8 kB 269 lines
1const std = @import("std"); 2const builtin = @import("builtin"); 3const metrics = @import("metrics.zig"); 4 5const Allocator = std.mem.Allocator; 6 7const backend_supports_vectors = switch (builtin.zig_backend) { 8 .stage2_llvm, .stage2_c => true, 9 else => false, 10}; 11 12pub const Url = struct { 13 raw: []const u8 = "", 14 path: []const u8 = "", 15 query: []const u8 = "", 16 17 pub fn parse(raw: []const u8) Url { 18 var path = raw; 19 var query: []const u8 = ""; 20 21 if (std.mem.indexOfScalar(u8, raw, '?')) |index| { 22 path = raw[0..index]; 23 query = raw[index + 1 ..]; 24 } 25 26 return .{ 27 .raw = raw, 28 .path = path, 29 .query = query, 30 }; 31 } 32 33 // the special "*" url, which is valid in HTTP OPTIONS request. 34 pub fn star() Url { 35 return .{ 36 .raw = "*", 37 .path = "*", 38 .query = "", 39 }; 40 } 41 42 pub const UnescapeResult = struct { 43 // Set to the value, whether or not it required unescaped. 44 value: []const u8, 45 46 // true if the value WAS unescaped AND placed in buffer 47 buffered: bool, 48 }; 49 // std.Url.unescapeString has 2 problems 50 // First, it doesn't convert '+' -> ' ' 51 // Second, it _always_ allocates a new string even if nothing needs to 52 // be unescaped 53 // When we _have_ to unescape a key or value, we'll try to store the new 54 // value in our static buffer (if we have space), else we'll fallback to 55 // allocating memory in the arena. 56 pub fn unescape(allocator: Allocator, buffer: []u8, input: []const u8) !UnescapeResult { 57 var has_plus = false; 58 var unescaped_len = input.len; 59 60 var in_i: usize = 0; 61 while (in_i < input.len) { 62 const b = input[in_i]; 63 if (b == '%') { 64 if (in_i + 2 >= input.len or !std.ascii.isHex(input[in_i + 1]) or !std.ascii.isHex(input[in_i + 2])) { 65 return error.InvalidEscapeSequence; 66 } 67 in_i += 3; 68 unescaped_len -= 2; 69 } else if (b == '+') { 70 has_plus = true; 71 in_i += 1; 72 } else { 73 in_i += 1; 74 } 75 } 76 77 // no encoding, and no plus. nothing to unescape 78 if (unescaped_len == input.len and !has_plus) { 79 return .{ .value = input, .buffered = false }; 80 } 81 82 var out = buffer; 83 var buffered = true; 84 if (buffer.len < unescaped_len) { 85 out = try allocator.alloc(u8, unescaped_len); 86 metrics.allocUnescape(unescaped_len); 87 buffered = false; 88 } 89 90 in_i = 0; 91 for (0..unescaped_len) |i| { 92 const b = input[in_i]; 93 if (b == '%') { 94 out[i] = decodeHex(input[in_i + 1]) << 4 | decodeHex(input[in_i + 2]); 95 in_i += 3; 96 } else if (b == '+') { 97 out[i] = ' '; 98 in_i += 1; 99 } else { 100 out[i] = b; 101 in_i += 1; 102 } 103 } 104 105 return .{ .value = out[0..unescaped_len], .buffered = buffered }; 106 } 107 108 pub fn isValid(url: []const u8) bool { 109 var rest = url; 110 111 if (comptime backend_supports_vectors) { 112 if (comptime std.simd.suggestVectorLength(u8)) |vector_len| { 113 while (rest.len >= vector_len) { 114 const block: @Vector(vector_len, u8) = rest[0..vector_len].*; 115 if (@reduce(.Min, block) < 32 or @reduce(.Max, block) > 126) { 116 return false; 117 } 118 rest = rest[vector_len..]; 119 } 120 } 121 } 122 123 for (rest) |c| { 124 if (c < 32 or c > 126) { 125 return false; 126 } 127 } 128 129 return true; 130 } 131}; 132 133/// converts ascii to unsigned int of appropriate size 134pub fn asUint(comptime string: anytype) std.meta.Int( 135 .unsigned, 136 @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 137) { 138 const byteLength = @sizeOf(@TypeOf(string.*)) - 1; 139 const expectedType = *const [byteLength:0]u8; 140 if (@TypeOf(string) != expectedType) { 141 @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string))); 142 } 143 144 return @bitCast(@as(*const [byteLength]u8, string).*); 145} 146 147const HEX_DECODE_ARRAY = blk: { 148 var all: ['f' - '0' + 1]u8 = undefined; 149 for ('0'..('9' + 1)) |b| all[b - '0'] = b - '0'; 150 for ('A'..('F' + 1)) |b| all[b - '0'] = b - 'A' + 10; 151 for ('a'..('f' + 1)) |b| all[b - '0'] = b - 'a' + 10; 152 break :blk all; 153}; 154 155inline fn decodeHex(char: u8) u8 { 156 return @as([*]const u8, @ptrFromInt((@intFromPtr(&HEX_DECODE_ARRAY) - @as(usize, '0'))))[char]; 157} 158 159const t = @import("t.zig"); 160test "url: parse" { 161 { 162 // absolute root 163 const url = Url.parse("/"); 164 try t.expectString("/", url.raw); 165 try t.expectString("/", url.path); 166 try t.expectString("", url.query); 167 } 168 169 { 170 // absolute path 171 const url = Url.parse("/a/bc/def"); 172 try t.expectString("/a/bc/def", url.raw); 173 try t.expectString("/a/bc/def", url.path); 174 try t.expectString("", url.query); 175 } 176 177 { 178 // absolute root with query 179 const url = Url.parse("/?over=9000"); 180 try t.expectString("/?over=9000", url.raw); 181 try t.expectString("/", url.path); 182 try t.expectString("over=9000", url.query); 183 } 184 185 { 186 // absolute root with empty query 187 const url = Url.parse("/?"); 188 try t.expectString("/?", url.raw); 189 try t.expectString("/", url.path); 190 try t.expectString("", url.query); 191 } 192 193 { 194 // absolute path with query 195 const url = Url.parse("/hello/teg?duncan=idaho&ghanima=atreides"); 196 try t.expectString("/hello/teg?duncan=idaho&ghanima=atreides", url.raw); 197 try t.expectString("/hello/teg", url.path); 198 try t.expectString("duncan=idaho&ghanima=atreides", url.query); 199 } 200} 201 202test "url: unescape" { 203 var arena = std.heap.ArenaAllocator.init(t.allocator); 204 const allocator = arena.allocator(); 205 defer arena.deinit(); 206 207 var buffer: [10]u8 = undefined; 208 209 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%")); 210 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%a")); 211 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%1")); 212 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "123%45%6")); 213 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%zzzzz")); 214 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%0\xff")); 215 216 var res = try Url.unescape(allocator, &buffer, "a+b"); 217 try t.expectString("a b", res.value); 218 try t.expectEqual(true, res.buffered); 219 220 res = try Url.unescape(allocator, &buffer, "a%20b"); 221 try t.expectString("a b", res.value); 222 try t.expectEqual(true, res.buffered); 223 224 const input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad"; 225 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad"; 226 res = try Url.unescape(allocator, &buffer, input); 227 try t.expectString(expected, res.value); 228 try t.expectEqual(false, res.buffered); 229} 230 231test "url: isValid" { 232 var input: [600]u8 = undefined; 233 for ([_]u8{ ' ', 'a', 'Z', '~' }) |c| { 234 @memset(&input, c); 235 for (0..input.len) |i| { 236 try t.expectEqual(true, Url.isValid(input[0..i])); 237 } 238 } 239 240 var r = t.getRandom(); 241 const random = r.random(); 242 243 for ([_]u8{ 31, 128, 0, 255 }) |c| { 244 for (1..input.len) |i| { 245 var slice = input[0..i]; 246 const idx = random.uintAtMost(usize, slice.len - 1); 247 slice[idx] = c; 248 try t.expectEqual(false, Url.isValid(slice)); 249 slice[idx] = 'a'; // revert this index to a valid value 250 } 251 } 252} 253 254test "toUint" { 255 const ASCII_x = @as(u8, @bitCast([1]u8{'x'})); 256 const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' })); 257 const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' })); 258 const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' })); 259 260 try t.expectEqual(ASCII_x, asUint("x")); 261 try t.expectEqual(ASCII_ab, asUint("ab")); 262 try t.expectEqual(ASCII_xyz, asUint("xyz")); 263 try t.expectEqual(ASCII_abcd, asUint("abcd")); 264 265 try t.expectEqual(u8, @TypeOf(asUint("x"))); 266 try t.expectEqual(u16, @TypeOf(asUint("ab"))); 267 try t.expectEqual(u24, @TypeOf(asUint("xyz"))); 268 try t.expectEqual(u32, @TypeOf(asUint("abcd"))); 269}