Files
effectcraft/crates/automation/src/base64.rs
T
Brandon ThomasandClaude Opus 5.5 c672e03160 M14.1: MCP server + agent CLI
- crates/automation (L5, no egui): hand-rolled JSON-RPC 2.0 MCP server over
  stdio (initialize, ping, tools/list, tools/call); headless backend (in-process
  Session) and bridge backend (desktop control channel). Tools: list_commands,
  execute_command, get_project/comp/layer, get/set_property, add_keyframe,
  render_frame (inline PNG), open/save_project, undo/redo; bridge-only
  screenshot, ui_* and raw control passthrough.
- effectcraft-cli: info, commands, exec, run, props, get, set, render-frame,
  mcp [--bridge PORT]; --json, --project/--demo/--empty, --save/--save-as.
- engine: prop.get, layer.tree node paths + time, Session::resolve_comp,
  render_rgba8, execute_checked (rejects unknown params, lists accepted keys).
- control channel: render.frame; engine.execute runs every engine command
  directly (no dialogs, validated params).
- docs: control-protocol.md, agents.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:40:31 -04:00

62 lines
2.2 KiB
Rust

//! Standard base64 (RFC 4648 §4, padded): MCP image content and the control channel's inline PNGs.
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn encode(data: &[u8]) -> String {
let mut s = String::with_capacity(data.len().div_ceil(3) * 4);
for c in data.chunks(3) {
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
for i in 0..4 {
s.push(if i <= c.len() { A[(n >> (18 - 6 * i) & 63) as usize] as char } else { '=' });
}
}
s
}
/// Decode, ignoring ASCII whitespace; `None` on any other invalid input.
pub fn decode(s: &str) -> Option<Vec<u8>> {
let val = |c: u8| -> Option<u32> {
Some(match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => return None,
} as u32)
};
let bytes: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
if !bytes.len().is_multiple_of(4) {
return None;
}
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for (ci, c) in bytes.chunks(4).enumerate() {
let last = ci == bytes.len() / 4 - 1;
let pad = c.iter().rev().take_while(|b| **b == b'=').count();
if pad > 2 || (pad > 0 && !last) {
return None;
}
let mut n = 0u32;
for &b in &c[..4 - pad] {
n = n << 6 | val(b)?;
}
n <<= 6 * pad as u32;
out.extend_from_slice(&[(n >> 16) as u8, (n >> 8) as u8, n as u8][..3 - pad]);
}
Some(out)
}
#[cfg(test)]
mod tests {
#[test]
fn rfc4648_vectors() {
for (i, o) in [("", ""), ("f", "Zg=="), ("fo", "Zm8="), ("foo", "Zm9v"), ("foob", "Zm9vYg=="), ("fooba", "Zm9vYmE="), ("foobar", "Zm9vYmFy")] {
assert_eq!(super::encode(i.as_bytes()), o);
assert_eq!(super::decode(o).unwrap(), i.as_bytes());
}
assert!(super::decode("Zm9v!").is_none());
let all: Vec<u8> = (0..=255).collect();
assert_eq!(super::decode(&super::encode(&all)).unwrap(), all);
}
}