1use std::io::Cursor;
40
41use oxideav_core::{BytesSource, Error, Result};
42
43use crate::uri;
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct DataUri {
48 pub mediatype: String,
53 pub base64: bool,
55 pub data: Vec<u8>,
57}
58
59pub fn parse(uri_str: &str) -> Result<DataUri> {
64 let (scheme, rest) = uri::split(uri_str);
65 if scheme != "data" {
66 return Err(Error::invalid(format!(
67 "data driver invoked on non-data URI: {uri_str}"
68 )));
69 }
70 let comma = rest
71 .find(',')
72 .ok_or_else(|| Error::invalid("data: URI missing comma separator"))?;
73 let (header, payload) = rest.split_at(comma);
74 let payload = &payload[1..]; let (mediatype, base64) = if let Some(stripped) = strip_base64_suffix(header) {
79 (stripped, true)
80 } else {
81 (header, false)
82 };
83
84 let data = if base64 {
85 decode_base64(payload)?
86 } else {
87 percent_decode(payload)?
88 };
89
90 Ok(DataUri {
91 mediatype: mediatype.to_string(),
92 base64,
93 data,
94 })
95}
96
97pub fn open_data(uri_str: &str) -> Result<Box<dyn BytesSource>> {
100 let parsed = parse(uri_str)?;
101 Ok(Box::new(Cursor::new(parsed.data)))
102}
103
104fn strip_base64_suffix(header: &str) -> Option<&str> {
107 let bytes = header.as_bytes();
111 const MARKER: &[u8] = b";base64";
112 if bytes.len() < MARKER.len() {
113 return None;
114 }
115 let tail = &bytes[bytes.len() - MARKER.len()..];
116 if tail.eq_ignore_ascii_case(MARKER) {
117 Some(&header[..header.len() - MARKER.len()])
119 } else {
120 None
121 }
122}
123
124fn percent_decode(s: &str) -> Result<Vec<u8>> {
129 let bytes = s.as_bytes();
130 let mut out = Vec::with_capacity(bytes.len());
131 let mut i = 0;
132 while i < bytes.len() {
133 let b = bytes[i];
134 if b == b'%' {
135 if i + 2 >= bytes.len() {
136 return Err(Error::invalid(format!(
137 "data:// percent-encoding truncated at offset {i}"
138 )));
139 }
140 let hi = hex_nibble(bytes[i + 1]).ok_or_else(|| {
141 Error::invalid(format!(
142 "data:// percent-encoding: non-hex digit {:?}",
143 bytes[i + 1] as char
144 ))
145 })?;
146 let lo = hex_nibble(bytes[i + 2]).ok_or_else(|| {
147 Error::invalid(format!(
148 "data:// percent-encoding: non-hex digit {:?}",
149 bytes[i + 2] as char
150 ))
151 })?;
152 out.push((hi << 4) | lo);
153 i += 3;
154 } else {
155 out.push(b);
156 i += 1;
157 }
158 }
159 Ok(out)
160}
161
162fn hex_nibble(b: u8) -> Option<u8> {
163 match b {
164 b'0'..=b'9' => Some(b - b'0'),
165 b'a'..=b'f' => Some(b - b'a' + 10),
166 b'A'..=b'F' => Some(b - b'A' + 10),
167 _ => None,
168 }
169}
170
171fn decode_base64(s: &str) -> Result<Vec<u8>> {
174 let mut clean: Vec<u8> = Vec::with_capacity(s.len());
177 for &b in s.as_bytes() {
178 if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
179 continue;
180 }
181 clean.push(b);
182 }
183 if clean.len() % 4 != 0 {
184 return Err(Error::invalid(format!(
185 "data:// base64 payload length {} is not a multiple of 4",
186 clean.len()
187 )));
188 }
189 let mut out: Vec<u8> = Vec::with_capacity(clean.len() / 4 * 3);
190 let mut chunk = [0u8; 4];
191 let mut i = 0;
192 while i < clean.len() {
193 let mut pad = 0;
194 for j in 0..4 {
195 let b = clean[i + j];
196 if b == b'=' {
197 pad += 1;
198 chunk[j] = 0;
199 } else {
200 if pad > 0 {
201 return Err(Error::invalid(
202 "data:// base64 padding character before end of payload",
203 ));
204 }
205 chunk[j] = b64_value(b).ok_or_else(|| {
206 Error::invalid(format!("data:// base64: invalid character {:?}", b as char))
207 })?;
208 }
209 }
210 if pad > 2 {
211 return Err(Error::invalid(
212 "data:// base64: more than two padding characters in a group",
213 ));
214 }
215 if pad > 0 && i + 4 < clean.len() {
217 return Err(Error::invalid("data:// base64: padding before final group"));
218 }
219 let triple = (u32::from(chunk[0]) << 18)
220 | (u32::from(chunk[1]) << 12)
221 | (u32::from(chunk[2]) << 6)
222 | u32::from(chunk[3]);
223 out.push(((triple >> 16) & 0xff) as u8);
224 if pad < 2 {
225 out.push(((triple >> 8) & 0xff) as u8);
226 }
227 if pad < 1 {
228 out.push((triple & 0xff) as u8);
229 }
230 i += 4;
231 }
232 Ok(out)
233}
234
235fn b64_value(b: u8) -> Option<u8> {
236 match b {
239 b'A'..=b'Z' => Some(b - b'A'),
240 b'a'..=b'z' => Some(b - b'a' + 26),
241 b'0'..=b'9' => Some(b - b'0' + 52),
242 b'+' => Some(62),
243 b'/' => Some(63),
244 _ => None,
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use std::io::Read;
251
252 use super::*;
253
254 #[test]
255 fn rfc2397_example_inline_text() {
256 let p = parse("data:,A%20brief%20note").unwrap();
258 assert_eq!(p.mediatype, "");
259 assert!(!p.base64);
260 assert_eq!(p.data, b"A brief note");
261 }
262
263 #[test]
264 fn rfc2397_example_base64_image_prefix() {
265 let p = parse("data:image/gif;base64,SGVsbG8=").unwrap();
269 assert_eq!(p.mediatype, "image/gif");
270 assert!(p.base64);
271 assert_eq!(p.data, b"Hello");
272 }
273
274 #[test]
275 fn empty_mediatype_no_payload() {
276 let p = parse("data:,").unwrap();
277 assert_eq!(p.mediatype, "");
278 assert_eq!(p.data, b"");
279 }
280
281 #[test]
282 fn mediatype_with_parameter() {
283 let p = parse("data:text/plain;charset=US-ASCII,abc").unwrap();
285 assert_eq!(p.mediatype, "text/plain;charset=US-ASCII");
286 assert!(!p.base64);
287 assert_eq!(p.data, b"abc");
288 }
289
290 #[test]
291 fn base64_marker_case_insensitive() {
292 let p = parse("data:application/octet-stream;BASE64,SGVsbG8=").unwrap();
293 assert!(p.base64);
294 assert_eq!(p.data, b"Hello");
295 }
296
297 #[test]
298 fn base64_with_internal_whitespace() {
299 let p = parse("data:;base64,SG Vs\nbG8=").unwrap();
301 assert_eq!(p.data, b"Hello");
302 }
303
304 #[test]
305 fn percent_decode_high_byte() {
306 let p = parse("data:,%FF%00%7E").unwrap();
307 assert_eq!(p.data, [0xff, 0x00, 0x7e]);
308 }
309
310 #[test]
311 fn missing_comma_rejected() {
312 let r = parse("data:text/plain;base64");
313 assert!(r.is_err());
314 }
315
316 #[test]
317 fn truncated_percent_rejected() {
318 let r = parse("data:,%F");
319 assert!(r.is_err());
320 }
321
322 #[test]
323 fn bad_hex_rejected() {
324 let r = parse("data:,%ZZ");
325 assert!(r.is_err());
326 }
327
328 #[test]
329 fn base64_bad_length_rejected() {
330 let r = parse("data:;base64,SGV");
332 assert!(r.is_err());
333 }
334
335 #[test]
336 fn base64_padding_in_middle_rejected() {
337 let r = parse("data:;base64,SGVs=GVs");
339 assert!(r.is_err());
340 }
341
342 #[test]
343 fn base64_invalid_char_rejected() {
344 let r = parse("data:;base64,SG!s");
345 assert!(r.is_err());
346 }
347
348 #[test]
349 fn wrong_scheme_rejected() {
350 let r = parse("file:///tmp/x");
351 assert!(r.is_err());
352 let r = open_data("mem://x");
353 assert!(r.is_err());
354 }
355
356 #[test]
357 fn open_data_returns_readable_cursor() {
358 let mut r = open_data("data:,hello").unwrap();
359 let mut buf = Vec::new();
360 r.read_to_end(&mut buf).unwrap();
361 assert_eq!(buf, b"hello");
362 }
363
364 #[test]
365 fn base64_full_alphabet_roundtrip() {
366 let payload: Vec<u8> = (0u8..=255).collect();
370 let encoded = encode_b64(&payload);
372 let uri = format!("data:application/octet-stream;base64,{encoded}");
373 let parsed = parse(&uri).unwrap();
374 assert_eq!(parsed.data, payload);
375 }
376
377 fn encode_b64(input: &[u8]) -> String {
381 const ALPHA: &[u8; 64] =
382 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
383 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
384 let mut i = 0;
385 while i + 3 <= input.len() {
386 let b0 = input[i];
387 let b1 = input[i + 1];
388 let b2 = input[i + 2];
389 out.push(ALPHA[(b0 >> 2) as usize] as char);
390 out.push(ALPHA[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
391 out.push(ALPHA[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
392 out.push(ALPHA[(b2 & 0x3f) as usize] as char);
393 i += 3;
394 }
395 match input.len() - i {
396 0 => {}
397 1 => {
398 let b0 = input[i];
399 out.push(ALPHA[(b0 >> 2) as usize] as char);
400 out.push(ALPHA[((b0 & 0x03) << 4) as usize] as char);
401 out.push('=');
402 out.push('=');
403 }
404 2 => {
405 let b0 = input[i];
406 let b1 = input[i + 1];
407 out.push(ALPHA[(b0 >> 2) as usize] as char);
408 out.push(ALPHA[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
409 out.push(ALPHA[((b1 & 0x0f) << 2) as usize] as char);
410 out.push('=');
411 }
412 _ => unreachable!(),
413 }
414 out
415 }
416}