diff --git a/src/aws.zig b/src/aws.zig index 0aa09ed..9cb804e 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -1412,14 +1412,14 @@ fn typeForField(comptime T: type, comptime field_name: []const u8) !type { test "custom serialization for map objects" { const allocator = std.testing.allocator; - var buffer = std.ArrayList(u8).init(allocator); + var buffer = std.Io.Writer.Allocating.init(allocator); defer buffer.deinit(); var tags = try std.ArrayList(@typeInfo(try typeForField(services.lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 2); - defer tags.deinit(); + defer tags.deinit(allocator); tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); tags.appendAssumeCapacity(.{ .key = "Baz", .value = "Qux" }); const req = services.lambda.TagResourceRequest{ .resource = "hello", .tags = tags.items }; - try std.json.stringify(req, .{ .whitespace = .indent_4 }, buffer.writer()); + try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); const parsed_body = try std.json.parseFromSlice(struct { Resource: []const u8, @@ -1427,7 +1427,7 @@ test "custom serialization for map objects" { Foo: []const u8, Baz: []const u8, }, - }, testing.allocator, buffer.items, .{}); + }, testing.allocator, buffer.written(), .{}); defer parsed_body.deinit(); try testing.expectEqualStrings("hello", parsed_body.value.Resource); @@ -1439,7 +1439,7 @@ test "proper serialization for kms" { // Github issue #8 // https://github.com/elerch/aws-sdk-for-zig/issues/8 const allocator = std.testing.allocator; - var buffer = std.ArrayList(u8).init(allocator); + var buffer = std.Io.Writer.Allocating.init(allocator); defer buffer.deinit(); const req = services.kms.encrypt.Request{ .encryption_algorithm = "SYMMETRIC_DEFAULT", @@ -1451,7 +1451,7 @@ test "proper serialization for kms" { .dry_run = false, .grant_tokens = &[_][]const u8{}, }; - try std.json.stringify(req, .{ .whitespace = .indent_4 }, buffer.writer()); + try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); { const parsed_body = try std.json.parseFromSlice(struct { @@ -1461,7 +1461,7 @@ test "proper serialization for kms" { GrantTokens: [][]const u8, EncryptionAlgorithm: []const u8, DryRun: bool, - }, testing.allocator, buffer.items, .{}); + }, testing.allocator, buffer.written(), .{}); defer parsed_body.deinit(); try testing.expectEqualStrings("42", parsed_body.value.KeyId); @@ -1471,7 +1471,7 @@ test "proper serialization for kms" { try testing.expectEqual(false, parsed_body.value.DryRun); } - var buffer_null = std.ArrayList(u8).init(allocator); + var buffer_null = std.Io.Writer.Allocating.init(allocator); defer buffer_null.deinit(); const req_null = services.kms.encrypt.Request{ .encryption_algorithm = "SYMMETRIC_DEFAULT", @@ -1483,7 +1483,7 @@ test "proper serialization for kms" { .grant_tokens = &[_][]const u8{}, }; - try std.json.stringify(req_null, .{ .whitespace = .indent_4 }, buffer_null.writer()); + try buffer_null.writer.print("{f}", .{std.json.fmt(req_null, .{ .whitespace = .indent_4 })}); { const parsed_body = try std.json.parseFromSlice(struct { @@ -1493,7 +1493,7 @@ test "proper serialization for kms" { GrantTokens: [][]const u8, EncryptionAlgorithm: []const u8, DryRun: bool, - }, testing.allocator, buffer_null.items, .{}); + }, testing.allocator, buffer_null.written(), .{}); defer parsed_body.deinit(); try testing.expectEqualStrings("42", parsed_body.value.KeyId); @@ -1541,8 +1541,8 @@ test "REST Json v1 serializes lists in queries" { } test "REST Json v1 buildpath substitutes" { const allocator = std.testing.allocator; - var al = std.ArrayList([]const u8).init(allocator); - defer al.deinit(); + var al = std.ArrayList([]const u8){}; + defer al.deinit(allocator); const svs = Services(.{.lambda}){}; const request = svs.lambda.list_functions.Request{ .max_items = 1, @@ -1554,8 +1554,8 @@ test "REST Json v1 buildpath substitutes" { } test "REST Json v1 buildpath handles restricted characters" { const allocator = std.testing.allocator; - var al = std.ArrayList([]const u8).init(allocator); - defer al.deinit(); + var al = std.ArrayList([]const u8){}; + defer al.deinit(allocator); const svs = Services(.{.lambda}){}; const request = svs.lambda.list_functions.Request{ .marker = ":", @@ -1571,7 +1571,7 @@ test "basic json request serialization" { const request = svs.dynamo_db.list_tables.Request{ .limit = 1, }; - var buffer = std.ArrayList(u8).init(allocator); + var buffer = std.Io.Writer.Allocating.init(allocator); defer buffer.deinit(); // The transformer needs to allocate stuff out of band, but we @@ -1583,13 +1583,13 @@ test "basic json request serialization" { // for a boxed member with no observable difference." But we're // seeing a lot of differences here between spec and reality // - try std.json.stringify(request, .{ .whitespace = .indent_4 }, buffer.writer()); + try buffer.writer.print("{f}", .{std.json.fmt(request, .{ .whitespace = .indent_4 })}); try std.testing.expectEqualStrings( \\{ \\ "ExclusiveStartTableName": null, \\ "Limit": 1 \\} - , buffer.items); + , buffer.written()); } test "layer object only" { const TestResponse = struct { @@ -2291,7 +2291,7 @@ test "rest_json_1_work_with_lambda: lambda tagResource (only), to excercise zig const options = try test_harness.start(); const lambda = (Services(.{.lambda}){}).lambda; var tags = try std.ArrayList(@typeInfo(try typeForField(lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 1); - defer tags.deinit(); + defer tags.deinit(allocator); tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); const req = services.lambda.tag_resource.Request{ .resource = "arn:aws:lambda:us-west-2:550620852718:function:awsome-lambda-LambdaStackawsomeLambda", .tags = tags.items }; const call = try Request(lambda.tag_resource).call(req, options); @@ -2674,7 +2674,7 @@ test "jsonStringify: structure + enums" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const request_json = try std.json.stringifyAlloc(std.testing.allocator, request, .{}); + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); defer std.testing.allocator.free(request_json); const parsed = try std.json.parseFromSlice(struct { @@ -2699,7 +2699,7 @@ test "jsonStringify: strings" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const request_json = try std.json.stringifyAlloc(std.testing.allocator, request, .{}); + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); defer std.testing.allocator.free(request_json); try testing.expectEqualStrings("{\"arn\":\"1234\"}", request_json); @@ -2721,7 +2721,7 @@ test "jsonStringify" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const request_json = try std.json.stringifyAlloc(std.testing.allocator, request, .{}); + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); defer std.testing.allocator.free(request_json); const json_parsed = try std.json.parseFromSlice(struct { @@ -2748,7 +2748,7 @@ test "jsonStringify nullable object" { }, }; - const request_json = try std.json.stringifyAlloc(std.testing.allocator, request, .{}); + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); defer std.testing.allocator.free(request_json); const json_parsed = try std.json.parseFromSlice(struct { @@ -2774,7 +2774,7 @@ test "jsonStringify nullable object" { .ciphertext_blob = "bar", }; - const request_json = try std.json.stringifyAlloc(std.testing.allocator, request, .{}); + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); defer std.testing.allocator.free(request_json); const json_parsed = try std.json.parseFromSlice(struct { diff --git a/src/aws_signing.zig b/src/aws_signing.zig index f2f6cae..26cd031 100644 --- a/src/aws_signing.zig +++ b/src/aws_signing.zig @@ -348,10 +348,10 @@ pub fn freeSignedRequest(allocator: std.mem.Allocator, request: *base.Request, c pub const credentialsFn = *const fn ([]const u8) ?Credentials; -pub fn verifyServerRequest(allocator: std.mem.Allocator, request: *std.http.Server.Request, request_body_reader: anytype, credentials_fn: credentialsFn) !bool { +pub fn verifyServerRequest(allocator: std.mem.Allocator, request: *std.http.Server.Request, credentials_fn: credentialsFn) !bool { var unverified_request = try UnverifiedRequest.init(allocator, request); defer unverified_request.deinit(); - return verify(allocator, unverified_request, request_body_reader, credentials_fn); + return verify(allocator, unverified_request, credentials_fn); } pub const UnverifiedRequest = struct { @@ -359,17 +359,19 @@ pub const UnverifiedRequest = struct { target: []const u8, method: std.http.Method, allocator: std.mem.Allocator, + raw: *std.http.Server.Request, pub fn init(allocator: std.mem.Allocator, request: *std.http.Server.Request) !UnverifiedRequest { - var al = std.ArrayList(std.http.Header).init(allocator); - defer al.deinit(); + var al = std.ArrayList(std.http.Header){}; + defer al.deinit(allocator); var it = request.iterateHeaders(); - while (it.next()) |h| try al.append(h); + while (it.next()) |h| try al.append(allocator, h); return .{ .target = request.head.target, .method = request.head.method, - .headers = try al.toOwnedSlice(), + .headers = try al.toOwnedSlice(allocator), .allocator = allocator, + .raw = request, }; } @@ -387,7 +389,7 @@ pub const UnverifiedRequest = struct { } }; -pub fn verify(allocator: std.mem.Allocator, request: UnverifiedRequest, request_body_reader: anytype, credentials_fn: credentialsFn) !bool { +pub fn verify(allocator: std.mem.Allocator, request: UnverifiedRequest, credentials_fn: credentialsFn) !bool { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const aa = arena.allocator(); @@ -420,7 +422,6 @@ pub fn verify(allocator: std.mem.Allocator, request: UnverifiedRequest, request_ return verifyParsedAuthorization( aa, request, - request_body_reader, credential.?, signed_headers.?, signature.?, @@ -431,7 +432,6 @@ pub fn verify(allocator: std.mem.Allocator, request: UnverifiedRequest, request_ fn verifyParsedAuthorization( allocator: std.mem.Allocator, request: UnverifiedRequest, - request_body_reader: anytype, credential: []const u8, signed_headers: []const u8, signature: []const u8, @@ -494,7 +494,8 @@ fn verifyParsedAuthorization( .content_type = request.getFirstHeaderValue("content-type").?, }; signed_request.query = request.target[signed_request.path.len..]; // TODO: should this be +1? query here would include '?' - signed_request.body = try request_body_reader.readAllAlloc(allocator, std.math.maxInt(usize)); + // TODO: This is almost certainly not what we want here long term, but will get tests working + signed_request.body = try request.raw.server.reader.in.allocRemaining(allocator, .unlimited); defer allocator.free(signed_request.body); signed_request = try signRequest(allocator, signed_request, config); defer freeSignedRequest(allocator, &signed_request, config); @@ -1010,13 +1011,13 @@ test "canonical query" { test "canonical headers" { const allocator = std.testing.allocator; var headers = try std.ArrayList(std.http.Header).initCapacity(allocator, 5); - defer headers.deinit(); - try headers.append(.{ .name = "Host", .value = "iam.amazonaws.com" }); - try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" }); - try headers.append(.{ .name = "User-Agent", .value = "This header should be skipped" }); - try headers.append(.{ .name = "My-header1", .value = " a b c " }); - try headers.append(.{ .name = "X-Amz-Date", .value = "20150830T123600Z" }); - try headers.append(.{ .name = "My-header2", .value = " \"a b c\" " }); + defer headers.deinit(allocator); + try headers.append(allocator, .{ .name = "Host", .value = "iam.amazonaws.com" }); + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" }); + try headers.append(allocator, .{ .name = "User-Agent", .value = "This header should be skipped" }); + try headers.append(allocator, .{ .name = "My-header1", .value = " a b c " }); + try headers.append(allocator, .{ .name = "X-Amz-Date", .value = "20150830T123600Z" }); + try headers.append(allocator, .{ .name = "My-header2", .value = " \"a b c\" " }); const expected = \\content-type:application/x-www-form-urlencoded; charset=utf-8 \\host:iam.amazonaws.com @@ -1035,12 +1036,12 @@ test "canonical headers" { test "canonical request" { const allocator = std.testing.allocator; var headers = try std.ArrayList(std.http.Header).initCapacity(allocator, 5); - defer headers.deinit(); - try headers.append(.{ .name = "User-agent", .value = "c sdk v1.0" }); + defer headers.deinit(allocator); + try headers.append(allocator, .{ .name = "User-agent", .value = "c sdk v1.0" }); // In contrast to AWS CRT (aws-c-auth), we add the date as part of the // signing operation. They add it as part of the canonicalization - try headers.append(.{ .name = "X-Amz-Date", .value = "20150830T123600Z" }); - try headers.append(.{ .name = "Host", .value = "example.amazonaws.com" }); + try headers.append(allocator, .{ .name = "X-Amz-Date", .value = "20150830T123600Z" }); + try headers.append(allocator, .{ .name = "Host", .value = "example.amazonaws.com" }); const req = base.Request{ .path = "/", .method = "GET", @@ -1095,10 +1096,10 @@ test "can sign" { const allocator = std.testing.allocator; var headers = try std.ArrayList(std.http.Header).initCapacity(allocator, 5); - defer headers.deinit(); - try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" }); - try headers.append(.{ .name = "Content-Length", .value = "13" }); - try headers.append(.{ .name = "Host", .value = "example.amazonaws.com" }); + defer headers.deinit(allocator); + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" }); + try headers.append(allocator, .{ .name = "Content-Length", .value = "13" }); + try headers.append(allocator, .{ .name = "Host", .value = "example.amazonaws.com" }); const req = base.Request{ .path = "/", .query = "", @@ -1165,25 +1166,25 @@ test "can verify server request" { "X-Amz-Date: 20230908T170252Z\r\n" ++ "x-amz-content-sha256: fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9\r\n" ++ "Authorization: AWS4-HMAC-SHA256 Credential=ACCESS/20230908/us-west-2/s3/aws4_request, SignedHeaders=accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class, Signature=fcc43ce73a34c9bd1ddf17e8a435f46a859812822f944f9eeb2aabcd64b03523\r\n\r\nbar"; - var read_buffer: [1024]u8 = undefined; - @memcpy(read_buffer[0..req.len], req); + var reader = std.Io.Reader.fixed(req); var server: std.http.Server = .{ - .connection = undefined, - .state = .ready, - .read_buffer = &read_buffer, - .read_buffer_len = req.len, - .next_request_start = 0, + .out = undefined, // We're not sending a response here + .reader = .{ + .in = &reader, + .interface = undefined, + .state = .ready, + .max_head_len = req.len, + }, }; var request: std.http.Server.Request = .{ .server = &server, - .head_end = req.len - 3, - .head = try std.http.Server.Request.Head.parse(read_buffer[0 .. req.len - 3]), - .reader_state = undefined, + .head = undefined, + .head_buffer = &.{}, }; + // I think we need a request.receiveHead() call here // std.testing.log_level = .debug; - var fbs = std.io.fixedBufferStream("bar"); - try std.testing.expect(try verifyServerRequest(allocator, &request, fbs.reader(), struct { + try std.testing.expect(try verifyServerRequest(allocator, &request, struct { cred: Credentials, const Self = @This(); @@ -1221,22 +1222,24 @@ test "can verify server request without x-amz-content-sha256" { const req_data = head ++ body; var read_buffer: [2048]u8 = undefined; @memcpy(read_buffer[0..req_data.len], req_data); + var reader = std.Io.Reader.fixed(&read_buffer); var server: std.http.Server = .{ - .connection = undefined, - .state = .ready, - .read_buffer = &read_buffer, - .read_buffer_len = req_data.len, - .next_request_start = 0, + .out = undefined, // We're not sending a response here + .reader = .{ + .interface = undefined, + .in = &reader, + .state = .ready, + .max_head_len = 1024, + }, }; var request: std.http.Server.Request = .{ .server = &server, - .head_end = head.len, - .head = try std.http.Server.Request.Head.parse(read_buffer[0..head.len]), - .reader_state = undefined, + .head = undefined, + .head_buffer = &.{}, }; { - var h = std.ArrayList(std.http.Header).init(allocator); - defer h.deinit(); + var h = try std.ArrayList(std.http.Header).initCapacity(allocator, 4); + defer h.deinit(allocator); const signed_headers = &[_][]const u8{ "content-type", "host", "x-amz-date", "x-amz-target" }; var it = request.iterateHeaders(); while (it.next()) |source| { @@ -1245,7 +1248,7 @@ test "can verify server request without x-amz-content-sha256" { match = std.ascii.eqlIgnoreCase(s, source.name); if (match) break; } - if (match) try h.append(.{ .name = source.name, .value = source.value }); + if (match) try h.append(allocator, .{ .name = source.name, .value = source.value }); } const req = base.Request{ .path = "/", @@ -1282,9 +1285,7 @@ test "can verify server request without x-amz-content-sha256" { } { // verification - var fis = std.io.fixedBufferStream(body[0..]); - - try std.testing.expect(try verifyServerRequest(allocator, &request, fis.reader(), struct { + try std.testing.expect(try verifyServerRequest(allocator, &request, struct { cred: Credentials, const Self = @This(); diff --git a/src/url.zig b/src/url.zig index 656bc1a..f0eed35 100644 --- a/src/url.zig +++ b/src/url.zig @@ -103,60 +103,72 @@ pub fn encodeInternal( return rc; } -fn testencode(allocator: std.mem.Allocator, expected: []const u8, value: anytype, comptime options: EncodingOptions) !void { - const ValidationWriter = struct { - const Self = @This(); - pub const Writer = std.io.Writer(*Self, Error, write); - pub const Error = error{ - TooMuchData, - DifferentData, - }; - - expected_remaining: []const u8, - - fn init(exp: []const u8) Self { - return .{ .expected_remaining = exp }; - } - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - fn write(self: *Self, bytes: []const u8) Error!usize { - // std.debug.print("{s}\n", .{bytes}); - if (self.expected_remaining.len < bytes.len) { - std.log.warn( - \\====== expected this output: ========= - \\{s} - \\======== instead found this: ========= - \\{s} - \\====================================== - , .{ - self.expected_remaining, - bytes, - }); - return error.TooMuchData; - } - if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) { - std.log.warn( - \\====== expected this output: ========= - \\{s} - \\======== instead found this: ========= - \\{s} - \\====================================== - , .{ - self.expected_remaining[0..bytes.len], - bytes, - }); - return error.DifferentData; - } - self.expected_remaining = self.expected_remaining[bytes.len..]; - return bytes.len; - } +const ValidationWriter = struct { + const Self = @This(); + pub const Writer = std.io.Writer(*Self, Error, write); + pub const Error = error{ + TooMuchData, + DifferentData, }; + expected_remaining: []const u8, + writer: std.Io.Writer, + + fn init(exp: []const u8) Self { + return .{ + .expected_remaining = exp, + .writer = .{ + .buffer = &.{}, + .vtable = &.{ + .drain = drain, + }, + }, + }; + } + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + if (splat > 0) @panic("No splat"); + const self: *ValidationWriter = @fieldParentPtr("writer", w); + + var bytes: usize = 0; + for (data) |d| bytes += try self.write(d); + return bytes; + } + + fn write(self: *Self, bytes: []const u8) std.Io.Writer.Error!usize { + if (self.expected_remaining.len < bytes.len) { + std.log.warn( + \\====== expected this output: ========= + \\{s} + \\======== instead found this: ========= + \\{s} + \\====================================== + , .{ + self.expected_remaining, + bytes, + }); + return error.WriteFailed; + } + if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) { + std.log.warn( + \\====== expected this output: ========= + \\{s} + \\======== instead found this: ========= + \\{s} + \\====================================== + , .{ + self.expected_remaining[0..bytes.len], + bytes, + }); + return error.WriteFailed; + } + self.expected_remaining = self.expected_remaining[bytes.len..]; + return bytes.len; + } +}; +fn testencode(allocator: std.mem.Allocator, expected: []const u8, value: anytype, comptime options: EncodingOptions) !void { var vos = ValidationWriter.init(expected); - try encode(allocator, value, vos.writer(), options); + try encode(allocator, value, &vos.writer, options); if (vos.expected_remaining.len > 0) return error.NotEnoughData; } @@ -238,17 +250,17 @@ test "can urlencode an EC2 Filter" { }, .{}, ) catch |err| { - var al = std.ArrayList(u8).init(std.testing.allocator); - defer al.deinit(); + var aw = std.Io.Writer.Allocating.init(std.testing.allocator); + defer aw.deinit(); try encode( std.testing.allocator, Request{ .filters = @constCast(&[_]Filter{.{ .name = "foo", .values = @constCast(&[_][]const u8{"bar"}) }}), }, - al.writer(), + &aw.writer, .{}, ); - std.log.warn("Error found. Full encoding is '{s}'", .{al.items}); + std.log.warn("Error found. Full encoding is '{s}'", .{aw.written()}); return err; }; }