JWT Debugger — Rust source
Decode a JSON Web Token and inspect its header and payload as pretty JSON, with exp/iat claim awareness and an expiry badge. Runs entirely in your browser.
This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
// JWT base64url decoding helper — Rust port.
//
// Language: Rust
// CosmoDev polyglot showcase port of the `jwt` tool.
// Ported from src/lib/jwt.ts.
//
// Display source — part of CosmoDev's polyglot tool pages
// (dev.cosmolabs.org).
//
// Decode-only: JWT signature verification stays server-side. The public
// helper converts a base64url string (the alphabet used by every JWT
// header/payload segment) into text.
//
// Rust's standard library ships no base64 codec, so this file contains a
// small, self-contained streaming decoder — no external crates required.
use std::fmt;
/// Error raised while decoding a base64url string: the offending character
/// is not part of the alphabet.
#[derive(Debug)]
pub struct InvalidChar(pub char);
impl fmt::Display for InvalidChar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid base64url character: {:?}", self.0)
}
}
impl std::error::Error for InvalidChar {}
/// Decode a base64url string into text.
///
/// JWT segments use RFC 4648's URL-safe base64 alphabet: `-` and `_` replace
/// `+` and `/`, and the trailing `=` padding is normally omitted. Because we
/// decode the alphabet directly (recognising the URL-safe characters
/// ourselves), no up-front character translation is needed — the streaming
/// decoder simply stops at any trailing `=`.
///
/// The result is byte-faithful: each decoded byte is promoted to a `char` of
/// the same value (a Latin-1 mapping), mirroring the Web `atob` binary-string
/// convention so the full 0x00-0xFF byte range round-trips. (A `String::from_utf8`
/// call would instead reinterpret the bytes as UTF-8 and reject the 0x80-0xFF
/// test vectors.)
///
/// Returns `Err(InvalidChar)` for any character outside the alphabet, matching
/// the TS throw on malformed input.
pub fn b64url_decode(input: &str) -> Result<String, InvalidChar> {
let bytes = decode_bytes(input)?;
// Promote each byte to a char of the same value (Latin-1) — never a UTF-8
// decode — so behaviour lines up with the JS `atob` binary string.
Ok(bytes.iter().map(|&b| b as char).collect())
}
/// base64url characters -> raw bytes (the core of the decoder).
///
/// Base64 packs 3 bytes (24 bits) into 4 six-bit symbols. We stream the input
/// one symbol at a time, accumulating bits and emitting a byte whenever at
/// least 8 are available. Any leftover bits at the end are padding and are
/// discarded. The bit window we read from always sits in the low ~13 bits of
/// the accumulator, so the u32 never loses data we still need.
fn decode_bytes(input: &str) -> Result<Vec<u8>, InvalidChar> {
let mut out: Vec<u8> = Vec::with_capacity(input.len() * 3 / 4);
let mut buffer: u32 = 0; // bit accumulator
let mut bits: u32 = 0; // how many valid bits are currently held in `buffer`
for ch in input.chars() {
// '=' padding marks the end of the encoded data.
if ch == '=' {
break;
}
let value = decode_char(ch).ok_or(InvalidChar(ch))?;
buffer = (buffer << 6) | u32::from(value);
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buffer >> bits) as u8);
}
}
Ok(out)
}
/// Map a single base64 symbol to its 6-bit value.
///
/// Accepts both the URL-safe alphabet (`-`, `_`) and the standard one (`+`,
/// `/`) so callers never have to translate alphabets first.
fn decode_char(ch: char) -> Option<u8> {
match ch {
'A'..='Z' => Some((ch as u8) - b'A'),
'a'..='z' => Some((ch as u8) - b'a' + 26),
'0'..='9' => Some((ch as u8) - b'0' + 52),
'-' | '+' => Some(62),
'_' | '/' => Some(63),
_ => None,
}
}
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 →