Skip to main content

structured_cms/
mime.rs

1//! Allocation-free MIME media types and original header-field framing.
2
3use structured_asn1::memory::{MemoryError, Work};
4
5mod headers;
6pub use headers::media_type_or_headers;
7mod essence;
8pub use essence::Essences;
9
10trait Head {
11    fn cost(&self) -> Result<u64, MemoryError>;
12    fn type_byte(&mut self, byte: u8);
13    fn subtype_byte(&mut self, byte: u8);
14}
15impl Head for () {
16    fn cost(&self) -> Result<u64, MemoryError> {
17        Ok(0)
18    }
19    fn type_byte(&mut self, _: u8) {}
20    fn subtype_byte(&mut self, _: u8) {}
21}
22
23#[derive(Clone, Copy)]
24enum Phase {
25    TypeStart,
26    Type,
27    Slash,
28    SubtypeStart,
29    Subtype,
30    Parameters,
31    AttributeStart,
32    Attribute,
33    Equals,
34    ValueStart,
35    Value,
36    AfterValue,
37}
38
39#[derive(Clone, Copy)]
40enum Context {
41    Normal,
42    Comment { depth: usize, escaped: bool },
43    Quoted { escaped: bool },
44}
45
46/// Incremental allocation-free RFC2045 media-type syntax.
47///
48/// The default consumes original MIME octets and requires CRLF folding.
49/// `XML_NORMALIZED = true` additionally accepts XML's already normalized LF.
50/// This does not resolve registered media types or compare their semantics.
51///
52/// ```
53/// use std::sync::Arc;
54/// use structured_asn1::memory::{Memory, MemoryAccount};
55/// use structured_cms::mime::Syntax;
56/// let work = Memory::new(Arc::new(MemoryAccount::new(0, 1000)));
57/// let mut value = Syntax::<false>::new();
58/// assert!(value.feed("text/plain; charset=", &work).unwrap());
59/// assert!(!value.complete());
60/// assert!(value.feed("utf-8", &work).unwrap());
61/// assert!(value.complete());
62/// ```
63pub struct Syntax<const XML_NORMALIZED: bool = false> {
64    valid: bool,
65    phase: Phase,
66    context: Context,
67    folding: u8,
68}
69
70impl<const XML_NORMALIZED: bool> Syntax<XML_NORMALIZED> {
71    /// Begins a value without allocation or input work.
72    #[must_use]
73    pub const fn new() -> Self {
74        Self {
75            valid: true,
76            phase: Phase::TypeStart,
77            context: Context::Normal,
78            folding: 0,
79        }
80    }
81
82    // RFC2045 §5.1 token/tspecials plus RFC822 §3.3/3.4 comments,
83    // quoted-pair and folding. This is MIME syntax, not HTTP's restricted-name
84    // grammar, registration lookup or media-type equivalence/normalization.
85    // https://www.rfc-editor.org/rfc/rfc2045.html#section-5.1
86    // https://www.rfc-editor.org/rfc/rfc822.html#section-3.3
87    fn byte<H: Head>(&mut self, byte: u8, head: &mut H) -> bool {
88        if !byte.is_ascii() || (byte == b'\n' && self.folding != 1 && !XML_NORMALIZED) {
89            return false;
90        }
91        if self.folding != 0 {
92            if self.folding == 1 && byte == b'\n' {
93                self.folding = 2;
94                return true;
95            }
96            if self.folding == 2 && matches!(byte, b' ' | b'\t') {
97                self.folding = 0;
98                return true;
99            }
100            return false;
101        }
102        match self.context {
103            Context::Comment { depth, escaped } => {
104                if matches!(byte, b'\r' | b'\n') {
105                    self.context = Context::Comment {
106                        depth,
107                        escaped: false,
108                    };
109                    self.folding = if byte == b'\r' { 1 } else { 2 };
110                } else if escaped {
111                    self.context = Context::Comment {
112                        depth,
113                        escaped: false,
114                    };
115                } else if byte == b'\\' {
116                    self.context = Context::Comment {
117                        depth,
118                        escaped: true,
119                    };
120                } else if byte == b'(' {
121                    let Some(depth) = depth.checked_add(1) else {
122                        return false;
123                    };
124                    self.context = Context::Comment {
125                        depth,
126                        escaped: false,
127                    };
128                } else if byte == b')' {
129                    self.context = if depth == 1 {
130                        Context::Normal
131                    } else {
132                        Context::Comment {
133                            depth: depth - 1,
134                            escaped: false,
135                        }
136                    };
137                }
138                true
139            }
140            Context::Quoted { escaped } => {
141                if matches!(byte, b'\r' | b'\n') {
142                    self.context = Context::Quoted { escaped: false };
143                    self.folding = if byte == b'\r' { 1 } else { 2 };
144                } else if escaped {
145                    self.context = Context::Quoted { escaped: false };
146                } else if byte == b'\\' {
147                    self.context = Context::Quoted { escaped: true };
148                } else if byte == b'"' {
149                    self.context = Context::Normal;
150                    self.phase = Phase::AfterValue;
151                }
152                true
153            }
154            Context::Normal => self.normal(byte, head),
155        }
156    }
157
158    fn normal<H: Head>(&mut self, byte: u8, head: &mut H) -> bool {
159        match byte {
160            b' ' | b'\t' => {
161                self.gap();
162                true
163            }
164            b'\r' | b'\n' => {
165                self.gap();
166                // XML 1.0 §2.11 normalizes physical CRLF to LF before
167                // Node::Text; character references can still carry CRLF.
168                // https://www.w3.org/TR/REC-xml/#sec-line-ends
169                self.folding = if byte == b'\r' { 1 } else { 2 };
170                true
171            }
172            b'(' => {
173                self.gap();
174                self.context = Context::Comment {
175                    depth: 1,
176                    escaped: false,
177                };
178                true
179            }
180            b'"' if matches!(self.phase, Phase::ValueStart) => {
181                self.context = Context::Quoted { escaped: false };
182                true
183            }
184            b'/' if matches!(self.phase, Phase::Type | Phase::Slash) => {
185                self.phase = Phase::SubtypeStart;
186                true
187            }
188            b';' if matches!(
189                self.phase,
190                Phase::Subtype | Phase::Parameters | Phase::Value | Phase::AfterValue
191            ) =>
192            {
193                self.phase = Phase::AttributeStart;
194                true
195            }
196            b'=' if matches!(self.phase, Phase::Attribute | Phase::Equals) => {
197                self.phase = Phase::ValueStart;
198                true
199            }
200            _ if token(byte) => {
201                self.phase = match self.phase {
202                    Phase::TypeStart | Phase::Type => {
203                        head.type_byte(byte);
204                        Phase::Type
205                    }
206                    Phase::SubtypeStart | Phase::Subtype => {
207                        head.subtype_byte(byte);
208                        Phase::Subtype
209                    }
210                    Phase::AttributeStart | Phase::Attribute => Phase::Attribute,
211                    Phase::ValueStart | Phase::Value => Phase::Value,
212                    _ => return false,
213                };
214                true
215            }
216            _ => false,
217        }
218    }
219
220    fn gap(&mut self) {
221        self.phase = match self.phase {
222            Phase::Type => Phase::Slash,
223            Phase::Subtype => Phase::Parameters,
224            Phase::Attribute => Phase::Equals,
225            Phase::Value => Phase::AfterValue,
226            other => other,
227        };
228    }
229
230    /// Admits the whole fragment before changing state, then consumes it.
231    /// Syntax failure is sticky across subsequent fragments. Resource failure
232    /// leaves the state unchanged. Input may be borrowed str or byte slices.
233    pub fn feed<W: Work>(
234        &mut self,
235        value: impl AsRef<[u8]>,
236        execution: &W,
237    ) -> Result<bool, MemoryError> {
238        self.scan(value.as_ref(), execution, &mut ())
239    }
240
241    fn scan<W: Work, H: Head>(
242        &mut self,
243        value: &[u8],
244        execution: &W,
245        head: &mut H,
246    ) -> Result<bool, MemoryError> {
247        // Each byte has bounded scalar dispatch only. Comment nesting is an
248        // iterative counter, never stack frames, allocations or recursive calls.
249        let per_byte = head
250            .cost()?
251            .checked_add(4)
252            .ok_or(MemoryError::WorkExceeded)?;
253        let cost = u64::try_from(value.len())
254            .ok()
255            .and_then(|n| n.checked_mul(per_byte))
256            .ok_or(MemoryError::WorkExceeded)?;
257        execution.charge_work(cost)?;
258        if !self.valid {
259            return Ok(false);
260        }
261        for &byte in value {
262            if !self.byte(byte, head) {
263                self.valid = false;
264                return Ok(false);
265            }
266        }
267        Ok(true)
268    }
269
270    /// A complete MIME value, with no open parameter, quote, comment or fold.
271    #[must_use]
272    pub fn complete(&self) -> bool {
273        self.valid
274            && self.folding == 0
275            && matches!(self.context, Context::Normal)
276            && matches!(
277                self.phase,
278                Phase::Subtype | Phase::Parameters | Phase::Value | Phase::AfterValue
279            )
280    }
281}
282
283fn token(byte: u8) -> bool {
284    matches!(byte, 33..=126)
285        && !matches!(
286            byte,
287            b'(' | b')'
288                | b'<'
289                | b'>'
290                | b'@'
291                | b','
292                | b';'
293                | b':'
294                | b'\\'
295                | b'"'
296                | b'/'
297                | b'['
298                | b']'
299                | b'?'
300                | b'='
301        )
302}
303
304impl<const XML_NORMALIZED: bool> Default for Syntax<XML_NORMALIZED> {
305    fn default() -> Self {
306        Self::new()
307    }
308}