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++ (C++17, standard library only). 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 <cctype>
#include <iostream>
#include <string>
#include <string_view>

namespace url_encode {

// component scope leaves A-Za-z0-9-_.!~*'() unescaped ("C" locale isalnum)
constexpr bool comp_safe(unsigned char c)
{
    return std::isalnum(c) || std::string_view("-_.!~*'()").find(c) != std::string_view::npos;
}

constexpr bool uri_extra(unsigned char c)
{
    return std::string_view(";/?:@&=+$#,").find(c) != std::string_view::npos;
}

inline std::string encode(std::string_view s, bool full_uri)
{
    static constexpr char HEX[] = "0123456789ABCDEF";
    std::string b;
    b.reserve(s.size() * 3);
    for (unsigned char c : s) {
        if (comp_safe(c) || (full_uri && uri_extra(c))) b += static_cast<char>(c);
        else b += '%', b += HEX[c >> 4], b += HEX[c & 0xf];
    }
    return b;
}

inline int unhex(char c)
{
    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.
inline bool utf8_ok(std::string_view p)
{
    for (size_t i = 0; i < p.size();) {
        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 false;
        if (i + len > p.size() || (unsigned char)p[i + 1] < lo || (unsigned char)p[i + 1] > hi)
            return false;
        for (size_t k = 2; k < len; k++)
            if ((unsigned char)p[i + k] < 0x80 || (unsigned char)p[i + k] > 0xBF) return false;
        i += len;
    }
    return true;
}

struct Result { std::string output, error; }; // error empty on success

inline Result decode(std::string_view s, bool full_uri)
{
    static const std::string BAD = "malformed URI sequence";
    std::string bytes;
    bytes.reserve(s.size());
    for (size_t i = 0; i < s.size();) {
        if (s[i] != '%') { bytes += s[i++]; continue; }
        if (i + 2 >= s.size()) return { "", BAD }; // truncated tail
        int hi = unhex(s[i + 1]), lo = unhex(s[i + 2]);
        if (hi < 0 || lo < 0) return { "", BAD }; // bad hex digit
        unsigned char c = static_cast<unsigned char>(hi << 4 | lo);
        if (full_uri && c < 0x80 && uri_extra(c)) bytes += s.substr(i, 3); // decodeURI keeps reserved
        else bytes += static_cast<char>(c);
        i += 3;
    }
    if (!utf8_ok(bytes)) return { "", BAD };
    return { bytes, "" };
}

} // namespace url_encode

int main()
{
    using namespace url_encode;
    auto show = [](std::string_view label, const Result &r) {
        std::cout << label << (r.error.empty() ? r.output : r.error) << "\n";
    };
    std::cout << "enc:  " << encode("hello world & café", false) << "\n";
    std::cout << "uri:  " << encode("hello world & café", true) << "\n";
    show("dec:  ", decode("hello%20world%20%26%20caf%C3%A9", false));
    show("duri: ", decode("a%2Fb%3Fc%2Cd", true)); // reserved stay escaped in decodeURI
    show("dcmp: ", decode("a%2Fb%3Fc%2Cd", false)); // ...but component scope decodes them
    show("bad:  ", decode("100%", false));
}

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 →