Skip to content

URL Encode / Decode — C source

Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.

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

/* url-encode — percent encode/decode, component (encodeURIComponent) and full-URI (encodeURI) scope.
 * Language: C (C11). Port of src/tools/UrlEncodeTool.tsx — same contract as this dir's go.go:
 * each byte outside the safe set becomes %XX (uppercase hex); component keeps A-Za-z0-9-_.!~*'(),
 * full URI also keeps ;,/?:@&=+$#; decode is strict (bad % pair, truncated/overlong/surrogate
 * UTF-8 -> error) and full-URI scope leaves encoded reserved bytes intact, like decodeURI. */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct result { char *out; const char *err; }; /* err == NULL on success */
#define BAD { NULL, "malformed URI sequence" }

/* component scope leaves A-Za-z0-9-_.!~*'() unescaped (ASCII-only "C" locale) */
static int comp_safe(unsigned char c) { return isalnum(c) || strchr("-_.!~*'()", c) != NULL; }
static int uri_extra(unsigned char c) { return strchr(";/?:@&=+$,#", c) != NULL; }

static char *encode(const char *s, int full_uri) /* caller frees; output <= 3x input */
{
    static const char HEX[] = "0123456789ABCDEF";
    char *b = malloc(3 * strlen(s) + 1);
    size_t n = 0;
    for (size_t i = 0; s[i]; i++) {
        unsigned char c = (unsigned char)s[i];
        if (comp_safe(c) || (full_uri && uri_extra(c))) b[n++] = (char)c;
        else b[n++] = '%', b[n++] = HEX[c >> 4], b[n++] = HEX[c & 0xf];
    }
    b[n] = '\0';
    return b;
}

static int unhex(char c) /* -1 on bad digit; the '\0' sentinel of a truncated tail maps there too */
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

/* strict UTF-8 walk — rejects truncated, overlong, surrogate and >U+10FFFF sequences */
static int utf8_ok(const unsigned char *p, size_t n)
{
    size_t i = 0;
    while (i < n) {
        unsigned char c = p[i], lo = 0x80, hi = 0xBF; /* lo..hi bound the 2nd byte */
        size_t len;
        if (c < 0x80) { i++; continue; }
        else if (c >= 0xC2 && c <= 0xDF) len = 2;
        else if (c == 0xE0) { len = 3, lo = 0xA0; } /* E0 80..9F would be overlong */
        else if ((c >= 0xE1 && c <= 0xEC) || c == 0xEE || c == 0xEF) len = 3;
        else if (c == 0xED) { len = 3, hi = 0x9F; } /* ED A0..BF are surrogates */
        else if (c == 0xF0) { len = 4, lo = 0x90; } /* F0 80..8F would be overlong */
        else if (c >= 0xF1 && c <= 0xF3) len = 4;
        else if (c == 0xF4) { len = 4, hi = 0x8F; } /* cap at U+10FFFF */
        else return 0;
        if (i + len > n || p[i + 1] < lo || p[i + 1] > hi) return 0;
        for (size_t k = 2; k < len; k++)
            if (p[i + k] < 0x80 || p[i + k] > 0xBF) return 0;
        i += len;
    }
    return 1;
}

static struct result decode(const char *s, int full_uri)
{
    unsigned char *b = malloc(strlen(s) + 1); /* decoded output never exceeds input length */
    size_t n = 0;
    for (size_t i = 0; s[i]; ) {
        if (s[i] != '%') { b[n++] = s[i++]; continue; }
        int hi = unhex(s[i + 1]), lo = unhex(s[i + 2]);
        if (hi < 0 || lo < 0) { free(b); return (struct result)BAD; }
        unsigned char c = (unsigned char)(hi << 4 | lo);
        if (full_uri && c < 0x80 && uri_extra(c)) /* decodeURI keeps encoded reserved */
            b[n++] = '%', b[n++] = s[i + 1], b[n++] = s[i + 2];
        else
            b[n++] = c;
        i += 3;
    }
    if (!utf8_ok(b, n)) { free(b); return (struct result)BAD; }
    b[n] = '\0';
    return (struct result){ (char *)b, NULL };
}

static void show(const char *label, struct result r) { printf("%s%s\n", label, r.err ? r.err : r.out); }

int main(void)
{
    printf("enc:  %s\n", encode("hello world & café", 0));
    printf("uri:  %s\n", encode("hello world & café", 1));
    show("dec:  ", decode("hello%20world%20%26%20caf%C3%A9", 0));
    show("duri: ", decode("a%2Fb%3Fc%2Cd", 1)); /* reserved stay escaped in decodeURI */
    show("dcmp: ", decode("a%2Fb%3Fc%2Cd", 0)); /* ...but component scope decodes them */
    show("bad:  ", decode("100%", 0));
    return 0;
}

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 →