A Rust backend that owns the sessions and an Android app that reads them. The server spawns and adopts CLI processes, normalises everything they emit into one event model, keeps the transcript, and serves it over pinned TLS on a WireGuard interface; the phone streams that, replies, sends images, and imports conversations the machine already has. `AGENTS.md` is the working guide -- what runs where, what has been measured, and the faults that were expensive to find. `PLAN.md` is the design record. History before this point was squashed away. It was a personal project's running commentary and carried a name and a couple of machine paths that have no business in a public repository; the tree is what mattered and the tree is here.
63 lines
2.1 KiB
Rust
63 lines
2.1 KiB
Rust
//! The image types that travel between the phone, the session
|
|
//! directories, and a driver's dialect.
|
|
//!
|
|
//! Media type and file extension have to agree in four places -- storing
|
|
//! an upload, serving it back, handing it to a CLI as a content block, and
|
|
//! saving one a tool produced -- so the table lives here once. The
|
|
//! *default* for an unrecognized type is deliberately not here: it differs
|
|
//! by direction (a phone upload is a photo, a produced image is a
|
|
//! screenshot), so each caller states its own.
|
|
|
|
/// Media type to extension. Only the types Claude's API accepts as image
|
|
/// content blocks -- anything else has nowhere to go.
|
|
const IMAGE_TYPES: [(&str, &str); 4] = [
|
|
("image/png", "png"),
|
|
("image/jpeg", "jpg"),
|
|
("image/gif", "gif"),
|
|
("image/webp", "webp"),
|
|
];
|
|
|
|
/// The extension to store `media_type` under, or `None` if it isn't an
|
|
/// image type this server handles.
|
|
pub fn extension_for(media_type: &str) -> Option<&'static str> {
|
|
IMAGE_TYPES
|
|
.iter()
|
|
.find(|(known, _)| *known == media_type)
|
|
.map(|(_, extension)| *extension)
|
|
}
|
|
|
|
/// The media type of a stored file, from its extension. Names are
|
|
/// server-generated (`<hex>.<extension>`, always lowercase), so no case
|
|
/// folding is needed; `None` for anything else.
|
|
pub fn media_type_for(name: &str) -> Option<&'static str> {
|
|
let (_, extension) = name.rsplit_once('.')?;
|
|
IMAGE_TYPES
|
|
.iter()
|
|
.find(|(_, known)| *known == extension)
|
|
.map(|(media_type, _)| *media_type)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_two_directions_agree() {
|
|
for (media_type, extension) in IMAGE_TYPES {
|
|
assert_eq!(extension_for(media_type), Some(extension));
|
|
assert_eq!(
|
|
media_type_for(&format!("abc123.{extension}")),
|
|
Some(media_type)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_types_are_the_callers_problem() {
|
|
assert_eq!(extension_for("application/pdf"), None);
|
|
assert_eq!(media_type_for("abc123.pdf"), None);
|
|
// No extension at all -- not "the whole name is the extension".
|
|
assert_eq!(media_type_for("abc123"), None);
|
|
}
|
|
}
|