1use serde::ser::{Serialize, SerializeMap, Serializer};
12use serde_json::Value;
13
14use crate::errors;
15use crate::spec::XyzSchema;
16use crate::spec::schema::Schema;
17
18#[derive(Debug, Clone, PartialEq)]
21pub enum Block {
22 Text {
23 text: String,
24 },
25 Image {
27 mime_type: String,
28 data: String,
29 },
30 Audio {
31 mime_type: String,
32 data: String,
33 },
34}
35
36#[derive(Debug, Clone, PartialEq, Default)]
39pub struct Blocks {
40 pub content: Vec<Block>,
41}
42
43impl Blocks {
44 pub fn new(content: Vec<Block>) -> Self {
45 Blocks { content }
46 }
47
48 pub fn text(text: impl Into<String>) -> Self {
49 Blocks::new(vec![Block::Text { text: text.into() }])
50 }
51
52 pub fn image(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
53 Blocks::new(vec![Block::Image {
54 mime_type: mime_type.into(),
55 data: data.into(),
56 }])
57 }
58
59 pub fn audio(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
60 Blocks::new(vec![Block::Audio {
61 mime_type: mime_type.into(),
62 data: data.into(),
63 }])
64 }
65
66 pub fn push(&mut self, block: Block) {
68 self.content.push(block);
69 }
70}
71
72impl Serialize for Block {
73 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
74 let mut map = s.serialize_map(Some(3))?;
75 match self {
76 Block::Text { text } => {
77 map.serialize_entry("type", "text")?;
78 map.serialize_entry("text", text)?;
79 }
80 Block::Image { mime_type, data } | Block::Audio { mime_type, data } => {
81 let kind = if matches!(self, Block::Image { .. }) {
82 "image"
83 } else {
84 "audio"
85 };
86 map.serialize_entry("type", kind)?;
87 map.serialize_entry("mimeType", mime_type)?;
88 map.serialize_entry("data", data)?;
89 }
90 }
91 map.end()
92 }
93}
94
95impl Serialize for Blocks {
96 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
97 let mut map = s.serialize_map(Some(1))?;
98 map.serialize_entry("content", &self.content)?;
99 map.end()
100 }
101}
102
103impl XyzSchema for Blocks {
104 fn xyz_schema() -> Option<Schema> {
105 None
107 }
108}
109
110pub fn extract(v: &Value) -> Option<Vec<Block>> {
113 let obj = v.as_object()?;
114 if obj.len() != 1 {
115 return None;
116 }
117 let items = obj.get("content")?.as_array()?;
118 let mut out = Vec::with_capacity(items.len());
119 for item in items {
120 out.push(block_from_value(item)?);
121 }
122 Some(out)
123}
124
125fn block_from_value(v: &Value) -> Option<Block> {
126 let o = v.as_object()?;
127 match o.get("type")?.as_str()? {
128 "text" => Some(Block::Text {
129 text: o.get("text")?.as_str()?.to_string(),
130 }),
131 "image" => Some(Block::Image {
132 mime_type: o.get("mimeType")?.as_str()?.to_string(),
133 data: o.get("data")?.as_str()?.to_string(),
134 }),
135 "audio" => Some(Block::Audio {
136 mime_type: o.get("mimeType")?.as_str()?.to_string(),
137 data: o.get("data")?.as_str()?.to_string(),
138 }),
139 _ => None,
140 }
141}
142
143pub fn ext_for_mime(mime: &str) -> &'static str {
145 match mime {
146 "image/png" => "png",
147 "image/jpeg" => "jpg",
148 "image/gif" => "gif",
149 "image/webp" => "webp",
150 "image/bmp" => "bmp",
151 "image/svg+xml" => "svg",
152 "audio/mpeg" => "mp3",
153 "audio/wav" => "wav",
154 "audio/ogg" => "ogg",
155 _ => "bin",
156 }
157}
158
159const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
160
161pub fn decode_base64(s: &str) -> errors::Result<Vec<u8>> {
164 fn val(c: u8) -> Option<u32> {
165 B64.iter().position(|&b| b == c).map(|i| i as u32)
166 }
167 let mut out = Vec::with_capacity(s.len() / 4 * 3);
168 let mut acc: u32 = 0;
169 let mut bits = 0u8;
170 for b in s.bytes() {
171 if b.is_ascii_whitespace() {
172 continue;
173 }
174 if b == b'=' {
175 break; }
177 let Some(v) = val(b) else {
178 return Err(errors::new(
179 errors::Kind::Internal,
180 format!("invalid base64 payload: byte {b:#04x}"),
181 ));
182 };
183 acc = (acc << 6) | v;
184 bits += 6;
185 if bits >= 8 {
186 bits -= 8;
187 out.push((acc >> bits) as u8);
188 }
189 }
190 Ok(out)
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde_json::json;
197
198 #[test]
199 fn envelope_round_trip() {
200 let b = Blocks::new(vec![
201 Block::Text {
202 text: "hello".to_string(),
203 },
204 Block::Image {
205 mime_type: "image/png".into(),
206 data: "aGVsbG8=".into(),
207 },
208 ]);
209 let v = serde_json::to_value(&b).unwrap();
210 assert_eq!(
211 v,
212 json!({"content":[
213 {"type":"text","text":"hello"},
214 {"type":"image","mimeType":"image/png","data":"aGVsbG8="}
215 ]})
216 );
217 let back = extract(&v).unwrap();
218 assert_eq!(back, b.content);
219 assert_eq!(decode_base64("aGVsbG8=").unwrap(), b"hello");
220 assert_eq!(decode_base64("aGVs bG8=").unwrap(), b"hello"); assert!(decode_base64("not*b64").is_err());
222 }
223
224 #[test]
225 fn extract_rejects_non_envelopes() {
226 assert!(extract(&json!({"msg": "hi"})).is_none());
228 assert!(extract(&json!({"content": "text"})).is_none());
229 assert!(extract(&json!({"content": [{"type": "text"}]})).is_none());
230 assert!(extract(&json!({"content": [{"type": "video", "data": "x"}]})).is_none());
231 assert!(extract(&json!({"content": [{"type": "text", "text": "a"}]})).is_some());
232 assert!(extract(&json!({"content": [], "extra": 1})).is_none());
233 assert_eq!(extract(&json!({"content": []})), Some(vec![]));
235 }
236
237 #[test]
238 fn mime_extensions() {
239 assert_eq!(ext_for_mime("image/png"), "png");
240 assert_eq!(ext_for_mime("audio/mpeg"), "mp3");
241 assert_eq!(ext_for_mime("application/x-unknown"), "bin");
242 }
243}