atproto pds in zig pds.zat.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125const std = @import("std");const zds = @import("zds");const zat = zds.zat;const Io = std.Io;var threaded: Io.Threaded = undefined;pub const std_options_debug_threaded_io: ?*Io.Threaded = if (@import("builtin").is_test) null else &threaded;const cache_dir = "src/internal/web/icons/";
pub fn main(init: std.process.Init) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); // Keep debug I/O alive for Zig's top-level error reporting. threaded = Io.Threaded.init(std.heap.smp_allocator, .{}); const io = threaded.io(); var args = std.process.Args.Iterator.init(init.minimal.args); _ = args.next(); const path = args.next() orelse "config/drawings.json"; const source = try Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(64 * 1024)); const config = try std.json.parseFromSlice(std.json.Value, allocator, source, .{ .duplicate_field_behavior = .@"error" }); const assignments = switch (config.value) { .object => |object| object, else => return error.ExpectedSlotMap, }; if (assignments.count() > zds.internal.web_icons.slots.names.len) return error.TooManySlots; var generated: Io.Writer.Allocating = .init(allocator); try generated.writer.writeAll("// Generated by just drawings; do not edit.\npub const entries = .{\n"); var transport = zat.HttpTransport.initWithUserAgent(io, allocator, "zds-drawings (+https://zat.dev)"); defer transport.deinit(); var resolver = zat.DidResolver.init(io, allocator); defer resolver.deinit(); var iterator = assignments.iterator(); while (iterator.next()) |entry| { const slot = entry.key_ptr.*; if (!zds.internal.web_icons.slots.contains(slot)) return error.UnknownDrawingSlot; const uri_text = zat.json.getString(entry.value_ptr.*, "uri") orelse return error.MissingDrawingUri; const record_cid = zat.json.getString(entry.value_ptr.*, "cid") orelse return error.MissingDrawingCid; const uri = zat.AtUri.parse(uri_text) orelse return error.InvalidDrawingUri; if (!std.mem.eql(u8, uri.collection() orelse "", "tech.waow.doodl.drawing")) return error.NotDoodlDrawing; const did = zat.Did.parse(uri.authority()) orelse return error.UseDidBasedUri; const rkey = uri.rkey() orelse return error.MissingRecordKey; _ = try zat.Cid.fromString(allocator, record_cid); var doc = try resolver.resolve(did); defer doc.deinit(); const endpoint = std.mem.trimEnd(u8, doc.pdsEndpoint() orelse return error.MissingPds, "/"); const parsed_endpoint = try std.Uri.parse(endpoint); if (!std.mem.eql(u8, parsed_endpoint.scheme, "https") or parsed_endpoint.user != null or parsed_endpoint.password != null or parsed_endpoint.query != null or parsed_endpoint.fragment != null) return error.InvalidPdsEndpoint; const record_url = try std.fmt.allocPrint(allocator, "{s}/xrpc/com.atproto.repo.getRecord?repo={s}&collection=tech.waow.doodl.drawing&rkey={s}&cid={s}", .{ endpoint, try percentEncode(allocator, uri.authority()), try percentEncode(allocator, rkey), record_cid }); const record = try transport.fetch(.{ .url = record_url, .max_response_size = 128 * 1024, .redirect_behavior = .not_allowed }); if (record.status != .ok) return error.DrawingFetchFailed; const parsed = try std.json.parseFromSlice(std.json.Value, allocator, record.body, .{}); if (!std.mem.eql(u8, zat.json.getString(parsed.value, "cid") orelse "", record_cid)) return error.DrawingRevisionMismatch; const value = zat.json.getPath(parsed.value, "value") orelse return error.MissingRecordValue; if (!std.mem.eql(u8, zat.json.getString(value, "$type") orelse "", "tech.waow.doodl.drawing")) return error.NotDoodlDrawing; const encoded = try zat.cbor.encodeAlloc(allocator, try zat.cbor.fromJson(allocator, value)); const computed = try (try zat.Cid.forDagCbor(allocator, encoded)).toString(allocator); if (!std.mem.eql(u8, computed, record_cid)) return error.DrawingContentMismatch; if (!std.mem.eql(u8, zat.json.getString(value, "image.mimeType") orelse "", "image/png")) return error.UnsupportedDrawingImage; const blob_cid = zat.json.getString(value, "image.ref.$link") orelse return error.MissingImageCid; _ = try zat.Cid.fromString(allocator, blob_cid); const blob_url = try std.fmt.allocPrint(allocator, "{s}/xrpc/com.atproto.sync.getBlob?did={s}&cid={s}", .{ endpoint, try percentEncode(allocator, uri.authority()), blob_cid }); const blob = try transport.fetch(.{ .url = blob_url, .max_response_size = 1_000_000, .redirect_behavior = .not_allowed }); if (blob.status != .ok) return error.ImageFetchFailed; try verifyImage(allocator, blob.body, blob_cid); const file = try std.fmt.allocPrint(allocator, "{s}{s}.png", .{ cache_dir, blob_cid }); const temporary = try std.fmt.allocPrint(allocator, "{s}.tmp", .{file}); try Io.Dir.cwd().writeFile(io, .{ .sub_path = temporary, .data = blob.body }); try Io.Dir.cwd().rename(temporary, Io.Dir.cwd(), file, io); try generated.writer.print(" .{{ .slot = {f}, .uri = {f}, .record_cid = {f}, .cid = {f}, .png = @embedFile(\"{s}.png\") }},\n", .{ std.json.fmt(slot, .{}), std.json.fmt(uri_text, .{}), std.json.fmt(record_cid, .{}), std.json.fmt(blob_cid, .{}), blob_cid }); std.debug.print("cached {s}\n", .{slot}); } try generated.writer.writeAll("};\n"); // Publish the manifest only after every assignment has been verified and cached. try Io.Dir.cwd().writeFile(io, .{ .sub_path = cache_dir ++ "generated.zig.tmp", .data = generated.written() }); try Io.Dir.cwd().rename(cache_dir ++ "generated.zig.tmp", Io.Dir.cwd(), cache_dir ++ "generated.zig", io);}
fn verifyImage(allocator: std.mem.Allocator, data: []const u8, expected: []const u8) !void { const cid = try zat.Cid.create(allocator, 1, 0x55, 0x12, data); defer allocator.free(cid.raw); const actual = try cid.toString(allocator); defer allocator.free(actual); if (!std.mem.eql(u8, actual, expected)) return error.ImageContentMismatch; try validatePng(data);}
fn validatePng(data: []const u8) !void { if (data.len < 33 or !std.mem.startsWith(u8, data, "\x89PNG\r\n\x1a\n") or !std.mem.eql(u8, data[12..16], "IHDR")) return error.InvalidPng; const width = std.mem.readInt(u32, data[16..20], .big); const height = std.mem.readInt(u32, data[20..24], .big); if (width == 0 or height == 0 or width > 4096 or height > 4096) return error.InvalidImageDimensions;}
fn percentEncode(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { var out: Io.Writer.Allocating = .init(allocator); for (value) |c| { if (std.ascii.isAlphanumeric(c) or c == '-' or c == '_' or c == '.' or c == '~') try out.writer.writeByte(c) else try out.writer.print("%{X:0>2}", .{c}); } return out.toOwnedSlice();}
test "image integrity rejects changed bytes and non-PNG content" { const allocator = std.testing.allocator; const data = "not an image"; const cid = try zat.Cid.create(allocator, 1, 0x55, 0x12, data); defer allocator.free(cid.raw); const expected = try cid.toString(allocator); defer allocator.free(expected); try std.testing.expectError(error.InvalidPng, verifyImage(allocator, data, expected)); try std.testing.expectError(error.ImageContentMismatch, verifyImage(allocator, "changed", expected));}
test "PNG dimensions are bounded before packaging" { var header = [_]u8{0} ** 33; @memcpy(header[0..8], "\x89PNG\r\n\x1a\n"); @memcpy(header[12..16], "IHDR"); std.mem.writeInt(u32, header[16..20], 1024, .big); std.mem.writeInt(u32, header[20..24], 1024, .big); try validatePng(&header); std.mem.writeInt(u32, header[16..20], 4097, .big); try std.testing.expectError(error.InvalidImageDimensions, validatePng(&header)); std.mem.writeInt(u32, header[16..20], 0, .big); try std.testing.expectError(error.InvalidImageDimensions, validatePng(&header));}