#[must_use]
pub fn detect_mime_type(bytes: &[u8]) -> &'static str {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
return "image/png";
}
if bytes.starts_with(b"\xFF\xD8") {
return "image/jpeg";
}
if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
return "image/gif";
}
if bytes.starts_with(b"BM") {
return "image/bmp";
}
if bytes.starts_with(b"RIFF") && bytes.len() >= 12 && &bytes[8..12] == b"WEBP" {
return "image/webp";
}
if bytes.starts_with(b"%PDF") {
return "application/pdf";
}
if bytes.starts_with(b"PK\x03\x04") {
if bytes.len() >= 50 {
let central_dir = &bytes[30..bytes.len().min(50)];
if central_dir.windows(6).any(|w| w == b"word/") {
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
}
if central_dir.windows(3).any(|w| w == b"xl/") {
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}
if central_dir.windows(4).any(|w| w == b"ppt/") {
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
}
}
return "application/zip";
}
if bytes.starts_with(b"<?xml") {
return "application/xml";
}
if bytes.starts_with(b"<svg") || bytes.starts_with(b"<!DOCTYPE svg") {
return "image/svg+xml";
}
if bytes.starts_with(b"{") || bytes.starts_with(b"[") {
return "application/json";
}
"application/octet-stream"
}
#[must_use]
pub fn vec_to_data_uri(bytes: &[u8]) -> String {
let mime_type = detect_mime_type(bytes);
vec_to_data_uri_with_mime(bytes, mime_type)
}
#[must_use]
pub fn vec_to_data_uri_with_mime(bytes: &[u8], mime_type: &str) -> String {
let mut data_url = format!("data:{mime_type};base64,");
base64::Engine::encode_string(
&base64::engine::general_purpose::STANDARD,
bytes,
&mut data_url,
);
data_url
}
#[must_use]
pub fn vec_to_data_uri_value(bytes: &[u8]) -> serde_json::Value {
serde_json::Value::String(vec_to_data_uri(bytes))
}
pub fn decode_data_uri(data_url: &str) -> anyhow::Result<(&str, Vec<u8>)> {
decode_data_uri_with_limit(data_url, usize::MAX)
}
pub fn decode_data_uri_with_limit(
data_url: &str,
max_decoded_size: usize,
) -> anyhow::Result<(&str, Vec<u8>)> {
use anyhow::Context as _;
let rest = data_url
.strip_prefix("data:")
.context("Invalid data URL: missing 'data:' prefix")?;
let (mut media_type, data) = rest
.split_once(',')
.context("Invalid data URL: missing comma")?;
let is_base64 = media_type.ends_with(";base64");
let max_percent_decoded_size = if is_base64 {
max_decoded_size
.saturating_add(2)
.saturating_div(3)
.saturating_mul(4)
} else {
max_decoded_size
};
let percent_decoded = percent_encoding::percent_decode(data.as_bytes())
.take(max_percent_decoded_size.saturating_add(1))
.collect::<Vec<_>>();
anyhow::ensure!(
percent_decoded.len() <= max_percent_decoded_size,
"Decoded data exceeds the limit of {max_decoded_size} bytes"
);
let bytes = if let Some(stripped) = media_type.strip_suffix(";base64") {
media_type = stripped;
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, percent_decoded)
.context("Invalid base64 data in data URL")?
} else {
percent_decoded
};
anyhow::ensure!(
bytes.len() <= max_decoded_size,
"Decoded data exceeds the limit of {max_decoded_size} bytes"
);
Ok((media_type, bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_mime_type() {
assert_eq!(detect_mime_type(&[]), "application/octet-stream");
assert_eq!(detect_mime_type(b"\x89PNG\r\n\x1a\n"), "image/png");
assert_eq!(detect_mime_type(b"\xFF\xD8\xFF\xE0"), "image/jpeg");
assert_eq!(detect_mime_type(b"GIF87a"), "image/gif");
assert_eq!(detect_mime_type(b"GIF89a"), "image/gif");
assert_eq!(detect_mime_type(b"BM\x00\x00"), "image/bmp");
assert_eq!(detect_mime_type(b"%PDF-"), "application/pdf");
assert_eq!(
detect_mime_type(b"<svg xmlns=\"http://www.w3.org/2000/svg\">"),
"image/svg+xml"
);
assert_eq!(
detect_mime_type(b"<?xml version=\"1.0\"?><root><data>test</data></root>"),
"application/xml"
);
assert_eq!(
detect_mime_type(b"{\"key\": \"value\"}"),
"application/json"
);
assert_eq!(detect_mime_type(b"PK\x03\x04"), "application/zip");
assert_eq!(
detect_mime_type(&[0x00, 0x01, 0x02, 0x03]),
"application/octet-stream"
);
}
#[test]
fn decodes_base64_and_percent_encoded_data_urls() {
assert_eq!(
decode_data_uri("data:text/plain;base64,aGVsbG8=").unwrap(),
("text/plain", b"hello".to_vec())
);
assert_eq!(
decode_data_uri("data:text/plain,hello%20world").unwrap(),
("text/plain", b"hello world".to_vec())
);
}
#[test]
fn limits_decoded_data_url_size_before_decoding() {
assert_eq!(
decode_data_uri_with_limit("data:text/plain;base64,aGVsbG8=", 5).unwrap(),
("text/plain", b"hello".to_vec())
);
assert!(decode_data_uri_with_limit("data:text/plain;base64,aGVsbG8=", 4).is_err());
assert!(decode_data_uri_with_limit("data:text/plain,hello%20world", 10).is_err());
}
#[test]
fn rejects_invalid_data_urls() {
assert!(decode_data_uri("text/plain,hello").is_err());
assert!(decode_data_uri("data:text/plain").is_err());
assert!(decode_data_uri("data:;base64,not base64").is_err());
}
#[test]
fn test_vec_to_data_uri() {
let result = vec_to_data_uri(&[]);
assert_eq!(result, "data:application/octet-stream;base64,");
let result = vec_to_data_uri(b"Hello World");
assert_eq!(
result,
"data:application/octet-stream;base64,SGVsbG8gV29ybGQ="
);
let binary_data = [0, 1, 2, 255, 254, 253];
let result = vec_to_data_uri(&binary_data);
assert_eq!(result, "data:application/octet-stream;base64,AAEC//79");
}
#[test]
fn test_vec_to_data_uri_with_mime() {
let result = vec_to_data_uri_with_mime(b"Hello", "text/plain");
assert_eq!(result, "data:text/plain;base64,SGVsbG8=");
let result = vec_to_data_uri_with_mime(&[255, 216, 255], "image/jpeg");
assert_eq!(result, "data:image/jpeg;base64,/9j/");
let result = vec_to_data_uri_with_mime(&[], "application/json");
assert_eq!(result, "data:application/json;base64,");
}
#[test]
fn test_vec_to_data_uri_value() {
let result = vec_to_data_uri_value(b"test");
match result {
serde_json::Value::String(s) => {
assert_eq!(s, "data:application/octet-stream;base64,dGVzdA==");
}
_ => panic!("Expected String value"),
}
}
}