Skip to content

URL Encode / Decode — Rust source

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

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

// URL encode / decode — component-level (encodeURIComponent) and full URI (encodeURI).
//
// Language: Rust
// CosmoDev polyglot showcase port of the `url-encode` tool.
// Ported from src/tools/UrlEncodeTool.tsx — display source, part of CosmoDev's
// polyglot tool pages.
//
// Explicit percent-encoding matching JavaScript's encodeURIComponent/encodeURI.
// Rust strings are UTF-8, so we walk the raw bytes: any byte outside the safe
// set becomes %XX (uppercase hex). Component scope leaves A-Za-z0-9-_.!~*'()
// unescaped; full URI scope additionally leaves the reserved set ;,/?:@&=+$#.
// Decode reverses this, returning Err on malformed % sequences or invalid
// UTF-8; full URI leaves encoded reserved bytes intact. No external crates —
// the percent crate is not used, to keep the showcase dependency-free.

fn is_component_safe(c: u8) -> bool {
    c.is_ascii_alphanumeric()
        || matches!(c, b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')')
}

fn is_uri_extra(c: u8) -> bool {
    matches!(c, b';' | b',' | b'/' | b'?' | b':' | b'@' | b'&' | b'=' | b'+' | b'$' | b'#')
}

const RESERVED: &[u8] = b";/?:@&=+$#";
const HEX: &[u8; 16] = b"0123456789ABCDEF";

/// Percent-encode `input`. `full_uri` selects encodeURI vs encodeURIComponent.
pub fn encode(input: &str, full_uri: bool) -> String {
    let mut out = String::with_capacity(input.len());
    for &c in input.as_bytes() {
        if is_component_safe(c) || (full_uri && is_uri_extra(c)) {
            out.push(c as char);
        } else {
            out.push('%');
            out.push(HEX[(c >> 4) as usize] as char);
            out.push(HEX[(c & 0x0f) as usize] as char);
        }
    }
    out
}

fn unhex(c: u8) -> Option<u8> {
    match c {
        b'0'..=b'9' => Some(c - b'0'),
        b'a'..=b'f' => Some(c - b'a' + 10),
        b'A'..=b'F' => Some(c - b'A' + 10),
        _ => None,
    }
}

// Reads the two hex digits after '%' at index `i`. Err on malformed input.
fn unhex_pair(bytes: &[u8], i: usize) -> Result<u8, &'static str> {
    if i + 2 >= bytes.len() {
        return Err("malformed URI sequence");
    }
    let hi = unhex(bytes[i + 1]).ok_or("malformed URI sequence")?;
    let lo = unhex(bytes[i + 2]).ok_or("malformed URI sequence")?;
    Ok(hi << 4 | lo)
}

/// Percent-decode `input`. `full_uri` selects decodeURI vs decodeURIComponent.
pub fn decode(input: &str, full_uri: bool) -> Result<String, &'static str> {
    let bytes = input.as_bytes();
    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] != b'%' {
            out.push(bytes[i]);
            i += 1;
            continue;
        }
        let b = unhex_pair(bytes, i)?;
        if b < 0x80 {
            if full_uri && RESERVED.contains(&b) {
                out.extend_from_slice(&bytes[i..i + 3]); // preserve encoded reserved byte
            } else {
                out.push(b);
            }
            i += 3;
            continue;
        }
        let seq_len = match b {
            _ if b & 0xe0 == 0xc0 => 2,
            _ if b & 0xf0 == 0xe0 => 3,
            _ if b & 0xf8 == 0xf0 => 4,
            _ => return Err("malformed URI sequence"),
        };
        out.push(b);
        for n in 1..seq_len {
            let pi = i + 3 * n;
            if pi >= bytes.len() || bytes[pi] != b'%' {
                return Err("malformed URI sequence");
            }
            let cb = unhex_pair(bytes, pi)?;
            if cb & 0xc0 != 0x80 {
                return Err("malformed URI sequence");
            }
            out.push(cb);
        }
        i += 3 * seq_len;
    }
    String::from_utf8(out).map_err(|_| "malformed URI sequence")
}

fn main() {
    println!("{}", encode("hello world & café", false));
    println!("{:?}", decode("hello%20world%20%26%20caf%C3%A9", 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 →