1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// SPDX-License-Identifier: AGPL-3.0-only
//! Shareable scenario permalinks (self-contained Base64, RFC 4648).
//!
//! A scenario in the playground is a TOML document. To share it as a URL, the TOML is
//! Base64-encoded into a `?s=` query parameter; opening that URL decodes it back. This
//! module provides the codec — a dependency-free RFC 4648 Base64 (the standard `+/`
//! alphabet for interop, and the URL-safe `-_` alphabet without padding for clean query
//! strings) and the `encode_scenario` / `decode_scenario` wrappers.
//!
//! Keeping it in the engine (rather than only in JavaScript) means the same encoding is
//! available to the Rust, CLI, Python, and WebAssembly surfaces, and is unit-tested
//! against the canonical RFC 4648 vectors.
const STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
/// Base64-encode `bytes` with the given 64-symbol `alphabet`, padding with `=` when
/// `pad` is set (standard) or leaving the output unpadded (URL-safe query strings).
fn encode(bytes: &[u8], alphabet: &[u8; 64], pad: bool) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.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;
out.push(alphabet[(n >> 18 & 63) as usize] as char);
out.push(alphabet[(n >> 12 & 63) as usize] as char);
if chunk.len() > 1 {
out.push(alphabet[(n >> 6 & 63) as usize] as char);
} else if pad {
out.push('=');
}
if chunk.len() > 2 {
out.push(alphabet[(n & 63) as usize] as char);
} else if pad {
out.push('=');
}
}
out
}
/// Decode a Base64 string under the given `alphabet`, accepting either alphabet's
/// padding and ignoring `=` and whitespace. Returns `None` on an invalid symbol or a
/// truncated (single-character) final group.
fn decode(s: &str, alphabet: &[u8; 64]) -> Option<Vec<u8>> {
let mut rev = [255u8; 256];
for (i, &c) in alphabet.iter().enumerate() {
rev[c as usize] = i as u8;
}
let chars: Vec<u8> = s
.bytes()
.filter(|&c| c != b'=' && !c.is_ascii_whitespace())
.collect();
let mut out = Vec::with_capacity(chars.len() / 4 * 3);
for chunk in chars.chunks(4) {
if chunk.len() < 2 {
return None;
}
let mut vals = [0u32; 4];
for (i, &c) in chunk.iter().enumerate() {
let v = rev[c as usize];
if v == 255 {
return None;
}
vals[i] = v as u32;
}
let n = (vals[0] << 18) | (vals[1] << 12) | (vals[2] << 6) | vals[3];
out.push((n >> 16 & 0xff) as u8);
if chunk.len() >= 3 {
out.push((n >> 8 & 0xff) as u8);
}
if chunk.len() >= 4 {
out.push((n & 0xff) as u8);
}
}
Some(out)
}
/// Standard RFC 4648 Base64 encode (alphabet `A–Za–z0–9+/`, `=` padding).
pub fn base64_encode(bytes: &[u8]) -> String {
encode(bytes, STD, true)
}
/// Standard RFC 4648 Base64 decode.
pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
decode(s, STD)
}
/// Encode a scenario TOML document into a URL-safe, unpadded permalink token suitable
/// for a `?s=` query parameter (no `+`, `/`, or `=` to escape).
pub fn encode_scenario(toml: &str) -> String {
encode(toml.as_bytes(), URL, false)
}
/// Decode a permalink token back into the scenario TOML, or `None` if the token is not
/// valid Base64 or not valid UTF-8.
pub fn decode_scenario(token: &str) -> Option<String> {
let bytes = decode(token, URL)?;
String::from_utf8(bytes).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc4648_standard_vectors() {
// The canonical RFC 4648 §10 test vectors.
let cases = [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
for (plain, b64) in cases {
assert_eq!(base64_encode(plain.as_bytes()), b64, "encode {plain}");
assert_eq!(
base64_decode(b64).unwrap(),
plain.as_bytes(),
"decode {b64}"
);
}
}
#[test]
fn url_safe_round_trips_a_scenario() {
let toml =
"kind = \"clock\"\n[time]\nstep_s = 1.0\n[gnss]\nshape = \"outage\"\n# share me 𝛂β\n";
let token = encode_scenario(toml);
// URL-safe and unpadded: nothing that needs percent-escaping in a query string.
assert!(!token.contains('+') && !token.contains('/') && !token.contains('='));
assert_eq!(decode_scenario(&token).as_deref(), Some(toml));
}
#[test]
fn decode_rejects_invalid_symbols() {
// A standard-alphabet '+' is not in the URL-safe alphabet.
assert!(decode_scenario("abc+def").is_none());
// Non-base64 punctuation is rejected outright.
assert!(base64_decode("not valid!@#").is_none());
}
#[test]
fn empty_and_binary_round_trip() {
assert_eq!(base64_encode(&[]), "");
assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
// All byte values survive a round trip through the standard codec.
let bytes: Vec<u8> = (0..=255u8).collect();
assert_eq!(base64_decode(&base64_encode(&bytes)).unwrap(), bytes);
}
}