//! JSON-string escape helpers — extracted so the broker server (server.zig)
//! and any future formatter that splices substrate-controlled bytes into a
//! JSON response can share the same escape discipline AND be unit-tested
//! independently of the broker's build target.
//!
//! ## Why a dedicated module
//!
//! `server.zig` is built as an executable, not a test target — its private
//! helpers cannot be exercised by `zig build test`. The G2 review-wave-2
//! finding flagged a real bug: per-attr values pushed by the Linux backends
//! into `inspect` responses were spliced into the JSON without escaping,
//! producing invalid JSON whenever a sysfs value legitimately contained
//! `"`, `\`, or a control byte. By living in its own module, the escape
//! routine is reachable from `build.zig`'s `test` step and pinned by the
//! test block at the bottom of this file.
//!
//! ## Escape set
//!
//! We escape the JSON-spec minimum required to make the output parseable
//! by any conforming JSON parser:
//!   * `"` → `\"`     (closes the string)
//!   * `\` → `\\`     (escape char itself)
//!   * `\n` `\r` `\t` `\b` `\f` → short forms (`\n`, `\r`, `\t`, `\b`, `\f`)
//!   * any other byte < 0x20 → `\u00XX`
//!   * bytes >= 0x20 (including >= 0x80) pass through as-is — the wire is
//!     byte-oriented and the SDK's `json.loads` accepts non-strict UTF-8
//!     (consumers that care about strictness can post-process).
//!
//! ## Overflow discipline
//!
//! On buffer overflow the helper returns `null` rather than truncating
//! mid-escape. The caller must decide whether to set a `truncated` flag,
//! emit an outer error response, or bail. Returning `null` (rather than
//! a partial pos) means a caller that ignores the return value gets a
//! compile error — there's no "looked like it worked" silent corruption.

const std = @import("std");

/// Append the JSON-escaped form of `s` to `out` starting at `pos`. Returns
/// the new pos on success, or `null` if `out[pos..]` was not large enough
/// to hold the full escaped form. On `null` return, `out` is left in an
/// indeterminate state from `pos` onward — the caller must NOT commit a
/// partial write (e.g., do not advance an outer `count` or emit a closing
/// `"`); discard the buffer or treat the whole response as truncated.
pub fn appendEscaped(out: []u8, pos: usize, s: []const u8) ?usize {
    var p = pos;
    for (s) |b| {
        // First branch — the canonical short escapes from RFC 8259 §7.
        // Listed explicitly (not as a single `if (b < 0x20)`) because the
        // short forms are smaller on the wire AND more readable in a
        // human reading a JSON trace.
        const escape: ?[]const u8 = switch (b) {
            '"' => "\\\"",
            '\\' => "\\\\",
            '\n' => "\\n",
            '\r' => "\\r",
            '\t' => "\\t",
            0x08 => "\\b",
            0x0C => "\\f",
            else => null,
        };
        if (escape) |esc| {
            if (p + esc.len > out.len) return null;
            @memcpy(out[p..][0..esc.len], esc);
            p += esc.len;
            continue;
        }
        if (b < 0x20) {
            // Fallback unicode-escape form for the remaining low control
            // bytes that don't have a short alias. `bufPrint` writes
            // exactly 6 bytes (`\u00XX`).
            const written = std.fmt.bufPrint(out[p..], "\\u{x:0>4}", .{b}) catch return null;
            p += written.len;
            continue;
        }
        // Pass-through for bytes >= 0x20. We do NOT validate or transcode
        // UTF-8 — the wire is byte-oriented and the SDK's `json.loads`
        // accepts non-strict input (consumers needing strict UTF-8 should
        // post-process). This matches the existing TreeJsonWriter
        // discipline in server.zig.
        if (p + 1 > out.len) return null;
        out[p] = b;
        p += 1;
    }
    return p;
}

// ===========================================================================
// Test pins
// ===========================================================================

test "appendEscaped: plain ASCII passes through" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "hello").?;
    try std.testing.expectEqualStrings("hello", buf[0..end]);
}

test "appendEscaped: empty string is a no-op" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "").?;
    try std.testing.expectEqual(@as(usize, 0), end);
}

test "appendEscaped: double-quote becomes \\\"" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "a\"b").?;
    try std.testing.expectEqualStrings("a\\\"b", buf[0..end]);
}

test "appendEscaped: backslash becomes \\\\" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "a\\b").?;
    try std.testing.expectEqualStrings("a\\\\b", buf[0..end]);
}

test "appendEscaped: newline / tab / cr use short forms" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "a\nb\tc\rd").?;
    try std.testing.expectEqualStrings("a\\nb\\tc\\rd", buf[0..end]);
}

test "appendEscaped: backspace / form-feed use short forms" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "a\x08b\x0Cc").?;
    try std.testing.expectEqualStrings("a\\bb\\fc", buf[0..end]);
}

test "appendEscaped: low-control bytes without short alias use \\u00XX" {
    var buf: [64]u8 = undefined;
    // 0x01, 0x07 (bell), 0x1F have no short-form alias.
    const end = appendEscaped(&buf, 0, "\x01\x07\x1f").?;
    try std.testing.expectEqualStrings("\\u0001\\u0007\\u001f", buf[0..end]);
}

test "appendEscaped: bytes >= 0x20 pass through (including 0x80+)" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "ab\x7fcd\x80ef").?;
    try std.testing.expectEqualStrings("ab\x7fcd\x80ef", buf[0..end]);
}

test "appendEscaped: overflow returns null without committing the partial" {
    var buf: [4]u8 = undefined;
    // `"a"b"` would need at least 6 bytes after escaping (`a\"b\"`), but
    // buf is 4 bytes. We expect null and the caller should treat the
    // contents of buf as indeterminate.
    const result = appendEscaped(&buf, 0, "a\"b\"");
    try std.testing.expect(result == null);
}

test "appendEscaped: respects a non-zero starting pos" {
    var buf: [64]u8 = undefined;
    @memcpy(buf[0..3], "pre");
    const end = appendEscaped(&buf, 3, "x\"y").?;
    try std.testing.expectEqualStrings("prex\\\"y", buf[0..end]);
}

test "appendEscaped: real-world sysfs-style input (driver name with no special chars)" {
    var buf: [64]u8 = undefined;
    const end = appendEscaped(&buf, 0, "usbhid").?;
    try std.testing.expectEqualStrings("usbhid", buf[0..end]);
}

test "appendEscaped: hostile sysfs-style input (every dangerous byte)" {
    var buf: [128]u8 = undefined;
    // A pathological substrate could legitimately produce a string
    // containing every escape-relevant byte; we pin that the output
    // is a valid JSON string body.
    const end = appendEscaped(&buf, 0, "\"\\\n\r\t\x08\x0c\x01").?;
    try std.testing.expectEqualStrings("\\\"\\\\\\n\\r\\t\\b\\f\\u0001", buf[0..end]);
}
