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# (.NET 8, 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, invalid UTF-8 -> error) and full-URI scope leaves encoded reserved bytes intact.
using System.Text;

static class UrlEncode
{
    const string Hex = "0123456789ABCDEF";
    const string Bad = "malformed URI sequence";

    static bool CompSafe(byte c) =>
        (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
        || "-_.!~*'()".Contains((char)c);

    static bool UriExtra(byte c) => ";,/?:@&=+$#".Contains((char)c);

    public static string Encode(string s, bool fullUri)
    {
        var b = new StringBuilder(s.Length * 3);
        foreach (byte c in Encoding.UTF8.GetBytes(s))
        {
            if (CompSafe(c) || (fullUri && UriExtra(c))) b.Append((char)c);
            else b.Append('%').Append(Hex[c >> 4]).Append(Hex[c & 0xf]);
        }
        return b.ToString();
    }

    static int Unhex(char c) =>
        c is >= '0' and <= '9' ? c - '0'
        : c is >= 'a' and <= 'f' ? c - 'a' + 10
        : c is >= 'A' and <= 'F' ? c - 'A' + 10
        : -1;

    public static (string Output, string? Error) Decode(string s, bool fullUri)
    {
        var bytes = new List<byte>(s.Length);
        for (int i = 0; i < s.Length;)
        {
            if (s[i] != '%') { bytes.Add((byte)s[i++]); continue; }
            if (i + 2 >= s.Length) 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
            byte c = (byte)(hi << 4 | lo);
            if (fullUri && c < 0x80 && UriExtra(c)) // decodeURI keeps encoded reserved
                bytes.AddRange(new[] { (byte)'%', (byte)s[i + 1], (byte)s[i + 2] });
            else
                bytes.Add(c);
            i += 3;
        }
        try
        {
            // throwOnInvalidBytes -> strict decoder: overlong/surrogate/truncated UTF-8 throws
            var utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
            return (utf8.GetString(bytes.ToArray()), null);
        }
        catch (DecoderFallbackException) { return ("", Bad); }
    }

    static void Main()
    {
        Console.WriteLine($"enc:  {Encode("hello world & café", fullUri: false)}");
        Console.WriteLine($"uri:  {Encode("hello world & café", fullUri: true)}");
        var r = Decode("hello%20world%20%26%20caf%C3%A9", fullUri: false);
        Console.WriteLine($"dec:  {r.Error ?? r.Output}");
        r = Decode("a%2Fb%3Fc%2Cd", fullUri: true); // reserved stay escaped in decodeURI
        Console.WriteLine($"duri: {r.Error ?? r.Output}");
        r = Decode("a%2Fb%3Fc%2Cd", fullUri: false); // ...but decode in component scope
        Console.WriteLine($"dcmp: {r.Error ?? r.Output}");
        r = Decode("100%", fullUri: false);
        Console.WriteLine($"bad:  {r.Error ?? r.Output}");
    }
}

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 →