Basic Auth Generator — Rust source
Build HTTP Basic Access Authentication headers from a username and password, or decode an existing Authorization header back to its credentials. UTF-8 safe and fully client-side, with a shareable link.
This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! basic-auth-generator — HTTP Basic-Auth header encode/decode.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source: CosmoDev polyglot showcase port of the Basic Auth Generator tool,
//! ported from src/lib/basic-auth.ts (the canonical TypeScript
//! implementation) and kept in lock-step with cli/basic-auth-generator.
//! License: display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//! - Pure + deterministic; never panics (public API returns Strings/Options).
//! - Functionally equivalent to the TS reference + Go twin: same inputs ->
//! same outputs, same reject behavior on malformed headers.
//! - Self-contained: std only (no crates.io dependencies — no `base64` crate).
//!
//! Unicode note: Rust's std has no base64 codec (the `base64` crate provides
//! one, but the brief forbids external deps). The full RFC 4648 §4 standard
//! alphabet encoder/decoder is implemented inline below so the port stays
//! dependency-free while remaining byte-for-byte equivalent to Go's
//! encoding/base64 and the TS b64encode/b64decode helpers (which are UTF-8
//! safe). The encoder operates on the UTF-8 byte representation of the input,
//! matching the TS `TextEncoder().encode(str)` path — so `café:päss` encodes
//! correctly, not just ASCII.
/// RFC 4648 §4 standard base64 alphabet.
const B64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Credentials hold a parsed username/password pair. Mirrors the Go twin's
/// `Credentials` struct and the TS `BasicAuthCredentials` interface.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
pub user: String,
pub pass: String,
}
/// Encode arbitrary bytes into standard base64 (with `=` padding), matching
/// Go's `base64.StdEncoding.EncodeToString` and TS's `b64encode`.
///
/// Processes input in 3-byte groups, emitting 4 characters per group. A
/// trailing 1- or 2-byte group produces 2 or 3 characters respectively, padded
/// with `=` to a multiple of 4.
fn b64_encode(input: &[u8]) -> String {
let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
let i0 = ((n >> 18) & 0x3F) as usize;
let i1 = ((n >> 12) & 0x3F) as usize;
let i2 = ((n >> 6) & 0x3F) as usize;
let i3 = (n & 0x3F) as usize;
out.push(B64_ALPHABET[i0] as char);
out.push(B64_ALPHABET[i1] as char);
match chunk.len() {
1 => {
out.push('=');
out.push('=');
}
2 => {
out.push(B64_ALPHABET[i2] as char);
out.push('=');
}
_ => {
out.push(B64_ALPHABET[i2] as char);
out.push(B64_ALPHABET[i3] as char);
}
}
}
out
}
/// Decode a standard base64 string into bytes. Returns `None` on any invalid
/// character or malformed padding, matching Go's `base64.StdEncoding.DecodeString`
/// (which returns an error) and the TS `b64decode` (which throws → caller maps
/// to None).
///
/// Whitespace inside the token is ignored, mirroring the TS
/// `b64decode(token.replace(/\s+/g, ''))` helper.
fn b64_decode(input: &str) -> Option<Vec<u8>> {
// Build the reverse lookup once: -1 marks bytes not in the alphabet.
let mut table = [-1i8; 256];
for (i, &b) in B64_ALPHABET.iter().enumerate() {
table[b as usize] = i as i8;
}
// Strip whitespace; standard base64 then requires a length that is a
// multiple of 4 (Go's StdEncoding enforces this; TS's btoa/atob path does
// too via the browser).
let cleaned: Vec<u8> = input
.bytes()
.filter(|b| !b.is_ascii_whitespace())
.collect();
if cleaned.is_empty() {
return Some(Vec::new());
}
if cleaned.len() % 4 != 0 {
return None;
}
let mut out = Vec::with_capacity(cleaned.len() / 4 * 3);
for chunk in cleaned.chunks(4) {
let mut idx = [0u8; 4];
let mut pads = 0u8;
for (j, &b) in chunk.iter().enumerate() {
if b == b'=' {
idx[j] = 0;
pads += 1;
} else {
let v = table[b as usize];
if v < 0 {
return None; // invalid character
}
idx[j] = v as u8;
}
}
let n = ((idx[0] as u32) << 18)
| ((idx[1] as u32) << 12)
| ((idx[2] as u32) << 6)
| (idx[3] as u32);
out.push((n >> 16) as u8);
if pads < 2 {
out.push((n >> 8) as u8);
}
if pads < 1 {
out.push(n as u8);
}
}
Some(out)
}
/// Build an HTTP Basic-Auth header value: `"Basic " + base64(user:pass)`.
/// Mirrors `BuildHeader` in the Go twin and `buildBasicAuth` in the TS source.
pub fn build_header(user: &str, pass: &str) -> String {
let raw = format!("{}:{}", user, pass);
format!("Basic {}", b64_encode(raw.as_bytes()))
}
/// Parse a `"Basic <token>"` header back into credentials, returning `None`
/// if the header is malformed or the credentials have no colon separator.
///
/// Logic mirrors `ParseHeader` in the Go twin exactly: trim the header,
/// require a case-insensitive `"basic "` prefix, trim the remaining token,
/// base64-decode it, then split on the FIRST colon (so a password containing
/// colons round-trips).
pub fn parse_header(header: &str) -> Option<Credentials> {
let h = header.trim();
let prefix = "basic ";
if h.len() < prefix.len() || !h.to_ascii_lowercase().starts_with(prefix) {
return None;
}
let token = h[prefix.len()..].trim();
let bytes = b64_decode(token)?;
let decoded = String::from_utf8(bytes).ok()?;
let (user, pass) = decoded.split_once(':')?;
Some(Credentials {
user: user.to_string(),
pass: pass.to_string(),
})
}
// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_known_vector() {
assert_eq!(build_header("user", "pass"), "Basic dXNlcjpwYXNz");
}
#[test]
fn round_trip() {
for (user, pass) in [("alice", "s3cr3t"), ("", "x"), ("x", ""), ("u", "a:b:c")] {
let creds = parse_header(&build_header(user, pass))
.expect("round-trip should parse");
assert_eq!(creds.user, user);
assert_eq!(creds.pass, pass, "password mismatch for ({user:?},{pass:?})");
}
}
#[test]
fn password_with_colon_preserved() {
// Split happens on the FIRST colon only — the rest stays in the password.
let creds = parse_header(&build_header("u", "a:b:c")).unwrap();
assert_eq!(creds.user, "u");
assert_eq!(creds.pass, "a:b:c");
}
#[test]
fn rejects_malformed() {
for bad in [
"Bearer xyz",
"",
"Basic !!!not-base64!!!",
"Basic bm9jb2xvbg==", // "nocolon" — no colon separator
] {
assert!(parse_header(bad).is_none(), "{bad:?} should be rejected");
}
}
}
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 →