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 →