mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00

This commit changes a LOT of areas of the code to use decl literals instead of redundantly referring to the type. These changes were mostly driven by some regex searches and then manual adjustment on a case-by-case basis. I almost certainly missed quite a few places where decl literals could be used, but this is a good first step in converting things, and other instances can be addressed when they're discovered. I tested GLFW+Metal and building the framework on macOS and tested a GTK build on Linux, so I'm 99% sure I didn't introduce any syntax errors or other problems with this. (fingers crossed)
628 lines
21 KiB
Zig
628 lines
21 KiB
Zig
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
|
|
/// client. It is designed and created by Sentry but is an open format
|
|
/// in that it is publicly documented and can be used by any system. This
|
|
/// lets us utilize the Sentry client for crash capture but also gives us
|
|
/// the opportunity to migrate to another system if we need to, and doesn't
|
|
/// force any user or developer to use Sentry the SaaS if they don't want
|
|
/// to.
|
|
///
|
|
/// This struct implements reading the envelope format (writing is not needed
|
|
/// currently but can be added later). It is incomplete; I only implemented
|
|
/// what I needed at the time.
|
|
pub const Envelope = struct {
|
|
/// 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.
|
|
headers: std.json.ObjectMap,
|
|
|
|
/// The items in the envelope in the order they're encoded.
|
|
items: std.ArrayListUnmanaged(Item),
|
|
|
|
/// Parse an envelope from a reader.
|
|
///
|
|
/// The full envelope must fit in memory for this to succeed. This
|
|
/// will always copy the data from the reader into memory, even if the
|
|
/// reader is already in-memory (i.e. a FixedBufferStream). This
|
|
/// simplifies memory lifetimes at the expense of a copy, but envelope
|
|
/// parsing in our use case is not a hot path.
|
|
pub fn parse(
|
|
alloc_gpa: Allocator,
|
|
reader: anytype,
|
|
) !Envelope {
|
|
// We use an arena allocator to read from reader. We pair this
|
|
// with `alloc_if_needed` when parsing json to allow the json
|
|
// to reference the arena-allocated memory if it can. That way both
|
|
// our temp and perm memory is part of the same arena. This slightly
|
|
// bloats our memory requirements but reduces allocations.
|
|
var arena = std.heap.ArenaAllocator.init(alloc_gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// Parse our elements. We do this outside of the struct assignment
|
|
// below to avoid the issue where order matters in struct assignment.
|
|
const headers = try parseHeader(alloc, reader);
|
|
const items = try parseItems(alloc, reader);
|
|
|
|
return .{
|
|
.headers = headers,
|
|
.items = items,
|
|
.arena = arena,
|
|
};
|
|
}
|
|
|
|
fn parseHeader(
|
|
alloc: Allocator,
|
|
reader: anytype,
|
|
) !std.json.ObjectMap {
|
|
var buf: std.ArrayListUnmanaged(u8) = .{};
|
|
reader.streamUntilDelimiter(
|
|
buf.writer(alloc),
|
|
'\n',
|
|
1024 * 1024, // 1MB, arbitrary choice
|
|
) catch |err| switch (err) {
|
|
// Envelope can be header-only.
|
|
error.EndOfStream => {},
|
|
else => |v| return v,
|
|
};
|
|
|
|
const value = try std.json.parseFromSliceLeaky(
|
|
std.json.Value,
|
|
alloc,
|
|
buf.items,
|
|
.{ .allocate = .alloc_if_needed },
|
|
);
|
|
|
|
return switch (value) {
|
|
.object => |map| map,
|
|
else => error.EnvelopeMalformedHeaders,
|
|
};
|
|
}
|
|
|
|
fn parseItems(
|
|
alloc: Allocator,
|
|
reader: anytype,
|
|
) !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(
|
|
alloc: Allocator,
|
|
reader: anytype,
|
|
) !?Item {
|
|
// Get the next item which must start with a header.
|
|
var buf: std.ArrayListUnmanaged(u8) = .{};
|
|
reader.streamUntilDelimiter(
|
|
buf.writer(alloc),
|
|
'\n',
|
|
1024 * 1024, // 1MB, arbitrary choice
|
|
) catch |err| switch (err) {
|
|
error.EndOfStream => return null,
|
|
else => |v| return v,
|
|
};
|
|
|
|
// Parse the header JSON
|
|
const headers: std.json.ObjectMap = headers: {
|
|
const line = std.mem.trim(u8, buf.items, " \t");
|
|
if (line.len == 0) return null;
|
|
|
|
const value = try std.json.parseFromSliceLeaky(
|
|
std.json.Value,
|
|
alloc,
|
|
line,
|
|
.{ .allocate = .alloc_if_needed },
|
|
);
|
|
|
|
break :headers switch (value) {
|
|
.object => |map| map,
|
|
else => return error.EnvelopeItemMalformedHeaders,
|
|
};
|
|
};
|
|
|
|
// Get the event type
|
|
const typ: ItemType = if (headers.get("type")) |v| switch (v) {
|
|
.string => |str| std.meta.stringToEnum(
|
|
ItemType,
|
|
str,
|
|
) orelse .unknown,
|
|
else => return error.EnvelopeItemTypeMissing,
|
|
} else return error.EnvelopeItemTypeMissing;
|
|
|
|
// Get the payload length. The length is not required. If the length
|
|
// is not specified then it is the next line ending in `\n`.
|
|
const len_: ?u64 = if (headers.get("length")) |v| switch (v) {
|
|
.integer => |int| std.math.cast(
|
|
u64,
|
|
int,
|
|
) orelse return error.EnvelopeItemLengthMalformed,
|
|
else => return error.EnvelopeItemLengthMalformed,
|
|
} else null;
|
|
|
|
// Get the payload
|
|
const payload: []const u8 = if (len_) |len| payload: {
|
|
// The payload length is specified so read the exact length.
|
|
var payload = std.ArrayList(u8).init(alloc);
|
|
defer payload.deinit();
|
|
for (0..len) |_| {
|
|
const byte = reader.readByte() catch |err| switch (err) {
|
|
error.EndOfStream => return error.EnvelopeItemPayloadTooShort,
|
|
else => return err,
|
|
};
|
|
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.
|
|
var payload = std.ArrayList(u8).init(alloc);
|
|
defer payload.deinit();
|
|
reader.streamUntilDelimiter(
|
|
payload.writer(),
|
|
'\n',
|
|
1024 * 1024 * 50, // 50MB, arbitrary choice
|
|
) catch |err| switch (err) {
|
|
error.EndOfStream => return error.EnvelopeItemPayloadTooShort,
|
|
else => |v| return v,
|
|
};
|
|
break :payload try payload.toOwnedSlice();
|
|
};
|
|
|
|
return .{ .encoded = .{
|
|
.headers = headers,
|
|
.type = typ,
|
|
.payload = payload,
|
|
} };
|
|
}
|
|
|
|
pub fn deinit(self: *Envelope) void {
|
|
self.arena.deinit();
|
|
}
|
|
|
|
/// 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 writer.writeByte('\n');
|
|
|
|
// Write each item
|
|
const alloc = self.allocator();
|
|
for (self.items.items, 0..) |*item, idx| {
|
|
if (idx > 0) try writer.writeByte('\n');
|
|
|
|
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);
|
|
}
|
|
}
|
|
};
|
|
|
|
/// The various item types that can be in an envelope. This is a point
|
|
/// in time snapshot of the types that are known whenever this is edited.
|
|
/// Event types can be introduced at any time and unknown types will
|
|
/// take the "unknown" enum value.
|
|
///
|
|
/// https://develop.sentry.dev/sdk/envelopes/#data-model
|
|
pub const ItemType = enum {
|
|
/// Special event type for when the item type is unknown.
|
|
unknown,
|
|
|
|
/// Documented event types
|
|
event,
|
|
transaction,
|
|
attachment,
|
|
session,
|
|
sessions,
|
|
statsd,
|
|
metric_meta,
|
|
user_feedback,
|
|
client_report,
|
|
replay_event,
|
|
replay_recording,
|
|
profile,
|
|
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,
|
|
|
|
/// Convert the item to an encoded item. This modify the item
|
|
/// in place.
|
|
pub fn encode(
|
|
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
|
|
/// it is an encoded item or not.
|
|
pub fn itemType(self: Item) ItemType {
|
|
return switch (self) {
|
|
.encoded => |v| v.type,
|
|
.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 .decode(
|
|
alloc,
|
|
encoded,
|
|
) },
|
|
else => return error.UnsupportedType,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// 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,
|
|
};
|
|
|
|
/// 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.
|
|
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,
|
|
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,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// 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 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.
|
|
.whitespace = .minified,
|
|
};
|
|
|
|
test "Envelope parse" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var fbs = std.io.fixedBufferStream(
|
|
\\{}
|
|
);
|
|
var v = try Envelope.parse(alloc, fbs.reader());
|
|
defer v.deinit();
|
|
}
|
|
|
|
test "Envelope parse 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();
|
|
|
|
try testing.expectEqual(@as(usize, 1), v.items.items.len);
|
|
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;
|
|
|
|
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();
|
|
|
|
try testing.expectEqual(@as(usize, 1), v.items.items.len);
|
|
try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type);
|
|
}
|
|
|
|
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;
|
|
|
|
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.serialize(output.writer());
|
|
|
|
try testing.expectEqualStrings(
|
|
\\{}
|
|
, std.mem.trim(u8, output.items, "\n"));
|
|
}
|
|
|
|
test "Envelope serialize 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.serialize(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"));
|
|
}
|
|
|
|
// // 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;
|
|
// }
|
|
// }
|