nostd_multipart/
encoder.rs1use crate::chars::is_valid_boundary;
5use crate::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub struct Part<'a> {
10 pub name: &'a [u8],
13 pub filename: Option<&'a [u8]>,
15 pub content_type: Option<&'a [u8]>,
17}
18
19#[derive(Debug, Clone)]
35pub struct Encoder<'b> {
36 boundary: &'b [u8],
37 first: bool,
38 pos: usize,
39}
40
41impl<'b> Encoder<'b> {
42 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 #[must_use]
59 pub const fn boundary(&self) -> &'b [u8] {
60 self.boundary
61 }
62
63 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 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 #[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
117fn 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
135fn 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
148fn 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
175fn 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
184pub 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
194fn 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}