Skip to main content

structured_cms/mime/
essence.rs

1//! Known type/subtype comparisons inside the existing incremental grammar.
2
3use super::{Head, Syntax};
4use structured_asn1::memory::{MemoryError, Work};
5
6struct Pattern<'a> {
7    media_type: &'a [u8],
8    subtype: &'a [u8],
9    matches: bool,
10}
11
12impl Pattern<'_> {
13    fn compare(remaining: &mut &[u8], matches: &mut bool, byte: u8) {
14        if !*matches {
15            return;
16        }
17        if remaining.is_empty() {
18            *matches = false;
19            return;
20        }
21        if !remaining[0].eq_ignore_ascii_case(&byte) {
22            *matches = false;
23        }
24        *remaining = &remaining[1..];
25    }
26}
27
28impl<const N: usize> Head for [Pattern<'_>; N] {
29    fn cost(&self) -> Result<u64, MemoryError> {
30        u64::try_from(N)
31            .ok()
32            .and_then(|count| count.checked_mul(2))
33            .ok_or(MemoryError::WorkExceeded)
34    }
35    fn type_byte(&mut self, byte: u8) {
36        for pattern in self {
37            Pattern::compare(&mut pattern.media_type, &mut pattern.matches, byte);
38        }
39    }
40    fn subtype_byte(&mut self, byte: u8) {
41        for pattern in self {
42            Pattern::compare(&mut pattern.subtype, &mut pattern.matches, byte);
43        }
44    }
45}
46
47/// One incremental MIME grammar pass and fixed, allocation-free comparisons
48/// against known type/subtype pairs. Parameters remain syntax-checked but do
49/// not participate in this comparison. This is not full MIME-value equality.
50///
51/// RFC2045 ยงยง2 and 5.1: type/subtype are case-insensitive; comments and linear
52/// whitespace separate tokens. Token fragments cannot be joined across CFWS.
53/// <https://www.rfc-editor.org/rfc/rfc2045.html#section-5.1>
54pub struct Essences<'a, const N: usize, const XML_NORMALIZED: bool = false> {
55    syntax: Syntax<XML_NORMALIZED>,
56    patterns: [Pattern<'a>; N],
57}
58
59impl<'a, const N: usize, const XML_NORMALIZED: bool> Essences<'a, N, XML_NORMALIZED> {
60    /// Borrows the caller's known pairs without allocation or registration
61    /// lookup. The number of pairs is a compile-time mechanism parameter.
62    #[must_use]
63    pub fn new(patterns: [(&'a str, &'a str); N]) -> Self {
64        Self {
65            syntax: Syntax::new(),
66            patterns: patterns.map(|(media_type, subtype)| Pattern {
67                media_type: media_type.as_bytes(),
68                subtype: subtype.as_bytes(),
69                matches: true,
70            }),
71        }
72    }
73
74    /// Admits grammar and all comparison work before consuming this fragment.
75    /// A refusal leaves both syntax and comparisons unchanged.
76    pub fn feed<W: Work>(
77        &mut self,
78        bytes: impl AsRef<[u8]>,
79        work: &W,
80    ) -> Result<bool, MemoryError> {
81        self.syntax.scan(bytes.as_ref(), work, &mut self.patterns)
82    }
83
84    /// Whether the entire original input is a complete MIME value.
85    #[must_use]
86    pub fn complete(&self) -> bool {
87        self.syntax.complete()
88    }
89
90    /// Compares a known pair only if the complete original grammar succeeded.
91    /// None denotes an out-of-range caller index, not a missing media type.
92    #[must_use]
93    pub fn matches(&self, index: usize) -> Option<bool> {
94        self.patterns.get(index).map(|pattern| {
95            self.complete()
96                && pattern.matches
97                && pattern.media_type.is_empty()
98                && pattern.subtype.is_empty()
99        })
100    }
101}