PranitInitial commit439f83c2 months agoHistory
1286 lines · 45.5 KB · Zig
RawDownload
const std = @import("std");const debug_trace = @import("debug_trace.zig");const display_width = @import("display_width.zig"); pub fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {    if (needle.len == 0) return true;    if (needle.len > haystack.len) return false;     const last_start = haystack.len - needle.len;    var i: usize = 0;    while (i <= last_start) : (i += 1) {        if (std.ascii.eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return true;    }    return false;} pub fn isModelSafeText(text: []const u8) bool {    if (std.mem.findScalar(u8, text, 0) != null) return false;    return std.unicode.utf8ValidateSlice(text);} pub fn utf8BackwardBoundary(text: []const u8, index: usize) usize {    var len = @min(index, text.len);    while (len > 0 and len < text.len and (text[len] & 0b1100_0000) == 0b1000_0000) {        len -= 1;    }    return len;} pub fn utf8ForwardBoundary(text: []const u8, index: usize) usize {    var len = @min(index, text.len);    while (len < text.len and (text[len] & 0b1100_0000) == 0b1000_0000) {        len += 1;    }    return len;} pub const HeadRounding = enum { down, up }; /// Writes text truncated to max_content_bytes as head + marker + tail with/// rune-safe cut points. The caller chooses how the retained budget splits/// between head and tail when it is odd.pub fn writeHeadTailBounded(    writer: *std.Io.Writer,    text: []const u8,    max_content_bytes: usize,    marker: []const u8,    head_rounding: HeadRounding,) !void {    if (text.len <= max_content_bytes) return writer.writeAll(text);    if (max_content_bytes <= marker.len) {        return writer.writeAll(marker[0..max_content_bytes]);    }    const retained_bytes = max_content_bytes - marker.len;    const head_bytes = switch (head_rounding) {        .down => retained_bytes / 2,        .up => (retained_bytes + 1) / 2,    };    const tail_bytes = retained_bytes - head_bytes;    const head_end = utf8BackwardBoundary(text, head_bytes);    const tail_start = utf8ForwardBoundary(text, text.len - tail_bytes);    try writer.writeAll(text[0..head_end]);    try writer.writeAll(marker);    try writer.writeAll(text[tail_start..]);} pub fn utf8PrefixByBytes(text: []const u8, max_bytes: usize) []const u8 {    return text[0..utf8BackwardBoundary(text, max_bytes)];} pub fn normalizeLineEndingsInPlace(bytes: []u8) []u8 {    var read: usize = 0;    var write: usize = 0;    while (read < bytes.len) {        if (bytes[read] == '\r') {            bytes[write] = '\n';            write += 1;            read += 1;            if (read < bytes.len and bytes[read] == '\n') read += 1;            continue;        }        bytes[write] = bytes[read];        write += 1;        read += 1;    }    return bytes[0..write];} pub const EncodedText = struct {    bytes: []u8,    truncated: bool,     pub fn deinit(self: *EncodedText, alloc: std.mem.Allocator) void {        alloc.free(self.bytes);    }}; /// Incrementally applies the same terminal-safe token policy as/// `encodeTerminalSafe`. It retains at most one unfinished UTF-8 token so a/// caller can process bounded file pages without exposing raw bytes.pub const IncrementalTerminalSafeEncoder = struct {    pending: [4]u8 = undefined,    pending_len: usize = 0,     pub fn append(        self: *IncrementalTerminalSafeEncoder,        writer: *std.Io.Writer,        raw: []const u8,    ) !void {        var index: usize = 0;        while (self.pending_len > 0) {            if (try self.writePendingToken(writer)) continue;            if (index == raw.len) return;            self.pending[self.pending_len] = raw[index];            self.pending_len += 1;            index += 1;        }         while (index < raw.len) {            const sequence_len = std.unicode.utf8ByteSequenceLength(raw[index]) catch 1;            if (raw.len - index < sequence_len) {                const remaining = raw[index..];                @memcpy(self.pending[0..remaining.len], remaining);                self.pending_len = remaining.len;                return;            }            try writeTerminalSafeToken(writer, raw, index);            var token_buf: [12]u8 = undefined;            index += terminalSafeToken(raw, index, &token_buf).source_len;        }    }     pub fn finish(self: *IncrementalTerminalSafeEncoder, writer: *std.Io.Writer) !void {        while (self.pending_len > 0) {            try self.emitPendingToken(writer);        }    }     fn writePendingToken(self: *IncrementalTerminalSafeEncoder, writer: *std.Io.Writer) !bool {        const sequence_len = std.unicode.utf8ByteSequenceLength(self.pending[0]) catch 1;        if (self.pending_len < sequence_len) return false;         try self.emitPendingToken(writer);        return true;    }     fn emitPendingToken(self: *IncrementalTerminalSafeEncoder, writer: *std.Io.Writer) !void {        var token_buf: [12]u8 = undefined;        const token = terminalSafeToken(self.pending[0..self.pending_len], 0, &token_buf);        try writer.writeAll(token.bytes);        const remaining = self.pending_len - token.source_len;        if (remaining > 0) {            std.mem.copyForwards(                u8,                self.pending[0..remaining],                self.pending[token.source_len..self.pending_len],            );        }        self.pending_len = remaining;    }}; /// Validates UTF-8 across arbitrary chunks and rejects an incomplete final sequence.pub const IncrementalUtf8Validator = struct {    pending: [4]u8 = undefined,    pending_len: usize = 0,     pub fn append(self: *IncrementalUtf8Validator, bytes: []const u8) error{InvalidUtf8}!void {        var index: usize = 0;        if (self.pending_len > 0) {            const sequence_len = std.unicode.utf8ByteSequenceLength(self.pending[0]) catch return error.InvalidUtf8;            const copied = @min(sequence_len - self.pending_len, bytes.len);            @memcpy(self.pending[self.pending_len .. self.pending_len + copied], bytes[0..copied]);            self.pending_len += copied;            index += copied;            if (self.pending_len < sequence_len) return;            _ = std.unicode.utf8Decode(self.pending[0..sequence_len]) catch return error.InvalidUtf8;            self.pending_len = 0;        }         while (index < bytes.len) {            const sequence_len = std.unicode.utf8ByteSequenceLength(bytes[index]) catch return error.InvalidUtf8;            if (bytes.len - index < sequence_len) {                const remaining = bytes[index..];                @memcpy(self.pending[0..remaining.len], remaining);                self.pending_len = remaining.len;                return;            }            _ = std.unicode.utf8Decode(bytes[index .. index + sequence_len]) catch return error.InvalidUtf8;            index += sequence_len;        }    }     pub fn finish(self: *IncrementalUtf8Validator) error{InvalidUtf8}!void {        if (self.pending_len != 0) return error.InvalidUtf8;    }}; pub const EncodedPathTail = struct {    bytes: []u8,    basename_start: usize,    source_truncated: bool,     pub fn deinit(self: *EncodedPathTail, alloc: std.mem.Allocator) void {        alloc.free(self.bytes);    }}; pub fn encodeTerminalSafePathTail(    alloc: std.mem.Allocator,    raw: []const u8,    max_encoded_bytes: usize,) error{ OutOfMemory, PathBasenameTooLong }!EncodedPathTail {    const basename = std.fs.path.basename(raw);    if (basename.len == 0) return error.PathBasenameTooLong;    const basename_source_start = raw.len - basename.len;     var encoded: std.ArrayList(u8) = .empty;    defer encoded.deinit(alloc);    var boundaries: std.ArrayList(usize) = .empty;    defer boundaries.deinit(alloc);    try boundaries.append(alloc, 0);     var encoded_basename_start: ?usize = null;    var source_index: usize = 0;    while (source_index < raw.len) {        if (source_index == basename_source_start) {            encoded_basename_start = encoded.items.len;        }        var token_buf: [12]u8 = undefined;        const token = terminalSafeToken(raw, source_index, &token_buf);        try encoded.appendSlice(alloc, token.bytes);        source_index += token.source_len;        try boundaries.append(alloc, encoded.items.len);    }    const basename_start = encoded_basename_start orelse        return error.PathBasenameTooLong;    const basename_bytes = encoded.items.len - basename_start;    if (basename_bytes > max_encoded_bytes) return error.PathBasenameTooLong;     if (encoded.items.len <= max_encoded_bytes) {        return .{            .bytes = try encoded.toOwnedSlice(alloc),            .basename_start = basename_start,            .source_truncated = false,        };    }     const marker = "…";    if (marker.len + basename_bytes > max_encoded_bytes) {        return error.PathBasenameTooLong;    }    const suffix_budget = max_encoded_bytes - marker.len;    var suffix_start: ?usize = null;    for (boundaries.items) |boundary| {        if (encoded.items.len - boundary <= suffix_budget) {            suffix_start = boundary;            break;        }    }    const start = suffix_start orelse return error.PathBasenameTooLong;    if (start > basename_start) return error.PathBasenameTooLong;     const output = try alloc.alloc(        u8,        marker.len + encoded.items.len - start,    );    @memcpy(output[0..marker.len], marker);    @memcpy(output[marker.len..], encoded.items[start..]);    return .{        .bytes = output,        .basename_start = marker.len + basename_start - start,        .source_truncated = true,    };} pub fn suffixTerminalSafeByWidth(    encoded: []const u8,    max_width: usize,) []const u8 {    var width = display_width.visibleWidth(encoded);    if (width <= max_width) return encoded;     var start: usize = 0;    while (start < encoded.len and width > max_width) {        const token_len = terminalSafeEncodedTokenLen(encoded[start..]);        const token = encoded[start .. start + token_len];        width -|= display_width.visibleWidth(token);        start += token_len;    }    return encoded[start..];} pub fn prefixTerminalSafeByWidth(    encoded: []const u8,    max_width: usize,) []const u8 {    var width: usize = 0;    var end: usize = 0;    while (end < encoded.len) {        const token_len = terminalSafeEncodedTokenLen(encoded[end..]);        const token_end = @min(encoded.len, end + token_len);        const token = encoded[end..token_end];        const token_width = display_width.visibleWidth(token);        if (width + token_width > max_width) break;        width += token_width;        end = token_end;    }    return encoded[0..end];} fn terminalSafeEncodedTokenLen(encoded: []const u8) usize {    if (encoded.len >= 4 and        encoded[0] == '\\' and        encoded[1] == 'x' and        std.ascii.isHex(encoded[2]) and        std.ascii.isHex(encoded[3]))    {        return 4;    }    if (encoded.len >= 5 and        encoded[0] == '\\' and        encoded[1] == 'u' and        encoded[2] == '{')    {        var index: usize = 3;        var saw_hex = false;        while (index < encoded.len and index < 12) : (index += 1) {            if (encoded[index] == '}') {                if (saw_hex) return index + 1;                break;            }            if (!std.ascii.isHex(encoded[index])) break;            saw_hex = true;        }    }    return @min(        encoded.len,        std.unicode.utf8ByteSequenceLength(encoded[0]) catch 1,    );} pub fn encodeTerminalSafe(    alloc: std.mem.Allocator,    raw: []const u8,    max_encoded_bytes: usize,) error{OutOfMemory}!EncodedText {    const marker = "...";    const marker_len = @min(marker.len, max_encoded_bytes);    const content_limit = max_encoded_bytes - marker_len;    var bytes: std.ArrayList(u8) = .empty;    errdefer bytes.deinit(alloc);     var marker_boundary: usize = 0;    var truncated = false;    var index: usize = 0;    while (index < raw.len) {        var token_buf: [12]u8 = undefined;        const token = terminalSafeToken(raw, index, &token_buf);        const next_len = std.math.add(usize, bytes.items.len, token.bytes.len) catch {            truncated = true;            break;        };        if (next_len > max_encoded_bytes) {            truncated = true;            break;        }        try bytes.appendSlice(alloc, token.bytes);        index += token.source_len;        if (next_len <= content_limit) marker_boundary = next_len;    }    if (truncated) {        bytes.items.len = marker_boundary;        try bytes.appendSlice(alloc, marker[0..marker_len]);    }    return .{        .bytes = try bytes.toOwnedSlice(alloc),        .truncated = truncated,    };} /// Returns true when `raw` is valid printable terminal text and/// `encodeTerminalSafe` would preserve it byte-for-byte.pub fn isTerminalSafe(raw: []const u8) bool {    var index: usize = 0;    while (index < raw.len) {        var token_buf: [12]u8 = undefined;        const token = terminalSafeToken(raw, index, &token_buf);        if (token.source_len != token.bytes.len) return false;        if (!std.mem.eql(u8, raw[index .. index + token.source_len], token.bytes)) return false;        index += token.source_len;    }    return true;} pub fn terminalSafeVisibleWidth(raw: []const u8) usize {    var width: usize = 0;    var index: usize = 0;    while (index < raw.len) {        var token_buf: [12]u8 = undefined;        const token = terminalSafeToken(raw, index, &token_buf);        width += display_width.visibleWidth(token.bytes);        index += token.source_len;    }    return width;} pub fn clippedLabel(buf: []u8, text: []const u8, max_len: usize) []const u8 {    if (buf.len == 0 or max_len == 0) return "";    if (display_width.visibleWidth(text) <= max_len) return text;     const keep_limit = if (max_len > 3) max_len - 3 else 0;    const prefix = display_width.prefixByWidth(text, keep_limit);    const keep = @min(prefix.len, buf.len);    if (keep == 0) return display_width.prefixByWidth(text, max_len);     @memcpy(buf[0..keep], prefix[0..keep]);    if (keep + 3 <= buf.len) {        @memcpy(buf[keep .. keep + 3], "...");        return buf[0 .. keep + 3];    }    return buf[0..keep];} pub fn sanitizeModelText(arena: std.mem.Allocator, text: []const u8) ![]const u8 {    if (isModelSafeText(text)) return text;    return std.fmt.allocPrint(arena, "binary or non-utf8 tool output omitted ({d} bytes)", .{text.len});} pub fn maskSecrets(arena: std.mem.Allocator, text: []const u8) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(arena);    defer out.deinit();    var modified = false;    var i: usize = 0;    while (i < text.len) {        if (findSecretSpan(text, i)) |span| {            modified = true;            try out.writer.writeAll(text[i..span.prefix_end]);            try out.writer.writeAll("[redacted]");            debug_trace.logf("core", "model-facing secret redacted kind={s} bytes={d}", .{ span.kind, span.value_len });            i = span.prefix_end + span.value_len;        } else {            try out.writer.writeByte(text[i]);            i += 1;        }    }    if (!modified) return text;    return try out.toOwnedSlice();} pub fn redactUrlForDisplay(alloc: std.mem.Allocator, raw_url: []const u8) error{OutOfMemory}![]u8 {    var out: std.Io.Writer.Allocating = .init(alloc);    errdefer out.deinit();     const scheme_end = std.mem.find(u8, raw_url, "://");    var query_search_start: usize = 0;    if (scheme_end) |end| {        const authority_start = end + "://".len;        const authority_end = firstDelimiter(raw_url, authority_start, "/?#");        if (std.mem.findScalar(u8, raw_url[authority_start..authority_end], '@')) |at| {            out.writer.writeAll(raw_url[0..authority_start]) catch return error.OutOfMemory;            out.writer.writeAll("[redacted]@") catch return error.OutOfMemory;            out.writer.writeAll(raw_url[authority_start + at + 1 .. authority_end]) catch return error.OutOfMemory;        } else {            out.writer.writeAll(raw_url[0..authority_end]) catch return error.OutOfMemory;        }        query_search_start = authority_end;    }     if (scheme_end == null) query_search_start = 0;    const query_marker = std.mem.findScalarPos(u8, raw_url, query_search_start, '?') orelse {        if (scheme_end == null) {            out.writer.writeAll(raw_url) catch return error.OutOfMemory;        } else {            out.writer.writeAll(raw_url[query_search_start..]) catch return error.OutOfMemory;        }        return out.toOwnedSlice() catch return error.OutOfMemory;    };    if (scheme_end == null) {        out.writer.writeAll(raw_url[0 .. query_marker + 1]) catch return error.OutOfMemory;    } else {        out.writer.writeAll(raw_url[query_search_start .. query_marker + 1]) catch return error.OutOfMemory;    }     const query_end = std.mem.findScalarPos(u8, raw_url, query_marker + 1, '#') orelse raw_url.len;    var segment_start = query_marker + 1;    while (segment_start < query_end) {        const segment_end = std.mem.findScalarPos(u8, raw_url, segment_start, '&') orelse query_end;        const equals = std.mem.findScalarPos(u8, raw_url, segment_start, '=') orelse segment_end;        out.writer.writeAll(raw_url[segment_start..equals]) catch return error.OutOfMemory;        if (equals < segment_end) {            out.writer.writeByte('=') catch return error.OutOfMemory;            if (sensitiveUrlQueryKey(raw_url[segment_start..equals])) {                out.writer.writeAll("[redacted]") catch return error.OutOfMemory;            } else {                out.writer.writeAll(raw_url[equals + 1 .. segment_end]) catch return error.OutOfMemory;            }        }        if (segment_end < query_end) out.writer.writeByte('&') catch return error.OutOfMemory;        segment_start = segment_end + 1;

Lines 1–500 of 1286.