ferrin_message/
data_url.rs1use base64::Engine;
11use base64::alphabet::STANDARD;
12use base64::engine::DecodePaddingMode;
13use base64::engine::GeneralPurpose;
14use base64::engine::GeneralPurposeConfig;
15use bytes::Bytes;
16use ferrin_spec::MediaType;
17use percent_encoding::percent_decode_str;
18
19use crate::error::InvalidDataContentError;
20
21pub const DEFAULT_MEDIA_TYPE: &str = "text/plain;charset=US-ASCII";
23
24const LENIENT_BASE64: GeneralPurpose = GeneralPurpose::new(
25 &STANDARD,
26 GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
27);
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct DataUrl {
32 pub media_type: MediaType,
34 pub data: Bytes,
36 pub is_base64: bool,
38}
39
40#[must_use]
42pub fn is_data_url(input: &str) -> bool {
43 input.len() >= 5 && input.as_bytes()[..5].eq_ignore_ascii_case(b"data:")
44}
45
46pub fn parse(input: &str) -> Result<DataUrl, InvalidDataContentError> {
53 if !is_data_url(input) {
54 return Err(InvalidDataContentError::new(
55 "data url must start with `data:`",
56 ));
57 }
58 let rest = &input[5..];
59 let Some((header, payload)) = rest.split_once(',') else {
60 return Err(InvalidDataContentError::new(
61 "data url has no `,` separator",
62 ));
63 };
64
65 let mut is_base64 = false;
66 let mut media_type = String::new();
67 for (index, segment) in header.split(';').enumerate() {
68 let segment = segment.trim();
69 if index == 0 {
70 media_type.push_str(segment);
71 } else if segment.eq_ignore_ascii_case("base64") {
72 is_base64 = true;
73 } else if !segment.is_empty() {
74 if media_type.is_empty() {
75 media_type.push_str("text/plain");
76 }
77 media_type.push(';');
78 media_type.push_str(segment);
79 }
80 }
81 if media_type.is_empty() {
82 media_type.push_str(DEFAULT_MEDIA_TYPE);
83 }
84
85 let decoded = percent_decode_str(payload).collect::<Vec<u8>>();
86 let data = if is_base64 {
87 let compact: Vec<u8> = decoded
88 .into_iter()
89 .filter(|byte| !byte.is_ascii_whitespace())
90 .collect();
91 LENIENT_BASE64
92 .decode(compact)
93 .map(Bytes::from)
94 .map_err(|error| {
95 InvalidDataContentError::new("data url payload is not valid base64")
96 .with_cause(error)
97 })?
98 } else {
99 Bytes::from(decoded)
100 };
101
102 Ok(DataUrl {
103 media_type: MediaType::new(media_type),
104 data,
105 is_base64,
106 })
107}