Skip to content

Defang / Refang — C 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 C 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: C (C11, 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 allocation failure (NULL return).
 *   - Functionally equivalent to the TS / Go reference: same inputs -> same outputs.
 *   - Self-contained: stdlib only (strstr + memcpy power 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. replace_all() replaces every occurrence, matching the
 * replaceAll semantics of the TS source one for one.
 *
 * Memory: defang() / refang() return a fresh malloc'd buffer the caller owns
 * and must free(); NULL is returned only when malloc fails.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*
 * Return a fresh heap buffer holding `haystack` with every occurrence of
 * `needle` (which must be non-empty) replaced by `replacement`.
 * Returns NULL only when malloc fails.
 */
static char *replace_all(const char *haystack, const char *needle,
                         const char *replacement) {
    const size_t needle_len = strlen(needle);
    const size_t repl_len = strlen(replacement);

    /* Count matches first so the output buffer is sized exactly once. */
    size_t matches = 0;
    const char *scan = haystack;
    while ((scan = strstr(scan, needle)) != NULL) {
        matches++;
        scan += needle_len;
    }

    const size_t hay_len = strlen(haystack);
    char *out = malloc(hay_len - matches * needle_len + matches * repl_len + 1);
    if (out == NULL) {
        return NULL;
    }

    char *dst = out;
    scan = haystack;
    for (;;) {
        const char *hit = strstr(scan, needle);
        if (hit == NULL) {
            break;
        }
        memcpy(dst, scan, (size_t)(hit - scan));
        dst += hit - scan;
        memcpy(dst, replacement, repl_len);
        dst += repl_len;
        scan = hit + needle_len;
    }
    strcpy(dst, scan); /* trailing remainder */
    return out;
}

/*
 * Chain one replacement onto a buffer this pipeline already owns, freeing the
 * intermediate result so callers never track it.
 */
static char *replace_owned(char *owned, const char *needle,
                           const char *replacement) {
    char *out = replace_all(owned, needle, replacement);
    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.
 */
char *defang(const char *text) {
    char *out = replace_all(text, "https://", "hxxps[://]");
    if (out == NULL) return NULL;
    out = replace_owned(out, "http://", "hxxp[://]");
    if (out == NULL) return NULL;
    out = replace_owned(out, ".", "[.]");
    if (out == NULL) return NULL;
    return replace_owned(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.
 */
char *refang(const char *text) {
    char *out = replace_all(text, "hxxps[://]", "https://");
    if (out == NULL) return NULL;
    out = replace_owned(out, "hxxp[://]", "http://");
    if (out == NULL) return NULL;
    out = replace_owned(out, "[.]", ".");
    if (out == NULL) return NULL;
    return replace_owned(out, "[@]", "@");
}

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

static int checks_failed = 0;

static void expect_str(const char *got, const char *want, const char *label) {
    if (strcmp(got, want) != 0) {
        fprintf(stderr, "FAIL %s:\n  got  \"%s\"\n  want \"%s\"\n", label, got, want);
        checks_failed = 1;
    }
}

static void expect_absent(const char *haystack, const char *needle, const char *label) {
    if (strstr(haystack, needle) != NULL) {
        fprintf(stderr, "FAIL %s: unexpectedly contains \"%s\"\n", label, needle);
        checks_failed = 1;
    }
}

int main(void) {
    char *s;

    /* defangs url, email, and ip */
    s = defang("https://example.com");
    expect_str(s, "hxxps[://]example[.]com", "defang url");
    free(s);
    s = defang("user@example.com");
    expect_str(s, "user[@]example[.]com", "defang email");
    free(s);
    s = defang("192.168.1.1");
    expect_str(s, "192[.]168[.]1[.]1", "defang ip");
    free(s);

    /* defangs mixed text */
    s = defang("contact admin@site.co.uk or visit http://site.co.uk");
    expect_str(s, "contact admin[@]site[.]co[.]uk or visit hxxp[://]site[.]co[.]uk",
               "defang mixed");
    free(s);

    /* applies scheme replacement before dot bracketing */
    s = defang("https://a.com");
    expect_str(s, "hxxps[://]a[.]com", "scheme first");
    expect_absent(s, "https[://]", "scheme marker not split");
    expect_absent(s, "hxxps://", "scheme marker not split");
    free(s);

    /* refangs and round-trips */
    s = refang("hxxps[://]example[.]com");
    expect_str(s, "https://example.com", "refang https");
    free(s);
    s = refang("hxxp[://]single[.]com");
    expect_str(s, "http://single.com", "refang http");
    free(s);
    s = refang("hxxps[://]secure[.]com"); /* hxxp and hxxps are distinct markers */
    expect_str(s, "https://secure.com", "refang hxxps distinct");
    free(s);
    static const char *const round_trips[] = {
        "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 (size_t i = 0; i < sizeof round_trips / sizeof round_trips[0]; i++) {
        char *defanged = defang(round_trips[i]);
        char *refanged = refang(defanged);
        expect_str(refanged, round_trips[i], "round trip");
        free(defanged);
        free(refanged);
    }

    /* empty and passthrough */
    s = defang("");
    expect_str(s, "", "defang empty");
    free(s);
    s = refang("");
    expect_str(s, "", "refang empty");
    free(s);
    s = defang("nothing to transform here");
    expect_str(s, "nothing to transform here", "defang passthrough");
    free(s);
    s = refang("no indicators here");
    expect_str(s, "no indicators here", "refang passthrough");
    free(s);

    if (checks_failed) {
        fputs("at least one check failed\n", stderr);
        return EXIT_FAILURE;
    }
    puts("all checks passed");
    return EXIT_SUCCESS;
}

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 →