moon-multipart

面向 MoonBit HTTP 生态的流式 multipart/form-data 解析与生成库,支持 RFC 7578 标准。

multipart
form-data
rfc7578
http
upload
streaming
moonbit
moon add GCodinggo/moon-multipart@0.3.1
Download zip
Author
Version
0.3.1
License
Apache-2.0
Last updated
14 days ago
Downloads
4
README

#moon-multipart

面向 MoonBit HTTP 生态的流式 multipart/form-data 解析与生成库,完整实现 RFC 7578 标准。

#项目维护者

本项目由郭康泰开发和维护。

GitHub 用户名为 GCodinggo

#特性

  • RFC 7578 合规 — 完整支持 multipart/form-data 编码格式
  • 流式解析 — 增量处理上传数据,PartData 逐块输出,不将整个文件加载到内存
  • 跨分块边界检测 — 正确处理被网络分块拆分的 boundary 分隔符
  • 安全限制 — 6 维度可配置限制(part 数量、header 大小、字段大小、文件大小、总大小、文件名长度)
  • 请求头注入防护 — 拒绝字段名、文件名、Content-Type 和自定义 header 中的 CRLF
  • 路径穿越防护 — 检测并拒绝 ../\、盘符、UNC 路径、Windows 保留设备名、尾随空格或点号、null 字节等危险文件名
  • 安全文件名工具safe_filename()validate_filename()unique_filename()
  • 严格/兼容双模式 — Strict 模式拒绝非标准扩展(如 filename*),Compatible 模式宽松接受
  • 流式 WriterStreamingWriter 支持 begin_part / write_chunk / end_part 分块添加数据,并提供带校验的 try_begin_part
  • 高层 APIparse_all() 一键解析返回 MultipartForm,支持同名字段/多文件

#快速开始

#安装

moon add GCodinggo/moon-multipart

#解析 multipart 请求体(高层 API)

let content_type = "multipart/form-data; boundary=----WebKitFormBoundary"
let boundary = parse_boundary_from_content_type(content_type)?
let form = parse_all(body, boundary, ParseOptions::default())?
// 读取字段
let username = form.field("username")
// 读取同名多值
let tags = form.field_values("tag")
// 读取文件
match form.file("avatar") {
Some(part) => { let data = part.data(); let fname = part.filename() }
None => { }
}
// 读取同名多文件
let images = form.files("image")

#解析 multipart 请求体(流式低层 API)

let boundary = parse_boundary_from_content_type(content_type)? let parser = Parser::new(boundary, ParseOptions::default()) for chunk in incoming_chunks { let events = parser.feed(chunk)? for event in events { match event { PartBegin(name, filename, content_type) => { /* 新 part 开始 */ } PartData(data) => { /* body 数据块,直接写文件/哈希 */ } PartEnd => { /* part 结束 */ } Finished => { /* 解析完成 */ } } } } parser.finish()?

#生成 multipart 请求体(基础 API)

let writer = MultipartWriter::new()
writer.add_field("username", "alice")
writer.add_file("avatar", "photo.png", Some("image/png"), image_bytes)
let (boundary, body) = writer.finish()
// Content-Type: multipart/form-data; boundary=<boundary>

#生成 multipart 请求体(流式 API)

let sw = StreamingWriter::new(boundary)
sw.begin_part("file", Some("large.bin"), Some("application/octet-stream"))
for chunk in read_file_in_chunks("large.bin") {
sw.write_chunk(chunk)
}
sw.end_part()
let body = sw.finish()

#安全文件名处理

// 验证
validate_filename("photo.jpg", 255)?

// 清理危险字符
let safe = safe_filename("../../../etc/passwd") // → "______etc_passwd"

// 生成唯一名称
let unique = unique_filename("photo.jpg") // → "photo_3A7F2C1D.jpg"

#API 参考

#类型

类型说明
Limits安全限制配置,提供 default() / strict() / permissive()
ParseModeStrict / Compatible
ParseOptions解析选项:mode + limits + reject_filename_star
Part解析后的 part:Field(String, String) / File(String, String, String?, Bytes)
MultipartForm高层解析结果,有序 parts,支持 field() / field_values() / file() / files()
MultipartError16 种错误类型
ParseEvent流式事件:PartBegin / PartData / PartEnd / Finished
Parser流式解析器
MultipartWriter基础请求体生成器
StreamingWriter流式请求体生成器(begin_part / write_chunk / end_part)

#核心函数

Content-Type 解析

  • parse_boundary_from_content_type(content_type) -> Result[String, MultipartError]
  • validate_boundary(boundary) -> Result[String, MultipartError]

Content-Disposition 解析

  • parse_content_disposition(header, ParseOptions) -> Result[(String, String?), MultipartError]
  • parse_header_line(line) -> (String, String)?

Parser

  • Parser::new(boundary, ParseOptions) -> Parser
  • Parser::feed(self, chunk) -> Result[Array[ParseEvent], MultipartError]
  • Parser::finish(self) -> Result[Array[ParseEvent], MultipartError]
  • parse_all(body, boundary, ParseOptions) -> Result[MultipartForm, MultipartError]

MultipartWriter

  • MultipartWriter::new() -> MultipartWriter
  • MultipartWriter::with_boundary(boundary) -> MultipartWriter
  • MultipartWriter::add_field(self, name, value) -> Unit
  • MultipartWriter::add_file(self, name, filename, content_type?, data) -> Unit
  • MultipartWriter::try_add_field(self, name, value) -> Result[Unit, MultipartError]
  • MultipartWriter::try_add_file(self, name, filename, content_type?, data) -> Result[Unit, MultipartError]
  • MultipartWriter::try_add_file_with_headers(self, name, filename, content_type?, extra_headers, data) -> Result[Unit, MultipartError]
  • MultipartWriter::finish(self) -> (String, Bytes)

StreamingWriter

  • StreamingWriter::new(boundary) -> StreamingWriter
  • StreamingWriter::begin_part(self, name, filename?, content_type?) -> Unit
  • StreamingWriter::try_begin_part(self, name, filename?, content_type?) -> Result[Unit, MultipartError]
  • StreamingWriter::write_chunk(self, data) -> Unit
  • StreamingWriter::end_part(self) -> Unit
  • StreamingWriter::finish(self) -> Bytes

安全文件名

  • is_dangerous_filename(filename) -> Bool
  • validate_filename(filename, max_len) -> Result[String, MultipartError]
  • safe_filename(original) -> String
  • unique_filename(original) -> String
  • validate_header_component(value, label) -> Result[Unit, MultipartError]
  • validate_custom_header(name, value) -> Result[Unit, MultipartError]

#安全限制

限制默认值strict()
最大 part 数量1,00050
最大 header 大小8 KB4 KB
最大字段大小1 MB64 KB
最大文件大小100 MB10 MB
最大总请求体500 MB50 MB
最大文件名长度255255

#项目规模

  • 3,650 行有效 MoonBit 代码
  • 96 个测试用例
  • 8 个源码模块

#RFC 7578 合规矩阵

项目状态
CRLF + -- + boundary 分隔符
首 boundary 可选前缀 CRLF
boundary 参数必需
引号 boundary
boundary 长度 1-70
closing boundary -- 后缀
Content-Disposition 必需
name 参数必需
filename 参数可选
同名字段多次出现✅ MultipartForm
preamble 忽略
epilogue 忽略
filename* (RFC 5987)✅ Strict 拒绝 / Compat 接受
路径穿越防护
流式解析(不缓存文件)✅ PartData 逐块输出

#Contributing

Development and verification instructions are available in CONTRIBUTING.md.

#许可证

Apache-2.0

#
BoundaryMatch

pub(all) enum BoundaryMatch {
Delimiter(Bytes, Bool)
Pending(Bytes)
NoData
}

Result of feeding bytes to the boundary matcher.

#
BoundaryMatcher

pub(all) struct BoundaryMatcher {
delimiter : Bytes
delimiter_len : Int
buf :
Buffer

}

A streaming boundary matcher that detects \r\n--<boundary> delimiters in an incoming byte stream. Handles split boundaries across chunk boundaries.

#
BoundaryMatcher::feed

fn BoundaryMatcher::feed(self : BoundaryMatcher, chunk : Bytes) -> BoundaryMatch

Feed a chunk of bytes into the boundary matcher.

#
BoundaryMatcher::finish

fn BoundaryMatcher::finish(self : BoundaryMatcher) -> Bytes

Feed the final chunk and flush any remaining buffered bytes.

#
BoundaryMatcher::new

fn BoundaryMatcher::new(boundary : String) -> BoundaryMatcher

Create a new boundary matcher for the given boundary string.

#
Limits

pub(all) struct Limits {
max_parts : Int
max_header_size : Int
max_field_size : Int
max_file_size : Int
max_total_size : Int
max_filename_len : Int
} derive(
Debug
)

Configurable security limits for multipart parsing. Set any field to -1 for unlimited.

#
Limits::default

fn Limits::default() -> Limits

#
Limits::permissive

fn Limits::permissive() -> Limits

#
Limits::strict

fn Limits::strict() -> Limits

#
MultipartError

pub(all) enum MultipartError {
MissingBoundary
InvalidBoundary(String)
HeaderTooLarge(Int)
FieldTooLarge(String, Int)
FileTooLarge(String, Int)
TotalSizeExceeded(Int)
TooManyParts(Int)
MalformedHeader(String)
MissingName
MissingDisposition
PathTraversal(String)
FilenameTooLong(String, Int)
IncompleteBody
InvalidEncoding(String)
NonCompliantHeader(String)
ProcessingError(String)
} derive(
Debug
)

Errors that can occur during multipart parsing or generation.

#
MultipartError::to_string

fn MultipartError::to_string(self : MultipartError) -> String

#
MultipartForm

pub(all) struct MultipartForm {
parts : Array[Part]
} derive(
Debug
)

High-level result of parsing a complete multipart body. Preserves insertion order and supports same-name fields/files.

#
MultipartForm::field

fn MultipartForm::field(self : MultipartForm, name : String) -> String?

Get the first field value with the given name (convenience).

#
MultipartForm::field_values

fn MultipartForm::field_values(self : MultipartForm, name : String) -> Array[String]

Get ALL field values with the given name (supports same-name fields).

#
MultipartForm::file

fn MultipartForm::file(self : MultipartForm, name : String) -> Part?

Get the first file with the given field name (convenience).

#
MultipartForm::files

fn MultipartForm::files(self : MultipartForm, name : String) -> Array[Part]

Get ALL file parts with the given field name (supports multi-file upload).

#
MultipartForm::len

fn MultipartForm::len(self : MultipartForm) -> Int

Total number of parts.

#
MultipartForm::new

#
MultipartWriter

pub(all) struct MultipartWriter {
boundary : String
buf :
Buffer

part_count : Int
}

A multipart/form-data body generator (RFC 7578 compliant).

#
MultipartWriter::add_field

fn MultipartWriter::add_field(self : MultipartWriter, name : String, value : String) -> Unit

Add a text form field.

#
MultipartWriter::add_file

fn MultipartWriter::add_file(self : MultipartWriter, name : String, filename : String, content_type : String?, data : Bytes) -> Unit

Add a file upload.

#
MultipartWriter::finish

fn MultipartWriter::finish(self : MultipartWriter) -> (String, Bytes)

Finalize and return the boundary and complete body bytes.

#
MultipartWriter::get_boundary

fn MultipartWriter::get_boundary(self : MultipartWriter) -> String

Get the boundary string for use in the Content-Type header.

#
MultipartWriter::new

Create a new multipart writer with a generated boundary.

#
MultipartWriter::part_count

fn MultipartWriter::part_count(self : MultipartWriter) -> Int

Get the current part count.

#
MultipartWriter::try_add_field

fn MultipartWriter::try_add_field(self : MultipartWriter, name : String, value : String) -> Result[Unit, MultipartError]

Add a text form field, rejecting values that could inject a header line.

#
MultipartWriter::try_add_file

fn MultipartWriter::try_add_file(self : MultipartWriter, name : String, filename : String, content_type : String?, data : Bytes) -> Result[Unit, MultipartError]

Add a file upload, rejecting CRLF in header parameters.

#
MultipartWriter::try_add_file_with_headers

fn MultipartWriter::try_add_file_with_headers(self : MultipartWriter, name : String, filename : String, content_type : String?, extra_headers : Array[(String, String)], data : Bytes) -> Result[Unit, MultipartError]

Add a file upload with validated custom part headers.

#
MultipartWriter::with_boundary

fn MultipartWriter::with_boundary(boundary : String) -> MultipartWriter

Create a multipart writer with a specific boundary string.

#
ParseEvent

pub(all) enum ParseEvent {
PartBegin(String, String?, String?)
PartData(Bytes)
PartEnd
Finished
} derive(
Debug
)

Events emitted during streaming multipart parsing.

#
ParseMode

pub(all) enum ParseMode {
Strict
Compatible
} derive(Eq,
Debug
)

Parsing strictness mode.

#
ParseOptions

pub(all) struct ParseOptions {
mode : ParseMode
limits : Limits
reject_filename_star : Bool
} derive(
Debug
)

Options controlling parser behavior.

#
ParseOptions::compatible

fn ParseOptions::compatible() -> ParseOptions

#
ParseOptions::default

fn ParseOptions::default() -> ParseOptions

#
ParseOptions::strict_rfc

fn ParseOptions::strict_rfc() -> ParseOptions

#
ParsePhase

type ParsePhase

#
Parser

pub(all) struct Parser {
boundary : String
delimiter : Bytes
boundary_prefix : Bytes
delimiter_len : Int
options : ParseOptions
buf :
Buffer

phase : ParsePhase
body_buf :
Buffer

part_name : String
part_filename : String?
part_content_type : String?
part_is_file : Bool
part_count : Int
total_size : Int
first_boundary_found : Bool
finished : Bool
offset : Int
}

Streaming multipart parser. Feed byte chunks and receive ParseEvents. Never buffers an entire file in memory — body data is emitted as PartData.

#
Parser::feed

fn Parser::feed(self : Parser, chunk : Bytes) -> Result[Array[ParseEvent], MultipartError]

Feed a chunk of bytes. Returns ParseEvents emitted during processing.

#
Parser::finish

fn Parser::finish(self : Parser) -> Result[Array[ParseEvent], MultipartError]

Signal end of input. Returns error if body was incomplete.

#
Parser::is_finished

fn Parser::is_finished(self : Parser) -> Bool

Is parsing complete?

#
Parser::new

fn Parser::new(boundary : String, options : ParseOptions) -> Parser

Create a new streaming parser.

#
Part

pub(all) enum Part {
Field(String, String)
File(String, String, String?, Bytes)
} derive(
Debug
)

A single parsed multipart part — either a text field or a file upload.

#
Part::content_type

fn Part::content_type(self : Part) -> String?

#
Part::data

fn Part::data(self : Part) -> Bytes?

#
Part::filename

fn Part::filename(self : Part) -> String?

#
Part::is_field

fn Part::is_field(self : Part) -> Bool

#
Part::is_file

fn Part::is_file(self : Part) -> Bool

#
Part::name

fn Part::name(self : Part) -> String

#
Part::value

fn Part::value(self : Part) -> String?

#
StreamingWriter

pub(all) struct StreamingWriter {
boundary : String
buf :
Buffer

part_count : Int
current_part_open : Bool
}

A streaming multipart writer that outputs body data in chunks. Use begin_part / write_chunk / end_part instead of loading entire files into memory before writing.

#
StreamingWriter::begin_part

fn StreamingWriter::begin_part(self : StreamingWriter, name : String, filename : String?, content_type : String?) -> Unit

Begin a new part.
  • name: Content-Disposition name parameter
  • filename: Optional filename (None for text fields)
  • content_type: Optional Content-Type for the part

#
StreamingWriter::end_part

fn StreamingWriter::end_part(self : StreamingWriter) -> Unit

End the current part.

#
StreamingWriter::finish

fn StreamingWriter::finish(self : StreamingWriter) -> Bytes

Finalize and return the complete multipart body.

#
StreamingWriter::get_boundary

fn StreamingWriter::get_boundary(self : StreamingWriter) -> String

Get the boundary string.

#
StreamingWriter::new

fn StreamingWriter::new(boundary : String) -> StreamingWriter

Create a new streaming writer with the given boundary.

#
StreamingWriter::try_begin_part

fn StreamingWriter::try_begin_part(self : StreamingWriter, name : String, filename : String?, content_type : String?) -> Result[Unit, MultipartError]

Begin a part while rejecting CRLF in every serialized header parameter.

#
StreamingWriter::with_random_boundary

fn StreamingWriter::with_random_boundary() -> StreamingWriter

Create a streaming writer with an auto-generated boundary.

#
StreamingWriter::write_chunk

fn StreamingWriter::write_chunk(self : StreamingWriter, data : Bytes) -> Unit

Write a chunk of body data for the current part.

#
is_content_disposition

fn is_content_disposition(name : String) -> Bool

#
is_content_type

fn is_content_type(name : String) -> Bool

#
is_dangerous_filename

fn is_dangerous_filename(filename : String) -> Bool

Check if a filename contains path traversal or other dangerous patterns.

#
parse_all

fn parse_all(body : Bytes, boundary : String, options : ParseOptions) -> Result[MultipartForm, MultipartError]

Parse a complete multipart body in one shot. Convenience wrapper that feeds all bytes at once and collects all parts.

#
parse_boundary_from_content_type

fn parse_boundary_from_content_type(content_type : String) -> Result[String, MultipartError]

#
parse_content_disposition

fn parse_content_disposition(header_value : String, opts : ParseOptions) -> Result[(String, String?), MultipartError]

Parse Content-Disposition header. Returns (name, filename?). In strict mode with reject_filename_star, filename* is rejected.

#
parse_content_disposition_simple

fn parse_content_disposition_simple(header_value : String) -> Result[(String, String?), MultipartError]

Legacy parse_content_disposition without options (backwards compat).

#
parse_header_line

fn parse_header_line(line : String) -> (String, String)?

#
safe_filename

fn safe_filename(original : String) -> String

Produce a safe, sanitized filename by removing dangerous characters.

#
unique_filename

fn unique_filename(original : String) -> String

Generate a unique safe filename preserving the original extension.

#
validate_boundary

fn validate_boundary(boundary : String) -> Result[String, MultipartError]

#
validate_custom_header

fn validate_custom_header(name : String, value : String) -> Result[Unit, MultipartError]

Validate a custom part header name and value before it is serialized.

#
validate_filename

fn validate_filename(filename : String, max_len : Int) -> Result[String, MultipartError]

Validate a filename for security. Returns Ok(filename) or Err.

#
validate_header_component

fn validate_header_component(value : String, label : String) -> Result[Unit, MultipartError]

Reject CRLF in an HTTP header component.