From ee6fd80746d0539616809225fe19ca18d6c66ecc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Sep 2024 21:24:05 -0700 Subject: [PATCH 1/5] crash: envelope encoder --- src/crash/sentry_envelope.zig | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 00c7ac931..b88122302 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -211,6 +211,29 @@ pub const Envelope = struct { pub fn deinit(self: *Envelope) void { self.arena.deinit(); } + + /// Encode the envelope to the given writer. + pub fn encode(self: *const Envelope, writer: anytype) !void { + const json_opts: std.json.StringifyOptions = .{ + // This is the default but I want to be explicit beacuse its + // VERY important for the correctness of the envelope. This is + // the only whitespace type in std.json that doesn't emit newlines. + // All JSON headers in the envelope must be on a single line. + .whitespace = .minified, + }; + + // Header line first + try std.json.stringify(std.json.Value{ .object = self.headers }, json_opts, writer); + try writer.writeByte('\n'); + + // Write each item + for (self.items, 0..) |*item, idx| { + if (idx > 0) try writer.writeByte('\n'); + try std.json.stringify(std.json.Value{ .object = item.headers }, json_opts, writer); + try writer.writeByte('\n'); + try writer.writeAll(item.payload); + } + } }; /// The various item types that can be in an envelope. This is a point @@ -282,3 +305,45 @@ test "Envelope parse end in new line" { try testing.expectEqual(@as(usize, 1), v.items.len); try testing.expectEqual(ItemType.session, v.items[0].type); } + +test "Envelope encode empty" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream( + \\{} + ); + var v = try Envelope.parse(alloc, fbs.reader()); + defer v.deinit(); + + var output = std.ArrayList(u8).init(alloc); + defer output.deinit(); + try v.encode(output.writer()); + + try testing.expectEqualStrings( + \\{} + , std.mem.trim(u8, output.items, "\n")); +} + +test "Envelope encode session" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + ); + var v = try Envelope.parse(alloc, fbs.reader()); + defer v.deinit(); + + var output = std.ArrayList(u8).init(alloc); + defer output.deinit(); + try v.encode(output.writer()); + + try testing.expectEqualStrings( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + , std.mem.trim(u8, output.items, "\n")); +} From 6788aefe9510a77d1c43f7228a469655b3a43557 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 08:48:01 -0700 Subject: [PATCH 2/5] crash: data model for encoded vs decoded items --- src/crash/sentry.zig | 2 +- src/crash/sentry_envelope.zig | 148 ++++++++++++++++++++++++---------- 2 files changed, 107 insertions(+), 43 deletions(-) diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 3995ffee7..a44d3d612 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -271,7 +271,7 @@ pub const Transport = struct { fn shouldDiscard(envelope: *const crash.Envelope) !bool { // If we have an event item then we're good. for (envelope.items) |item| { - if (item.type == .event) return false; + if (item.itemType() == .event) return false; } return true; diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index b88122302..a65efdd3d 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -16,24 +16,8 @@ const Allocator = std.mem.Allocator; /// currently but can be added later). It is incomplete; I only implemented /// what I needed at the time. pub const Envelope = struct { - // Developer note: this struct is really geared towards decoding an - // already-encoded envelope vs. building up an envelope from rich - // data types. I think it can be used for both I just didn't have - // the latter need. - // - // If I were to make that ability more enjoyable I'd probably change - // Item below a tagged union of either an "EncodedItem" (which is the - // current Item type) or a "DecodedItem" which is a union(ItemType) - // to its rich data type. This would allow the user to cheaply append - // items to the envelope without paying the encoding cost until - // serialization time. - // - // The way it is now, the user has to encode every entry as they build - // the envelope, which is probably fine but I wanted to write this down - // for my future self or some future contributor since it is fresh - // in my mind. Cheers. - - /// The arena that the envelope is allocated in. + /// The arena that the envelope is allocated in. All items are welcome + /// to use this allocator for their data, which is freed on deinit. arena: std.heap.ArenaAllocator, /// The headers of the envelope decoded into a json ObjectMap. @@ -42,15 +26,6 @@ pub const Envelope = struct { /// The items in the envelope in the order they're encoded. items: []const Item, - /// An encoded item. It is "encoded" in the sense that the payload - /// is a byte slice. The headers are "decoded" into a json ObjectMap - /// but that's still a pretty low-level representation. - pub const Item = struct { - headers: std.json.ObjectMap, - type: ItemType, - payload: []const u8, - }; - /// Parse an envelope from a reader. /// /// The full envelope must fit in memory for this to succeed. This @@ -201,11 +176,11 @@ pub const Envelope = struct { break :payload try payload.toOwnedSlice(); }; - return .{ + return .{ .encoded = .{ .headers = headers, .type = typ, .payload = payload, - }; + } }; } pub fn deinit(self: *Envelope) void { @@ -214,14 +189,6 @@ pub const Envelope = struct { /// Encode the envelope to the given writer. pub fn encode(self: *const Envelope, writer: anytype) !void { - const json_opts: std.json.StringifyOptions = .{ - // This is the default but I want to be explicit beacuse its - // VERY important for the correctness of the envelope. This is - // the only whitespace type in std.json that doesn't emit newlines. - // All JSON headers in the envelope must be on a single line. - .whitespace = .minified, - }; - // Header line first try std.json.stringify(std.json.Value{ .object = self.headers }, json_opts, writer); try writer.writeByte('\n'); @@ -229,9 +196,7 @@ pub const Envelope = struct { // Write each item for (self.items, 0..) |*item, idx| { if (idx > 0) try writer.writeByte('\n'); - try std.json.stringify(std.json.Value{ .object = item.headers }, json_opts, writer); - try writer.writeByte('\n'); - try writer.writeAll(item.payload); + try item.encode(writer); } } }; @@ -262,6 +227,105 @@ pub const ItemType = enum { check_in, }; +/// An item in the envelope. An item can be either in an encoded +/// or decoded state. The encoded state lets us parse the envelope +/// more cheaply since we can defer the full decoding of the item +/// until we need it. +/// +/// The decoded state is more ergonomic to work with and lets us +/// easily build up new items and defer encoding until serialization +/// time. +pub const Item = union(enum) { + encoded: EncodedItem, + attachment: Attachment, + + pub fn encode( + self: Item, + writer: anytype, + ) !void { + switch (self) { + inline .encoded, + .attachment, + => |v| try v.encode(writer), + } + } + + /// Returns the type of item represented here, whether + /// it is an encoded item or not. + pub fn itemType(self: Item) ItemType { + return switch (self) { + .encoded => |v| v.type, + .attachment => .attachment, + }; + } +}; + +/// An encoded item. It is "encoded" in the sense that the payload +/// is a byte slice. The headers are "decoded" into a json ObjectMap +/// but that's still a pretty low-level representation. +pub const EncodedItem = struct { + headers: std.json.ObjectMap, + type: ItemType, + payload: []const u8, + + pub fn encode( + self: EncodedItem, + writer: anytype, + ) !void { + try std.json.stringify( + std.json.Value{ .object = self.headers }, + json_opts, + writer, + ); + try writer.writeByte('\n'); + try writer.writeAll(self.payload); + } +}; + +/// An arbitrary file attachment. +/// +/// https://develop.sentry.dev/sdk/envelopes/#attachment +pub const Attachment = struct { + /// "filename" field is the name of the uploaded file without + /// a path component. + filename: []const u8, + + /// A special "type" associated with the attachment. This + /// is documented on the Sentry website. In the future we should + /// make this an enum. + type: ?[]const u8 = null, + + /// Additional headers for the attachment. + header_extra: ObjectMapUnmanaged = .{}, + + /// The data for the attachment. + payload: []const u8, + + pub fn encode( + self: Attachment, + writer: anytype, + ) !void { + _ = self; + _ = writer; + @panic("TODO"); + } +}; + +/// Same as std.json.ObjectMap but unmanaged. This lets us store +/// them alongside all our items without the overhead of duplicated +/// allocators. Additional, items do not own their own memory so this +/// makes it clear that deinit of an item will not free the memory. +pub const ObjectMapUnmanaged = std.StringArrayHashMapUnmanaged(std.json.Value); + +/// The options we must use for serialization. +const json_opts: std.json.StringifyOptions = .{ + // This is the default but I want to be explicit beacuse its + // VERY important for the correctness of the envelope. This is + // the only whitespace type in std.json that doesn't emit newlines. + // All JSON headers in the envelope must be on a single line. + .whitespace = .minified, +}; + test "Envelope parse" { const testing = std.testing; const alloc = testing.allocator; @@ -286,7 +350,7 @@ test "Envelope parse session" { defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.len); - try testing.expectEqual(ItemType.session, v.items[0].type); + try testing.expectEqual(ItemType.session, v.items[0].encoded.type); } test "Envelope parse end in new line" { @@ -303,7 +367,7 @@ test "Envelope parse end in new line" { defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.len); - try testing.expectEqual(ItemType.session, v.items[0].type); + try testing.expectEqual(ItemType.session, v.items[0].encoded.type); } test "Envelope encode empty" { From 646b8a4cdd6680e34e271c1e67efe61824cbf96c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 09:10:03 -0700 Subject: [PATCH 3/5] crash: parse attachments from sentry envelope --- src/crash/sentry.zig | 2 +- src/crash/sentry_envelope.zig | 225 +++++++++++++++++++++++++++------- 2 files changed, 179 insertions(+), 48 deletions(-) diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index a44d3d612..1a4be6841 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -270,7 +270,7 @@ pub const Transport = struct { fn shouldDiscard(envelope: *const crash.Envelope) !bool { // If we have an event item then we're good. - for (envelope.items) |item| { + for (envelope.items.items) |item| { if (item.itemType() == .event) return false; } diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index a65efdd3d..69dab398c 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -2,6 +2,8 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const log = std.log.scoped(.sentry_envelope); + /// The Sentry Envelope format: https://develop.sentry.dev/sdk/envelopes/ /// /// The envelope is our primary crash report format since use the Sentry @@ -24,7 +26,7 @@ pub const Envelope = struct { headers: std.json.ObjectMap, /// The items in the envelope in the order they're encoded. - items: []const Item, + items: std.ArrayListUnmanaged(Item), /// Parse an envelope from a reader. /// @@ -89,11 +91,14 @@ pub const Envelope = struct { fn parseItems( alloc: Allocator, reader: anytype, - ) ![]const Item { - var items = std.ArrayList(Item).init(alloc); - defer items.deinit(); - while (try parseOneItem(alloc, reader)) |item| try items.append(item); - return try items.toOwnedSlice(); + ) !std.ArrayListUnmanaged(Item) { + var items: std.ArrayListUnmanaged(Item) = .{}; + errdefer items.deinit(alloc); + while (try parseOneItem(alloc, reader)) |item| { + try items.append(alloc, item); + } + + return items; } fn parseOneItem( @@ -187,16 +192,42 @@ pub const Envelope = struct { self.arena.deinit(); } - /// Encode the envelope to the given writer. - pub fn encode(self: *const Envelope, writer: anytype) !void { + /// The arena allocator associated with this envelope + pub fn allocator(self: *Envelope) Allocator { + return self.arena.allocator(); + } + + /// Serialize the envelope to the given writer. + /// + /// This will convert all decoded items to encoded items and + /// therefore may allocate. + pub fn serialize( + self: *Envelope, + writer: anytype, + ) !void { // Header line first - try std.json.stringify(std.json.Value{ .object = self.headers }, json_opts, writer); + try std.json.stringify( + std.json.Value{ .object = self.headers }, + json_opts, + writer, + ); try writer.writeByte('\n'); // Write each item - for (self.items, 0..) |*item, idx| { + const alloc = self.allocator(); + for (self.items.items, 0..) |*item, idx| { if (idx > 0) try writer.writeByte('\n'); - try item.encode(writer); + + const encoded = try item.encode(alloc); + assert(item.* == .encoded); + + try std.json.stringify( + std.json.Value{ .object = encoded.headers }, + json_opts, + writer, + ); + try writer.writeByte('\n'); + try writer.writeAll(encoded.payload); } } }; @@ -239,15 +270,18 @@ pub const Item = union(enum) { encoded: EncodedItem, attachment: Attachment, + /// Convert the item to an encoded item. This modify the item + /// in place. pub fn encode( - self: Item, - writer: anytype, - ) !void { - switch (self) { - inline .encoded, - .attachment, - => |v| try v.encode(writer), - } + self: *Item, + alloc: Allocator, + ) !EncodedItem { + const result: EncodedItem = switch (self.*) { + .encoded => |v| return v, + .attachment => |*v| try v.encode(alloc), + }; + self.* = .{ .encoded = result }; + return result; } /// Returns the type of item represented here, whether @@ -258,6 +292,43 @@ pub const Item = union(enum) { .attachment => .attachment, }; } + + pub const DecodeError = Allocator.Error || error{ + MissingRequiredField, + InvalidFieldType, + UnsupportedType, + }; + + /// Decode the item if it is encoded. This will modify itself. + /// If the item is already decoded this does nothing. + /// + /// The allocator argument should be an arena-style allocator, + /// typically the allocator associated with the Envelope. + /// + /// If the decoding fails because the item is in an invalid + /// state (i.e. its missing a required field) then this will + /// error but the encoded item will remain unmodified. This + /// allows the caller to handle the error without corrupting the + /// envelope. + /// + /// If decoding fails, the allocator may still allocate so the + /// allocator should be an arena-style allocator. + pub fn decode(self: *Item, alloc: Allocator) DecodeError!void { + // Get our encoded item. If we're not encoded we're done. + const encoded: EncodedItem = switch (self.*) { + .encoded => |v| v, + else => return, + }; + + // Decode the item. + self.* = switch (encoded.type) { + .attachment => .{ .attachment = try Attachment.decode( + alloc, + encoded, + ) }, + else => return error.UnsupportedType, + }; + } }; /// An encoded item. It is "encoded" in the sense that the payload @@ -267,19 +338,6 @@ pub const EncodedItem = struct { headers: std.json.ObjectMap, type: ItemType, payload: []const u8, - - pub fn encode( - self: EncodedItem, - writer: anytype, - ) !void { - try std.json.stringify( - std.json.Value{ .object = self.headers }, - json_opts, - writer, - ); - try writer.writeByte('\n'); - try writer.writeAll(self.payload); - } }; /// An arbitrary file attachment. @@ -296,18 +354,58 @@ pub const Attachment = struct { type: ?[]const u8 = null, /// Additional headers for the attachment. - header_extra: ObjectMapUnmanaged = .{}, + headers_extra: ObjectMapUnmanaged = .{}, /// The data for the attachment. payload: []const u8, + pub fn decode( + alloc: Allocator, + item: EncodedItem, + ) Item.DecodeError!Attachment { + _ = alloc; + + return .{ + .filename = if (item.headers.get("filename")) |v| switch (v) { + .string => |str| str, + else => return error.InvalidFieldType, + } else return error.MissingRequiredField, + + .type = if (item.headers.get("attachment_type")) |v| switch (v) { + .string => |str| str, + else => return error.InvalidFieldType, + } else null, + + .headers_extra = item.headers.unmanaged, + .payload = item.payload, + }; + } + pub fn encode( - self: Attachment, - writer: anytype, - ) !void { - _ = self; - _ = writer; - @panic("TODO"); + self: *Attachment, + alloc: Allocator, + ) !EncodedItem { + try self.headers_extra.put( + alloc, + "filename", + .{ .string = self.filename }, + ); + + if (self.type) |v| { + try self.headers_extra.put( + alloc, + "attachment_type", + .{ .string = v }, + ); + } else { + _ = self.headers_extra.swapRemove("attachment_type"); + } + + return .{ + .headers = self.headers_extra.promote(alloc), + .type = .attachment, + .payload = self.payload, + }; } }; @@ -349,8 +447,8 @@ test "Envelope parse session" { var v = try Envelope.parse(alloc, fbs.reader()); defer v.deinit(); - try testing.expectEqual(@as(usize, 1), v.items.len); - try testing.expectEqual(ItemType.session, v.items[0].encoded.type); + try testing.expectEqual(@as(usize, 1), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); } test "Envelope parse end in new line" { @@ -366,11 +464,44 @@ test "Envelope parse end in new line" { var v = try Envelope.parse(alloc, fbs.reader()); defer v.deinit(); - try testing.expectEqual(@as(usize, 1), v.items.len); - try testing.expectEqual(ItemType.session, v.items[0].encoded.type); + try testing.expectEqual(@as(usize, 1), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); } -test "Envelope encode empty" { +test "Envelope parse attachment" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream( + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, fbs.reader()); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 1), v.items.items.len); + + var item = &v.items.items[0]; + try testing.expectEqual(ItemType.attachment, item.encoded.type); + try item.decode(v.allocator()); + try testing.expect(item.* == .attachment); + try testing.expectEqualStrings("test.txt", item.attachment.filename); + + // Serialization test + { + var output = std.ArrayList(u8).init(alloc); + defer output.deinit(); + try v.serialize(output.writer()); + try testing.expectEqualStrings( + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + , std.mem.trim(u8, output.items, "\n")); + } +} + +test "Envelope serialize empty" { const testing = std.testing; const alloc = testing.allocator; @@ -382,14 +513,14 @@ test "Envelope encode empty" { var output = std.ArrayList(u8).init(alloc); defer output.deinit(); - try v.encode(output.writer()); + try v.serialize(output.writer()); try testing.expectEqualStrings( \\{} , std.mem.trim(u8, output.items, "\n")); } -test "Envelope encode session" { +test "Envelope serialize session" { const testing = std.testing; const alloc = testing.allocator; @@ -403,7 +534,7 @@ test "Envelope encode session" { var output = std.ArrayList(u8).init(alloc); defer output.deinit(); - try v.encode(output.writer()); + try v.serialize(output.writer()); try testing.expectEqualStrings( \\{} From 4acbdbc03865211c488192749166869e380dc633 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 09:23:31 -0700 Subject: [PATCH 4/5] typos --- src/crash/sentry_envelope.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 69dab398c..805faafe0 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -417,7 +417,7 @@ pub const ObjectMapUnmanaged = std.StringArrayHashMapUnmanaged(std.json.Value); /// The options we must use for serialization. const json_opts: std.json.StringifyOptions = .{ - // This is the default but I want to be explicit beacuse its + // This is the default but I want to be explicit because its // VERY important for the correctness of the envelope. This is // the only whitespace type in std.json that doesn't emit newlines. // All JSON headers in the envelope must be on a single line. From b2696ee6fa89af30c7035505df25d69d6be80bea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 09:30:25 -0700 Subject: [PATCH 5/5] crash: envelope parsing handles multiple payloads with length properly --- src/crash/sentry_envelope.zig | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 805faafe0..70eb99f51 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -165,6 +165,15 @@ pub const Envelope = struct { }; try payload.append(byte); } + + // The next byte must be a newline. + if (reader.readByte()) |byte| { + if (byte != '\n') return error.EnvelopeItemPayloadNoNewline; + } else |err| switch (err) { + error.EndOfStream => {}, + else => return err, + } + break :payload try payload.toOwnedSlice(); } else payload: { // The payload is the next line ending in `\n`. It is required. @@ -451,6 +460,44 @@ test "Envelope parse session" { try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); } +test "Envelope parse multiple" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, fbs.reader()); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 2), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); + try testing.expectEqual(ItemType.attachment, v.items.items[1].encoded.type); +} + +test "Envelope parse multiple no length" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream( + \\{} + \\{"type":"session"} + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, fbs.reader()); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 2), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); + try testing.expectEqual(ItemType.attachment, v.items.items[1].encoded.type); +} + test "Envelope parse end in new line" { const testing = std.testing; const alloc = testing.allocator; @@ -542,3 +589,39 @@ test "Envelope serialize session" { \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} , std.mem.trim(u8, output.items, "\n")); } + +// // Uncomment this test if you want to extract a minidump file from an +// // existing envelope. This is useful for getting new test contents. +// test "Envelope extract mdmp" { +// const testing = std.testing; +// const alloc = testing.allocator; +// +// var fbs = std.io.fixedBufferStream(@embedFile("in.crash")); +// var v = try Envelope.parse(alloc, fbs.reader()); +// defer v.deinit(); +// +// try testing.expect(v.items.items.len > 0); +// for (v.items.items, 0..) |*item, i| { +// if (item.encoded.type != .attachment) { +// log.warn("ignoring item type={} i={}", .{ item.encoded.type, i }); +// continue; +// } +// +// try item.decode(v.allocator()); +// const attach = item.attachment; +// const attach_type = attach.type orelse { +// log.warn("attachment missing type i={}", .{i}); +// continue; +// }; +// if (!std.mem.eql(u8, attach_type, "event.minidump")) { +// log.warn("ignoring attachment type={s} i={}", .{ attach_type, i }); +// continue; +// } +// +// log.warn("found minidump i={}", .{i}); +// var f = try std.fs.cwd().createFile("out.mdmp", .{}); +// defer f.close(); +// try f.writer().writeAll(attach.payload); +// return; +// } +// }