//! 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 (`.`, 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); } }