Streaming-oriented multipart/form-data parser and encoder for MoonBit.
moon add eisem/moonpart@0.1.0///|
test "read a binary file field" {
let body = b"--bin\r\nContent-Disposition: form-data; name=\"file\"; filename=\"raw.bin\"\r\n\r\n\x00\xff\r\n--bin--\r\n"
match @moonpart.decode_bytes("bin", body) {
Ok(
[
@moonpart.StreamEvent::PartBegin(_),
@moonpart.StreamEvent::PartData(data),
..,
]
) => assert_eq(data.length(), 2)
_ => fail("invalid multipart body")
}
}///|
test "parse one field" {
let parser = @moonpart.Parser::new("MyBoundary")
ignore(
parser.feed(
"--MyBoundary\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nMoonBit\r\n--MyBoundary--\r\n",
),
)
match parser.finish() {
Ok(parts) => println(parts[0].body)
Err(_) => println("invalid multipart body")
}
}///|
test "handle an upload incrementally" {
let parser = @moonpart.StreamingParser::from_content_type(
"multipart/form-data; boundary=upload",
)
guard parser is Ok(parser) else { fail("invalid content type") }
let chunks = [
b"--upload\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.bin\"\r\n\r\nabc",
b"\r\n--upload--\r\n",
]
for chunk in chunks {
guard parser.feed_bytes(chunk) is Ok(events) else {
fail("invalid multipart body")
}
for event in events {
match event {
@moonpart.StreamEvent::PartData(data) =>
// Write data to a file, socket, or other application sink here.
assert_true(data.length() > 0)
_ => ()
}
}
}
assert_true(parser.finish() is Ok(_))
}///|
test "encode binary data incrementally" {
let encoder = @moonpart.StreamingEncoder::new("out")
let chunks = []
let events = [
@moonpart.StreamEvent::PartBegin([
@moonpart.Header::{
name: "Content-Disposition",
value: "form-data; name=\"file\"; filename=\"a.bin\"",
},
]),
@moonpart.StreamEvent::PartData(b"\x00\xff"),
@moonpart.StreamEvent::PartEnd,
@moonpart.StreamEvent::Finished,
]
for event in events {
guard encoder.push(event) is Ok(chunk) else {
fail("invalid encoder event")
}
chunks.push(chunk)
}
assert_true(chunks[1] == b"\x00\xff")
assert_true(chunks[3] == b"--out--\r\n")
}| Use case | API | Body storage |
|---|---|---|
| Small text-oriented forms | Parser | Complete body as String |
| Binary body decoded at once | decode_bytes | Events contain raw Bytes |
| Incremental protocol parsing | StreamingParser | Possible-boundary suffix only |
| Small forms collected in memory | FormDataCollector | Text and file bodies |
| Large files sent to a sink | StreamingFormDecoder | Text fields and file metadata |
| Incremental binary encoding | StreamingEncoder | Current header chunk only |
moon run cmd/mainmoon fmt
moon info
moon check --target all
moon build --target all
moon test --target all
moon coverage analyze
moon bench --target native
moon run cmd/mainpub(all) suberror MultipartError {
InvalidBoundary
InvalidContentType
BodyTooLarge(Int)
MissingOpeningBoundary
MissingHeaderTerminator
InvalidHeader(String)
DuplicateHeader(String)
MissingContentDisposition
InvalidContentDisposition
MissingFieldName
HeaderTooLarge(Int)
HeaderCountExceeded(Int)
PartCountExceeded(Int)
PartTooLarge(Int)
InvalidStreamEvent
FieldTooLarge(Int)
FileTooLarge(Int)
FieldCountExceeded(Int)
FileCountExceeded(Int)
UnexpectedField(String)
SinkFailure(String)
MissingClosingBoundary
UnexpectedBoundary
} derive(Debug)pub struct FormDataCollector {
entries : Array[FormEntry]
buffer : Buffer
limits : FormDataLimits
allowed_fields : Array[String]?
field_size_limits : Array[FieldSizeLimit]
current_part : FormPartMetadata?
current_size : Int
field_count : Int
file_count : Int
finished : Bool
}fn FormDataCollector::push(self : FormDataCollector, event : StreamEvent) -> Result[Unit, MultipartError]fn FormDataCollector::with_allowed_fields(allowed_fields : Array[String], limits? : FormDataLimits) -> FormDataCollectorfn FormDataCollector::with_field_size_limits(limits : FormDataLimits, field_size_limits : Array[FieldSizeLimit]) -> FormDataCollectorpub(all) struct FormDataLimits {
max_field_size : Int
max_file_size : Int
max_field_count : Int
max_file_count : Int
}type FormPartMetadatapub struct Parser {
boundary : String
max_body_size : Int
max_part_count : Int
max_header_size : Int
max_header_count : Int
buffer : String
failed : MultipartError?
}fn Parser::from_content_type(content_type : String, max_body_size? : Int) -> Result[Parser, MultipartError]pub(all) struct StreamLimits {
max_body_size : Int
max_part_count : Int
max_header_size : Int
max_header_count : Int
max_part_size : Int
}pub(all) struct StreamedFormData {
fields : Array[TextField]
files : Array[StreamedFile]
} derive(Debug)fn StreamingEncoder::from_content_type(content_type : String) -> Result[StreamingEncoder, MultipartError]fn StreamingEncoder::push(self : StreamingEncoder, event : StreamEvent) -> Result[Bytes, MultipartError]pub struct StreamingFormCollector {
fields : Array[TextField]
files : Array[StreamedFile]
text_buffer : Buffer
limits : FormDataLimits
allowed_fields : Array[String]
field_size_limits : Array[FieldSizeLimit]
sink : (FileSinkEvent) -> Result[Unit, MultipartError]
current_part : FormPartMetadata?
current_size : Int
field_count : Int
file_count : Int
finished : Bool
failed : MultipartError?
}fn StreamingFormCollector::finish(self : StreamingFormCollector) -> Result[StreamedFormData, MultipartError]fn StreamingFormCollector::new(sink : (FileSinkEvent) -> Result[Unit, MultipartError], limits? : FormDataLimits, allowed_fields? : Array[String], field_size_limits? : Array[FieldSizeLimit]) -> StreamingFormCollectorfn StreamingFormCollector::push(self : StreamingFormCollector, event : StreamEvent) -> Result[Unit, MultipartError]pub struct StreamingFormDecoder {
parser : StreamingParser
collector : StreamingFormCollector
failed : MultipartError?
}fn StreamingFormDecoder::feed_bytes(self : StreamingFormDecoder, chunk : Bytes) -> Result[Unit, MultipartError]fn StreamingFormDecoder::finish(self : StreamingFormDecoder) -> Result[StreamedFormData, MultipartError]fn StreamingFormDecoder::from_content_type(content_type : String, sink : (FileSinkEvent) -> Result[Unit, MultipartError], stream_limits? : StreamLimits, form_limits? : FormDataLimits, allowed_fields? : Array[String], field_size_limits? : Array[FieldSizeLimit]) -> Result[StreamingFormDecoder, MultipartError]fn StreamingFormDecoder::new(boundary : String, sink : (FileSinkEvent) -> Result[Unit, MultipartError], stream_limits? : StreamLimits, form_limits? : FormDataLimits, allowed_fields? : Array[String], field_size_limits? : Array[FieldSizeLimit]) -> StreamingFormDecoderpub struct StreamingParser {
boundary : String
opening : Bytes
marker : Bytes
limits : StreamLimits
pending : Bytes
state : StreamState
received_size : Int
part_count : Int
current_part_size : Int
failed : MultipartError?
}fn StreamingParser::feed_bytes(self : StreamingParser, chunk : Bytes) -> Result[Array[StreamEvent], MultipartError]fn StreamingParser::from_content_type(content_type : String, limits? : StreamLimits) -> Result[StreamingParser, MultipartError]pub(all) struct UploadedFile {
field_name : String
filename : String
content_type : String
data : Bytes
} derive(Debug)fn filename_basename(filename : String) -> StringInstall
Download zipStreaming-oriented multipart/form-data parser and encoder for MoonBit.