Skip to content

Defang / Refang — Rust 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 Rust 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: Rust (edition 2021, 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:
//!   - Pure + deterministic; never panics (public API returns Strings, no Result).
//!   - Functionally equivalent to the TS / Go reference: same inputs -> same outputs.
//!   - Self-contained: std only (no crates.io dependencies).
//!
//! 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. `str::replace` replaces every occurrence, matching the
//! `replaceAll` semantics of the TS source one for one.

/// 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(input: &str) -> String {
    let mut out = input.replace("https://", "hxxps[://]");
    out = out.replace("http://", "hxxp[://]");
    out = out.replace('.', "[.]");
    out = out.replace('@', "[@]");
    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(input: &str) -> String {
    let mut out = input.replace("hxxps[://]", "https://");
    out = out.replace("hxxp[://]", "http://");
    out = out.replace("[.]", ".");
    out = out.replace("[@]", "@");
    out
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defangs_url_email_and_ip() {
        // Scheme first, then every dot bracketed, then every at-sign bracketed.
        assert_eq!(defang("https://example.com"), "hxxps[://]example[.]com");
        assert_eq!(defang("user@example.com"), "user[@]example[.]com");
        assert_eq!(defang("192.168.1.1"), "192[.]168[.]1[.]1");
    }

    #[test]
    fn defangs_mixed_text() {
        assert_eq!(
            defang("contact admin@site.co.uk or visit http://site.co.uk"),
            "contact admin[@]site[.]co[.]uk or visit hxxp[://]site[.]co[.]uk",
        );
    }

    #[test]
    fn applies_scheme_replacement_before_dot_bracketing() {
        // The `://` is preserved inside [://], not split into [.] payloads.
        let got = defang("https://a.com");
        assert_eq!(got, "hxxps[://]a[.]com");
        assert!(!got.contains("https[://]"));
        assert!(!got.contains("hxxps://"));
    }

    #[test]
    fn refangs_and_round_trips() {
        // Refang is the exact inverse: scheme markers restored before unbracketing.
        assert_eq!(refang("hxxps[://]example[.]com"), "https://example.com");
        assert_eq!(refang("hxxp[://]single[.]com"), "http://single.com");
        // hxxp and hxxps are distinct markers — never confused.
        assert_eq!(refang("hxxps[://]secure[.]com"), "https://secure.com");
        // Round-trip identity for typical URLs, IPs, emails, and plain text.
        for x in [
            "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",
        ] {
            assert_eq!(refang(&defang(x)), x);
        }
    }

    #[test]
    fn empty_and_passthrough() {
        assert_eq!(defang(""), "");
        assert_eq!(refang(""), "");
        assert_eq!(defang("nothing to transform here"), "nothing to transform here");
        assert_eq!(refang("no indicators here"), "no indicators here");
    }
}

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 →