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