Skip to content

Defang / Refang — Zig source

Make URLs, IPs, and emails safe to paste by defanging indicators (https:// → hxxps[://], . → [.], @ → [@]) - and reverse refanged text back to its original form. Runs entirely in your browser, with a shareable link to your exact input and mode.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! defang-refang — make URLs, IPs, and emails safe to paste (and reverse it).
//!
//! Language: Zig (0.13, standard library only)
//! Source:   CosmoDev polyglot showcase port of the Defang/Refang tool, ported
//!           from src/lib/defang.ts (the canonical TypeScript implementation)
//!           and held in lock-step with cli/defang-refang/defang-refang.go.
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Deterministic; the only failure mode is OutOfMemory (surfaced as an
//!     error, never a panic).
//!   - Functionally equivalent to the TS / Go reference: same inputs -> same outputs.
//!   - Self-contained: std only — std.mem.replaceOwned powers the replace-all.
//!
//! Algorithm: defang() replaces "https://" / "http://" schemes with "hxxps[://]"
//! / "hxxp[://]", then brackets EVERY dot as "[.]" and every at-sign as "[@]" —
//! the standard threat-intel indicator-defanging convention. refang() is its
//! exact inverse: scheme markers are restored first, then "[.]" / "[@]" are
//! unbracketed, so refang(defang(x)) == x for typical URLs, IPs, emails, and
//! plain text.
//!
//! Ordering note: scheme replacements run BEFORE dot/at bracketing so the dots
//! and colon inside the resulting "[://]" marker are not themselves re-bracketed
//! (which would corrupt the scheme marker). Note that EVERY dot is bracketed,
//! not just the domain separator — so defang is NOT idempotent by design: apply
//! it once to fresh text. std.mem.replaceOwned replaces every occurrence,
//! matching the replaceAll semantics of the TS source one for one.
//!
//! Memory: defang() / refang() allocate through the caller's allocator and
//! return a slice the caller owns and must free. Intermediate buffers are
//! freed internally (see replaceAndFree), so exactly one free balances each
//! call.

const std = @import("std");

/// Replace every occurrence of `needle` in `owned` with `replacement`, then
/// free `owned` — so chained replacements never leak their intermediates.
/// `owned` must be a slice allocated by `allocator`.
fn replaceAndFree(
    allocator: std.mem.Allocator,
    owned: []u8,
    needle: []const u8,
    replacement: []const u8,
) std.mem.Allocator.Error![]u8 {
    const out = try std.mem.replaceOwned(u8, allocator, owned, needle, replacement);
    allocator.free(owned);
    return out;
}

/// Defang text so URLs, IPs, and emails are no longer clickable or auto-linked.
///
/// Mirrors `defang()` in src/lib/defang.ts and `Defang()` in the Go twin, and
/// must agree with both on every shared vector. Scheme replacements run first
/// so the dots and colon inside the resulting "[://]" marker are not themselves
/// re-bracketed. EVERY dot is bracketed, not just the domain separator. Empty
/// input returns empty; text with no indicators passes through unchanged.
pub fn defang(allocator: std.mem.Allocator, text: []const u8) std.mem.Allocator.Error![]u8 {
    var out = try std.mem.replaceOwned(u8, allocator, text, "https://", "hxxps[://]");
    out = try replaceAndFree(allocator, out, "http://", "hxxp[://]");
    out = try replaceAndFree(allocator, out, ".", "[.]");
    return replaceAndFree(allocator, out, "@", "[@]");
}

/// Reverse defanging — restore "hxxps[://]" / "hxxp[://]" to real schemes and
/// unbracket "[.]" / "[@]". Mirrors `refang()` in src/lib/defang.ts and
/// `Refang()` in the Go twin: the exact inverse of `defang`.
///
/// Scheme markers are restored before "[.]" / "[@]" are unbracketed, so a
/// marker like "hxxps[://]" reassembles into "https://" rather than being
/// split by an earlier dot/at substitution. Empty input returns empty.
pub fn refang(allocator: std.mem.Allocator, text: []const u8) std.mem.Allocator.Error![]u8 {
    var out = try std.mem.replaceOwned(u8, allocator, text, "hxxps[://]", "https://");
    out = try replaceAndFree(allocator, out, "hxxp[://]", "http://");
    out = try replaceAndFree(allocator, out, "[.]", ".");
    return replaceAndFree(allocator, out, "[@]", "@");
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------

test "defangs url, email, and ip" {
    // Scheme first, then every dot bracketed, then every at-sign bracketed.
    const a = try defang(std.testing.allocator, "https://example.com");
    defer std.testing.allocator.free(a);
    try std.testing.expectEqualStrings("hxxps[://]example[.]com", a);

    const b = try defang(std.testing.allocator, "user@example.com");
    defer std.testing.allocator.free(b);
    try std.testing.expectEqualStrings("user[@]example[.]com", b);

    const c = try defang(std.testing.allocator, "192.168.1.1");
    defer std.testing.allocator.free(c);
    try std.testing.expectEqualStrings("192[.]168[.]1[.]1", c);
}

test "defangs mixed text" {
    const out = try defang(
        std.testing.allocator,
        "contact admin@site.co.uk or visit http://site.co.uk",
    );
    defer std.testing.allocator.free(out);
    try std.testing.expectEqualStrings(
        "contact admin[@]site[.]co[.]uk or visit hxxp[://]site[.]co[.]uk",
        out,
    );
}

test "applies scheme replacement before dot bracketing" {
    // The "://" is preserved inside [://], not split into [.] payloads.
    const got = try defang(std.testing.allocator, "https://a.com");
    defer std.testing.allocator.free(got);
    try std.testing.expectEqualStrings("hxxps[://]a[.]com", got);
    try std.testing.expect(std.mem.indexOf(u8, got, "https[://]") == null);
    try std.testing.expect(std.mem.indexOf(u8, got, "hxxps://") == null);
}

test "refangs and round-trips" {
    // Refang is the exact inverse: scheme markers restored before unbracketing.
    const a = try refang(std.testing.allocator, "hxxps[://]example[.]com");
    defer std.testing.allocator.free(a);
    try std.testing.expectEqualStrings("https://example.com", a);

    const b = try refang(std.testing.allocator, "hxxp[://]single[.]com");
    defer std.testing.allocator.free(b);
    try std.testing.expectEqualStrings("http://single.com", b);

    // hxxp and hxxps are distinct markers — never confused.
    const c = try refang(std.testing.allocator, "hxxps[://]secure[.]com");
    defer std.testing.allocator.free(c);
    try std.testing.expectEqualStrings("https://secure.com", c);

    // Round-trip identity for typical URLs, IPs, emails, and plain text.
    const inputs = [_][]const u8{
        "https://sub.example.com/path?q=1&a=b",
        "first.last@sub.example.co.uk",
        "172.16.254.1",
        "just a sentence with no indicators",
    };
    for (inputs) |x| {
        const d = try defang(std.testing.allocator, x);
        defer std.testing.allocator.free(d);
        const r = try refang(std.testing.allocator, d);
        defer std.testing.allocator.free(r);
        try std.testing.expectEqualStrings(x, r);
    }
}

test "empty and passthrough" {
    const d = try defang(std.testing.allocator, "");
    defer std.testing.allocator.free(d);
    try std.testing.expectEqualStrings("", d);

    const r = try refang(std.testing.allocator, "");
    defer std.testing.allocator.free(r);
    try std.testing.expectEqualStrings("", r);

    const p = try defang(std.testing.allocator, "nothing to transform here");
    defer std.testing.allocator.free(p);
    try std.testing.expectEqualStrings("nothing to transform here", p);

    const q = try refang(std.testing.allocator, "no indicators here");
    defer std.testing.allocator.free(q);
    try std.testing.expectEqualStrings("no indicators here", q);
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →