PranitInitial commit439f83c3 months agoHistory
115 lines · 4.0 KB · Zig
RawDownload
const std = @import("std");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig");const url_policy = @import("url_policy.zig"); const Allocator = std.mem.Allocator; const whitespace = " \t\r\n"; pub const Input = struct {    url: []u8,     pub fn deinit(self: *Input, alloc: Allocator) void {        alloc.free(self.url);        self.* = .{ .url = &.{} };    }}; pub fn decode(ctx: tool_dispatch.DispatchContext, args_json: []const u8) tool_dispatch.DispatchError!tool_dispatch.DecodeResult {    var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch {        return .{ .failure = try ctx.allocator.dupe(u8, "web_fetch arguments must be valid JSON") };    };    defer parsed.deinit();     if (parsed.value != .object) {        return .{ .failure = try ctx.allocator.dupe(u8, "web_fetch arguments must be an object") };    }     var it = parsed.value.object.iterator();    while (it.next()) |entry| {        if (std.mem.eql(u8, entry.key_ptr.*, "url")) continue;        return .{ .failure = try std.fmt.allocPrint(ctx.allocator, "web_fetch field \"{s}\" is not allowed", .{entry.key_ptr.*}) };    }     const url_value = parsed.value.object.get("url") orelse {        return .{ .failure = try ctx.allocator.dupe(u8, "web_fetch field \"url\" is required") };    };    if (url_value != .string) {        return .{ .failure = try ctx.allocator.dupe(u8, "web_fetch field \"url\" must be a string") };    }     const input = try ctx.allocator.create(Input);    errdefer ctx.allocator.destroy(input);    const url = try ctx.allocator.dupe(u8, url_value.string);    errdefer ctx.allocator.free(url);    input.* = .{        .url = url,    };    errdefer input.deinit(ctx.allocator);     return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };} pub fn validate(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput) tool_dispatch.DispatchError!?[]u8 {    const input = erased.as(Input);    const trimmed = std.mem.trim(u8, input.url, whitespace);    if (!std.mem.eql(u8, input.url, trimmed)) {        const owned = try ctx.allocator.dupe(u8, trimmed);        ctx.allocator.free(input.url);        input.url = owned;    }     var normalized = url_policy.normalize(ctx.allocator, input.url) catch |err| {        return try ctx.allocator.dupe(u8, urlValidationMessage(err));    };    defer normalized.deinit(ctx.allocator);    if (!std.mem.eql(u8, input.url, normalized.retrieval_url)) {        const owned = try ctx.allocator.dupe(u8, normalized.retrieval_url);        ctx.allocator.free(input.url);        input.url = owned;    }     return null;} pub fn readsOnly(_: tool_dispatch.ToolInput) bool {    return true;} pub fn isIrreversible(_: tool_dispatch.ToolInput) bool {    return false;} fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void {    const input: *Input = @ptrCast(@alignCast(ptr));    input.deinit(alloc);    alloc.destroy(input);} fn urlValidationMessage(err: url_policy.Error) []const u8 {    return switch (err) {        error.EmptyUrl => "web_fetch field \"url\" must not be empty",        error.UrlTooLong => "web_fetch field \"url\" must be at most 2000 bytes",        error.UnsupportedScheme => "web_fetch url must start with http:// or https://",        error.MissingHost => "web_fetch url must include a host",        error.CredentialedUrl => "web_fetch refuses credential-bearing URLs",        error.NonPublicAddress,        error.SingleLabelHost,        => "web_fetch only fetches known public HTTP(S) URLs",        error.MalformedPercentEncoding,        error.PercentEncodedHost,        error.UnicodeHost,        error.ControlByte,        error.InvalidPort,        error.InvalidIpv4,        error.ScopeIdRejected,        error.MalformedHost,        error.MalformedLocation,        error.PortChanged,        error.ProtocolChanged,        error.RequestTargetWhitespace,        error.OutOfMemory,        error.WriteFailed,        => "web_fetch url is malformed",    };}