Skip to main content

nostd_multipart/
encoder.rs

1//! Encoder (DESIGN.md §11): writes boundary lines and part headers into a caller
2//! buffer, resumably; part data is written by the caller.
3
4use crate::chars::is_valid_boundary;
5use crate::Error;
6
7/// Description of one part for the [`Encoder`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub struct Part<'a> {
10    /// The form field name (escaped per WHATWG: `"` → `%22`, CR → `%0D`, LF → `%0A`;
11    /// additionally `\` → `\\`, see DESIGN.md §11).
12    pub name: &'a [u8],
13    /// Optional file name (escaped the same way).
14    pub filename: Option<&'a [u8]>,
15    /// Optional `Content-Type` value; must not contain CR or LF.
16    pub content_type: Option<&'a [u8]>,
17}
18
19/// Streaming `multipart/form-data` encoder.
20///
21/// ```
22/// use nostd_multipart::{Encoder, Part};
23/// let mut enc = Encoder::new(b"xyz").unwrap();
24/// let mut out = [0u8; 128];
25/// let part = Part { name: b"file", filename: Some(b"a.bin"), content_type: Some(b"application/octet-stream") };
26/// let (n, done) = enc.write_part_header(&part, &mut out).unwrap();
27/// assert!(done);
28/// assert_eq!(&out[..n], &b"--xyz\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n"[..]);
29/// // ... write the file data yourself ...
30/// let (n, done) = enc.write_close(&mut out);
31/// assert!(done);
32/// assert_eq!(&out[..n], b"\r\n--xyz--\r\n");
33/// ```
34#[derive(Debug, Clone)]
35pub struct Encoder<'b> {
36    boundary: &'b [u8],
37    first: bool,
38    pos: usize,
39}
40
41impl<'b> Encoder<'b> {
42    /// Creates an encoder for `boundary` (rule 1).
43    ///
44    /// # Errors
45    /// [`Error::Boundary`] if the boundary is invalid.
46    pub fn new(boundary: &'b [u8]) -> Result<Self, Error> {
47        if !is_valid_boundary(boundary) {
48            return Err(Error::Boundary);
49        }
50        Ok(Self {
51            boundary,
52            first: true,
53            pos: 0,
54        })
55    }
56
57    /// The boundary.
58    #[must_use]
59    pub const fn boundary(&self) -> &'b [u8] {
60        self.boundary
61    }
62
63    /// Writes the next bytes of a part's prefix (CRLF for parts after the first,
64    /// `--boundary` CRLF, headers, empty line) into `out`. Returns the number of
65    /// bytes written and whether the prefix is complete. If not complete, flush
66    /// `out[..written]` and call again with the *same* `part`.
67    ///
68    /// # Errors
69    /// [`Error::Header`] if `part.content_type` contains CR or LF.
70    pub fn write_part_header(
71        &mut self,
72        part: &Part<'_>,
73        out: &mut [u8],
74    ) -> Result<(usize, bool), Error> {
75        if part
76            .content_type
77            .is_some_and(|ct| ct.iter().any(|&b| b == b'\r' || b == b'\n'))
78        {
79            return Err(Error::Header);
80        }
81        let (written, done) = copy_stream(
82            part_header_bytes(self.boundary, self.first, part),
83            &mut self.pos,
84            out,
85        );
86        if done {
87            self.first = false;
88        }
89        Ok((written, done))
90    }
91
92    /// Writes the next bytes of the close delimiter `CRLF --boundary-- CRLF`
93    /// into `out`, resumably like [`Self::write_part_header`].
94    pub fn write_close(&mut self, out: &mut [u8]) -> (usize, bool) {
95        copy_stream(close_bytes(self.boundary), &mut self.pos, out)
96    }
97
98    /// Exact length of the complete body for `parts` (each with its data length),
99    /// as this encoder would produce it from the start. Saturates at `u64::MAX`.
100    #[must_use]
101    pub fn content_length(&self, parts: &[(Part<'_>, u64)]) -> u64 {
102        let mut total = 0u64;
103        let mut first = true;
104        for (part, data_len) in parts {
105            let header = count_u64(part_header_bytes(self.boundary, first, part));
106            total = total.saturating_add(header).saturating_add(*data_len);
107            first = false;
108        }
109        total.saturating_add(count_u64(close_bytes(self.boundary)))
110    }
111}
112
113fn count_u64(it: impl Iterator<Item = u8>) -> u64 {
114    u64::try_from(it.count()).unwrap_or(u64::MAX)
115}
116
117/// Copies `stream[*pos..]` into `out`; returns `(written, done)` and advances
118/// `*pos` (reset to 0 when done).
119fn copy_stream(stream: impl Iterator<Item = u8>, pos: &mut usize, out: &mut [u8]) -> (usize, bool) {
120    let mut it = stream.skip(*pos).peekable();
121    let mut written = 0usize;
122    for (slot, b) in out.iter_mut().zip(&mut it) {
123        *slot = b;
124        written = written.saturating_add(1);
125    }
126    let done = it.peek().is_none();
127    *pos = if done { 0 } else { pos.saturating_add(written) };
128    (written, done)
129}
130
131fn bytes(s: &[u8]) -> impl Iterator<Item = u8> + '_ {
132    s.iter().copied()
133}
134
135/// WHATWG escaping of one byte of a name or filename, plus `\` → `\\` so the
136/// value survives RFC 2183 quoted-pair decoding (DESIGN.md §11).
137fn escape(b: u8) -> impl Iterator<Item = u8> {
138    let (buf, len) = match b {
139        b'"' => (*b"%22", 3),
140        b'\r' => (*b"%0D", 3),
141        b'\n' => (*b"%0A", 3),
142        b'\\' => (*b"\\\\\0", 2),
143        b => ([b, 0, 0], 1),
144    };
145    buf.into_iter().take(len)
146}
147
148/// The byte stream of a part's prefix (§11).
149fn part_header_bytes<'a>(
150    boundary: &'a [u8],
151    first: bool,
152    part: &Part<'a>,
153) -> impl Iterator<Item = u8> + 'a {
154    let lead: &'static [u8] = if first { b"" } else { b"\r\n" };
155    bytes(lead)
156        .chain(bytes(b"--"))
157        .chain(bytes(boundary))
158        .chain(bytes(b"\r\nContent-Disposition: form-data; name=\""))
159        .chain(bytes(part.name).flat_map(escape))
160        .chain(bytes(b"\""))
161        .chain(part.filename.into_iter().flat_map(|f| {
162            bytes(b"; filename=\"")
163                .chain(bytes(f).flat_map(escape))
164                .chain(bytes(b"\""))
165        }))
166        .chain(bytes(b"\r\n"))
167        .chain(part.content_type.into_iter().flat_map(|ct| {
168            bytes(b"Content-Type: ")
169                .chain(bytes(ct))
170                .chain(bytes(b"\r\n"))
171        }))
172        .chain(bytes(b"\r\n"))
173}
174
175/// The byte stream of the close delimiter line.
176fn close_bytes(boundary: &[u8]) -> impl Iterator<Item = u8> + '_ {
177    bytes(b"\r\n--")
178        .chain(bytes(boundary))
179        .chain(bytes(b"--\r\n"))
180}
181
182const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
183
184/// Maps `N` random bytes (e.g. from a hardware RNG) to a valid boundary of `N`
185/// bchars from `A–Z a–z 0–9 - _` (6 bits of entropy per byte).
186///
187/// # Errors
188/// [`Error::Boundary`] if `N` is 0 or greater than 70.
189pub fn boundary_from_random<const N: usize>(random: &[u8; N]) -> Result<[u8; N], Error> {
190    let mut out = [0u8; N];
191    fill_boundary(random, &mut out).map(|()| out)
192}
193
194/// Non-generic worker of [`boundary_from_random`]: one copy in the binary for
195/// all `N`, and llvm-cov does not count an uncovered per-`N` instantiation.
196fn fill_boundary(random: &[u8], out: &mut [u8]) -> Result<(), Error> {
197    if random.is_empty() || random.len() > 70 {
198        return Err(Error::Boundary);
199    }
200    for (o, &r) in out.iter_mut().zip(random) {
201        *o = ALPHABET.get(usize::from(r & 63)).copied().unwrap_or(b'x');
202    }
203    Ok(())
204}