Skip to content

Defang / Refang — Swift 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 Swift 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: Swift (5.9, standard library + Foundation)
// 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 traps.
//   - Functionally equivalent to the TS / Go reference: same inputs -> same outputs.
//   - Library note: String in the pure Swift standard library has no
//     replace-all primitive, so this port uses Foundation's
//     String.replacingOccurrences(of:with:) — literal, case-sensitive, replaces
//     every occurrence — the platform-standard equivalent of the TS replaceAll
//     (Foundation ships with every Swift toolchain).
//
// 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.

import Foundation

/// 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.
func defang(_ text: String) -> String {
    text
        .replacingOccurrences(of: "https://", with: "hxxps[://]")
        .replacingOccurrences(of: "http://", with: "hxxp[://]")
        .replacingOccurrences(of: ".", with: "[.]")
        .replacingOccurrences(of: "@", with: "[@]")
}

/// 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.
func refang(_ text: String) -> String {
    text
        .replacingOccurrences(of: "hxxps[://]", with: "https://")
        .replacingOccurrences(of: "hxxp[://]", with: "http://")
        .replacingOccurrences(of: "[.]", with: ".")
        .replacingOccurrences(of: "[@]", with: "@")
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
// Top-level statements make the file runnable directly (`swift swift.swift`).
var showcaseFailures = 0

func expect(_ got: String, _ want: String, _ label: String) {
    if got != want {
        print("FAIL \(label):\n  got  \"\(got)\"\n  want \"\(want)\"")
        showcaseFailures += 1
    }
}

// defangs url, email, and ip
expect(defang("https://example.com"), "hxxps[://]example[.]com", "defang url")
expect(defang("user@example.com"), "user[@]example[.]com", "defang email")
expect(defang("192.168.1.1"), "192[.]168[.]1[.]1", "defang ip")

// defangs mixed text
expect(
    defang("contact admin@site.co.uk or visit http://site.co.uk"),
    "contact admin[@]site[.]co[.]uk or visit hxxp[://]site[.]co[.]uk",
    "defang mixed"
)

// applies scheme replacement before dot bracketing
let schemeFirst = defang("https://a.com")
expect(schemeFirst, "hxxps[://]a[.]com", "scheme first")
if schemeFirst.contains("https[://]") || schemeFirst.contains("hxxps://") {
    print("FAIL scheme marker corrupted: \"\(schemeFirst)\"")
    showcaseFailures += 1
}

// refangs and round-trips
expect(refang("hxxps[://]example[.]com"), "https://example.com", "refang https")
expect(refang("hxxp[://]single[.]com"), "http://single.com", "refang http")
// hxxp and hxxps are distinct markers — never confused.
expect(refang("hxxps[://]secure[.]com"), "https://secure.com", "refang hxxps distinct")
// 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",
] {
    expect(refang(defang(x)), x, "round trip")
}

// empty and passthrough
expect(defang(""), "", "defang empty")
expect(refang(""), "", "refang empty")
expect(defang("nothing to transform here"), "nothing to transform here", "defang passthrough")
expect(refang("no indicators here"), "no indicators here", "refang passthrough")

print(showcaseFailures == 0 ? "all checks passed" : "\(showcaseFailures) check(s) failed")

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 →