moon-multipart

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

    multipart
    form-data
    rfc7578
    http
    upload
    streaming
    moonbit
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    3 months ago
    Downloads
    54

    No dependencies

    #moon-multipart

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

    #特性

    • RFC 7578 合规 — 完整支持 multipart/form-data 编码格式
    • 流式解析 — 增量处理上传数据,PartData 逐块输出,不将整个文件加载到内存
    • 跨分块边界检测 — 正确处理被网络分块拆分的 boundary 分隔符
    • 安全限制 — 6 维度可配置限制(part 数量、header 大小、字段大小、文件大小、总大小、文件名长度)
    • 路径穿越防护 — 检测并拒绝 ../、\、盘符、null 字节等危险文件名
    • 安全文件名工具 — safe_filename()、validate_filename()、unique_filename()
    • 严格/兼容双模式 — Strict 模式拒绝非标准扩展(如 filename*),Compatible 模式宽松接受
    • 流式 Writer — StreamingWriter 支持 begin_part / write_chunk / end_part 分块添加数据;真正恒定内存输出计划在 v0.3.0 引入 Sink/回调接口
    • 高层 API — parse_all() 一键解析返回 MultipartForm,支持同名字段/多文件

    #快速开始

    #安装

    moon add Songyz002/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::finish(self) -> (String, Bytes)

    StreamingWriter

    • StreamingWriter::new(boundary) -> StreamingWriter
    • StreamingWriter::begin_part(self, name, filename?, content_type?) -> Unit
    • 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

    #安全限制

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

    #项目规模

    • 3,650 行有效 MoonBit 代码
    • 87 个测试用例
    • 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 逐块输出

    #许可证

    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::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::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_filename

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

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