nostd-multipart 0.1.1

Heap-free, panic-free, streaming multipart/form-data parser and encoder for no_std
Documentation
//! Streams a 50 MiB multipart upload through a 1 KiB buffer with constant memory.
//!
//! The upload source is generated lazily (no 50 MiB buffer anywhere): the encoder
//! writes the boundary/header lines, a deterministic generator produces the file
//! bytes, and the consumer verifies the parsed file against the same generator.
//!
//! Run with `cargo run --release --features std --example stream_upload`.

use std::io::Read;
use std::time::Instant;

use nostd_multipart::{content_disposition, Encoder, Event, Parser, Part};

const FILE_LEN: u64 = 50 * 1024 * 1024;
const BOUNDARY: &[u8] = b"----nostd-multipart-example-6f2a";
const BUF: usize = 1024;

/// Deterministic byte generator (xorshift32), used by both sides.
struct Gen(u32);
impl Gen {
    fn next_byte(&mut self) -> u8 {
        let mut x = self.0;
        x ^= x << 13;
        x ^= x >> 17;
        x ^= x << 5;
        self.0 = x;
        (x >> 24) as u8
    }
}

/// Lazily produced request body: [part header] [FILE_LEN generated bytes] [close].
struct UploadSource {
    enc: Encoder<'static>,
    stage: u8,
    header: Vec<u8>,
    header_pos: usize,
    gen: Gen,
    remaining: u64,
}

const PART: Part<'static> = Part {
    name: b"firmware",
    filename: Some(b"app.bin"),
    content_type: Some(b"application/octet-stream"),
};

impl UploadSource {
    fn new() -> Self {
        let mut enc = Encoder::new(BOUNDARY).unwrap();
        let mut header = vec![0u8; 256];
        let (n, done) = enc.write_part_header(&PART, &mut header).unwrap();
        assert!(done);
        header.truncate(n);
        UploadSource {
            enc,
            stage: 0,
            header,
            header_pos: 0,
            gen: Gen(0x1234_5678),
            remaining: FILE_LEN,
        }
    }
}

impl Read for UploadSource {
    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
        match self.stage {
            0 => {
                let n = (self.header.len() - self.header_pos).min(out.len());
                out[..n].copy_from_slice(&self.header[self.header_pos..self.header_pos + n]);
                self.header_pos += n;
                if self.header_pos == self.header.len() {
                    self.stage = 1;
                }
                Ok(n)
            }
            1 => {
                let n = (self.remaining.min(out.len() as u64)) as usize;
                for b in &mut out[..n] {
                    *b = self.gen.next_byte();
                }
                self.remaining -= n as u64;
                if self.remaining == 0 {
                    self.stage = 2;
                }
                Ok(n)
            }
            2 => {
                let (n, done) = self.enc.write_close(out);
                if done {
                    self.stage = 3;
                }
                Ok(n)
            }
            _ => Ok(0),
        }
    }
}

fn main() {
    let mut source = UploadSource::new();
    let expected_len = Encoder::new(BOUNDARY)
        .unwrap()
        .content_length(&[(PART, FILE_LEN)]);

    // --- the receiving side: everything below lives on the stack -------------
    let mut parser: Parser<'_, 256, 8> = Parser::new(BOUNDARY).unwrap();
    let mut buf = [0u8; BUF];
    let (mut start, mut end, mut eof) = (0usize, 0usize, false);
    assert!(BUF >= parser.min_buffer());

    let mut check = Gen(0x1234_5678);
    let mut file_bytes: u64 = 0;
    let mut body_bytes: u64 = 0;
    let mut is_firmware = false;
    let mut parse_calls: u64 = 0;
    let t0 = Instant::now();

    loop {
        parse_calls += 1;
        let (consumed, ev) = parser.parse(&buf[start..end], eof).expect("malformed body");
        start += consumed;
        body_bytes += consumed as u64;
        match ev {
            Some(Event::Header { name, value })
                if name.eq_ignore_ascii_case(b"content-disposition") =>
            {
                let cd = content_disposition(value).expect("bad Content-Disposition");
                is_firmware = cd.name().is_some_and(|n| n == "firmware");
                let mut fname = [0u8; 64];
                let fname = cd
                    .filename()
                    .and_then(|f| f.decode_into(&mut fname))
                    .unwrap_or(b"");
                println!(
                    "part name={:?} filename={:?}",
                    cd.name().map(|n| n.raw()),
                    std::str::from_utf8(fname)
                );
            }
            Some(Event::Data(d)) if is_firmware => {
                // "Write to flash": here, verify against the generator instead.
                for &b in d {
                    assert_eq!(b, check.next_byte(), "data mismatch at byte {file_bytes}");
                    file_bytes += 1;
                }
            }
            Some(Event::Finished) => break,
            _ => {}
        }
        if consumed == 0 && ev.is_none() {
            // Need more input: compact, then read.
            buf.copy_within(start..end, 0);
            end -= start;
            start = 0;
            let n = source.read(&mut buf[end..]).unwrap();
            if n == 0 {
                eof = true;
            }
            end += n;
        }
    }

    let secs = t0.elapsed().as_secs_f64();
    println!("file bytes      : {file_bytes} (expected {FILE_LEN})");
    println!("body bytes      : {body_bytes} (Content-Length {expected_len})");
    println!("parse calls     : {parse_calls}");
    println!("buffer          : {BUF} bytes");
    println!(
        "parser          : {} bytes (size_of)",
        core::mem::size_of::<Parser<'_, 256, 8>>()
    );
    println!(
        "throughput      : {:.0} MiB/s",
        body_bytes as f64 / 1048576.0 / secs
    );
    assert_eq!(file_bytes, FILE_LEN);
    assert_eq!(body_bytes, expected_len);
}