mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
- 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>
62 lines
2.2 KiB
Rust
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);
|
|
}
|
|
}
|