moon-content-disposition

RFC 6266 Content-Disposition parser, serializer, filename resolver and safety audit toolkit for MoonBit.

content-disposition
rfc6266
rfc8187
http
filename
download
header
moonbit
moon add xiaojing012/moon-content-disposition@0.1.1
Download zip
Version
0.1.1
License
Apache-2.0
Last updated
8 days ago
Downloads
4
README

#moon-content-disposition

GitHub repository

An RFC 6266 Content-Disposition parser, serializer, international filename resolver, safe filename policy and security audit toolkit for MoonBit.

The library reads and writes Content-Disposition header field values — attachment; filename="report.pdf", the RFC 8187 internationalised form filename*=UTF-8'en'caf%C3%A9.txt, and the extension parameters used by multipart/form-data (form-data; name="field") — with a structured error model, bounded resource limits, and an audit layer that surfaces the RFC 6266 Section 7 security concerns before a filename ever reaches a filesystem.

#Highlights

  • Strict, deterministic parser. Implements the RFC 6266 Section 4.2 grammar with a stable, structured error model: every failure is a DispositionError carrying a processing stage, a concrete kind, a UTF-8 byte offset and a bounded context excerpt.
  • RFC 8187 international filenames. filename* extended values are parsed and serialised for the two charsets that matter in practice (UTF-8 and ISO-8859-1), with strict percent-decoding, RFC 5646 language tags, and the RFC 6266 Section 4.3 precedence rule (filename* wins; filename is the fallback).
  • Safe filename policy. resolve_filename selects the download name; sanitize_filename turns it into a single safe path component under one of three profiles (Portable / Windows-like / Posix-like). Every transformation is recorded as a stable issue key; a non-empty name never fails.
  • Security audit. audit_header reports path separators, control characters, missing filename*, risky extensions, Windows reserved names, trailing dots, duplicate parameters and more — each with a severity, before anything is written to disk.
  • Deterministic serialisation. Parsed values re-serialise byte-stably; canonicalize_content_disposition produces a canonical form that is idempotent.
  • CR/LF injection defence. The generator rejects CR, LF, NUL and other control bytes outright rather than emitting them.
  • Bounded resource use. Seven independent Limits (input bytes, parameter count, value bytes, …) guarantee that no parse can exhaust memory, loop forever, or silently truncate.
  • Portable. The same code builds and passes 327 tests on the native, js and wasm-gc targets. Includes a JSON-emitting CLI (disposition-tool) and six runnable examples.

#Quick start

let header = "attachment; filename=\"café.txt\"; filename*=UTF-8'en'caf%C3%A9.txt"

match @cd.parse_content_disposition(header) {
Err(e) => println("parse failed: \{e.to_display()}")
Ok(cd) => {
match @cd.resolve_filename(cd) {
Err(e) => println("resolve failed: \{e.to_display()}")
Ok(sel) => {
// sel.selected() == "café.txt", sel.source() == FilenameStar
match @cd.sanitize_portable_filename(sel.selected()) {
Err(e) => println("sanitize failed: \{e.to_display()}")
Ok(result) => println("safe download name: \{result.safe()}")
}
}
}
}
}

Generate a header from a filename of your own:

let value = @cd.generate_attachment("café report.pdf")
// "attachment; filename=\"café report.pdf\"; filename*=UTF-8''caf%C3%A9%20report.pdf"

Canonicalise and audit:

let canonical = @cd.canonicalize_content_disposition(
"ATTACHMENT; FILE=fallback.txt; FILE*=UTF-8''caf%C3%A9.pdf",
)
// "attachment; filename=fallback.txt; filename*=UTF-8''caf%C3%A9.pdf"

match @cd.audit_header("attachment; filename=\"../../install.exe\"") {
Err(e) => println("audit failed: \{e.to_display()}")
Ok(report) =>
for issue in report.issues() {
println("[ \{issue.severity().to_string()} ] \{issue.kind().to_string()}: \{issue.message()}")
}
}

#The pipeline

Content-Disposition processing in this library is a pipeline; each stage is a pure function and the output of one is the input of the next.

parse → resolve → sanitize │ │ └─ FilenamePolicy (Portable / WindowsLike / PosixLike) │ └─ RFC 6266 §4.3 precedence, advisory warnings └─ ContentDisposition model ├─ serialize / canonicalize ├─ generate (the inverse of parse) └─ audit (advisory, never mutates)

The one rule to remember: a value from a header is untrusted input. Parse it with limits, resolve the filename, run it through a sanitisation policy, and audit it before you write it anywhere.

#Documentation

#Command-line toolkit

moon run cmd/disposition-tool -- help (or disposition-tool once built) prints the eleven commands:

parse <value> serialize <value> canonicalize <value> resolve <value> sanitize <name> [profile] generate <type> <name> audit <value> limits [preset] profiles version help

Every command emits a single JSON object on stdout and exits 1 on a failed command. See docs/cli.md.

#Examples

Six runnable demonstrations, one per library concern:

moon run examples/parse moon run examples/resolve -- "attachment; filename=fallback.txt; filename*=UTF-8'en'caf%C3%A9.txt" moon run examples/sanitize moon run examples/generate -- "café report.pdf" moon run examples/canonicalize moon run examples/audit

#Building and testing

moon build # library moon build cmd/disposition-tool --target wasm-gc moon test # native (default) moon test --target js moon test --target wasm-gc scripts/verify_all.ps1 # end-to-end verification (three targets) scripts/count_code.py # source-line report

327 tests across 25 files pass on all three targets, including a fully deterministic PRNG-driven property suite (2600+ property cases, 1300 sanitizer invariants, 600 canonicalisation cases). See docs/testing.md.

#Requirements

  • MoonBit toolchain 0.1.20260713 or newer. The build is verified on the native, js and wasm-gc targets.
  • Python 3 for scripts/count_code.py; Windows PowerShell for scripts/verify_all.ps1 (the script also works under pwsh on other platforms).

#Versioning

The single source of truth for the version is library_version() in model.mbt; moon.mod mirrors it. Current version: 0.1.1.

#License

Apache-2.0. See LICENSE and THIRD_PARTY_NOTICES.

#
DispositionError

pub suberror DispositionError {
DispositionError(DispositionErrorStage, DispositionErrorKind, Int, String)
}

A structured error returned by every public API.

  • stage() — where the failure happened.
  • kind() — what failed.
  • offset() — UTF-8 byte offset into the input, 0 when not meaningful.
  • context() — short excerpt of the input at the failure point (bounded, never the full input).

#
DispositionError::context

fn DispositionError::context(self : DispositionError) -> String

A short, bounded excerpt of the input around the failure point.

#
DispositionError::kind

The concrete kind of this error.

#
DispositionError::offset

fn DispositionError::offset(self : DispositionError) -> Int

The UTF-8 byte offset into the input where the error was detected, or 0 when the offset is not meaningful for this error kind.

#
DispositionError::stage

The stage in which this error was detected.

#
DispositionError::to_display

fn DispositionError::to_display(self : DispositionError) -> String

A single-line human readable rendering of the error, intended for terminal output and CLI use.

#
AuditIssue

pub struct AuditIssue {
severity : AuditSeverity
kind : AuditKind
parameter : String
message : String
}

A single audit finding.

#
AuditIssue::kind

fn AuditIssue::kind(self : AuditIssue) -> AuditKind

The stable kind of this issue.

#
AuditIssue::message

fn AuditIssue::message(self : AuditIssue) -> String

A human-readable explanation of the issue.

#
AuditIssue::parameter

fn AuditIssue::parameter(self : AuditIssue) -> String

The parameter the issue concerns (filename, filename*, or an empty string for issues that concern the whole value).

#
AuditIssue::severity

fn AuditIssue::severity(self : AuditIssue) -> AuditSeverity

The severity of this issue.

#
AuditKind

pub enum AuditKind {
MissingFilename
EmptyFilename
BothFilenameAndFilenameStar
PlainFilenameOnly
FilenameStarWithoutFallback
PathSeparatorInFilename
ControlCharacterInFilename
NonAsciiUnquotedFilename
MissingFilenameStarForNonAscii
UnsupportedCharset
InvalidLanguageTag
DuplicateParameter
RecoveryApplied
ExtensionRisk
ReservedWindowsName
TrailingDotOrSpace
LongFilename
ExtensionDispositionType
}

The stable category of an audit issue.

#
AuditKind::to_string

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

A stable programmatic name for a kind, used by the CLI JSON output.

#
AuditReport

pub struct AuditReport {
issues : Array[AuditIssue]
}

The result of an audit: the issues found, in a stable order.

#
AuditReport::count_at_least_warning

fn AuditReport::count_at_least_warning(self : AuditReport) -> Int

The number of issues at Warning severity or above.

#
AuditReport::count_high

fn AuditReport::count_high(self : AuditReport) -> Int

The number of High-severity issues.

#
AuditReport::count_severity

fn AuditReport::count_severity(self : AuditReport, severity : AuditSeverity) -> Int

The number of issues at or above the given severity. Severity values are obtained from AuditIssue::severity() (enum variants cannot be named in expression position from consumer packages); count_high and count_at_least_warning are the convenient forms.

#
AuditReport::empty

fn AuditReport::empty() -> AuditReport

An empty audit report.

#
AuditReport::issue_count

fn AuditReport::issue_count(self : AuditReport) -> Int

The number of issues.

#
AuditReport::issues

fn AuditReport::issues(self : AuditReport) -> Array[AuditIssue]

The issues found, in a stable order.

#
AuditSeverity

pub enum AuditSeverity {
Info
Warning
High
}

The severity of an audit issue. Info is informational, Warning deserves attention, High should normally cause the download to be treated as risky.

#
AuditSeverity::to_string

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

A stable programmatic name for a severity.

#
ContentDisposition

pub struct ContentDisposition {
disposition_type : DispositionType
parameters : Array[DispositionParameter]
raw_disposition_type : String?
}

A parsed Content-Disposition header field value.

parameters preserves the input order of every parameter, including extension parameters and (in compatible mode) duplicates. The typed accessors below look parameters up without consuming them.

#
ContentDisposition::disposition_type

fn ContentDisposition::disposition_type(self : ContentDisposition) -> DispositionType

The disposition type.

#
ContentDisposition::filename

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

The value of the last filename parameter, if present. Only the plain (non-extended) filename parameter is considered; the decoded value of filename* is exposed through filename_star().

#
ContentDisposition::filename_star

The decoded extended value of the last filename* parameter, if present.

#
ContentDisposition::get_parameter

fn ContentDisposition::get_parameter(self : ContentDisposition, name : String) -> DispositionParameter?

The parameter with the given name (case-insensitive), or None. If the name appears more than once, the first occurrence is returned.

#
ContentDisposition::parameter_count

fn ContentDisposition::parameter_count(self : ContentDisposition) -> Int

The number of parameters.

#
ContentDisposition::parameters

All parameters, in input order.

#
ContentDisposition::raw_disposition_type

fn ContentDisposition::raw_disposition_type(self : ContentDisposition) -> String?

The disposition type token exactly as it appeared in the input, if this value was produced by parsing (never present on hand-built models).

#
ContentDisposition::semantic_equal

fn ContentDisposition::semantic_equal(self : ContentDisposition, other : ContentDisposition) -> Bool

Semantic equality between two Content-Disposition values. Parameter order must match; names are compared case-insensitively.

#
ContentDisposition::to_debug_string

fn ContentDisposition::to_debug_string(self : ContentDisposition) -> String

A compact human-readable rendering used by tests and the CLI.

#
DispositionErrorKind

pub enum DispositionErrorKind {
EmptyInput
InvalidDispositionType
ExpectedToken
UnexpectedCharacter
MissingEquals
MissingParameterValue
InvalidParameterName
DuplicateParameter
UnterminatedQuotedString
InvalidQuotedPair
InvalidControlCharacter
InvalidExtendedValue
MissingCharset
InvalidCharset
UnsupportedCharset
InvalidLanguage
InvalidPercentEncoding
InvalidUtf8
InvalidFilename
UnsafeFilename
LimitExceeded
TrailingInput
}

The concrete error category. Stable across versions so callers can switch on it without string matching.

#
DispositionErrorKind::to_string

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

Stable programmatic name for a kind, used by the CLI JSON output.

#
DispositionErrorStage

pub enum DispositionErrorStage {
Input
DispositionType
ParameterName
ParameterValue
Token
QuotedString
ExtendedValue
PercentEncoding
Charset
FilenameResolution
FilenamePolicy
Serialization
Limit
}

The processing stage in which an error was detected.

#
DispositionErrorStage::to_string

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

Stable programmatic name for a stage, used by the CLI JSON output.

#
DispositionParameter

pub struct DispositionParameter {
name : String
value : ParameterValue
}

A single disposition parameter: a name and a value.

#
DispositionParameter::is_extended

fn DispositionParameter::is_extended(self : DispositionParameter) -> Bool

Whether the parameter name ends with * (the RFC 8187 extended form).

#
DispositionParameter::name

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

The parameter name.

#
DispositionParameter::semantic_equal

fn DispositionParameter::semantic_equal(self : DispositionParameter, other : DispositionParameter) -> Bool

Semantic equality between two parameters: names are compared case-insensitively, values structurally.

#
DispositionParameter::value

The parameter value.

#
DispositionParse

pub struct DispositionParse {
content_disposition : ContentDisposition
duplicates : Array[String]
recoveries : Array[String]
mode : ParseMode
}

The detailed result of parsing a Content-Disposition header value: the model plus the duplicate names and compatible recoveries observed.

#
DispositionParse::content_disposition

fn DispositionParse::content_disposition(self : DispositionParse) -> ContentDisposition

The parsed Content-Disposition value.

#
DispositionParse::duplicates

fn DispositionParse::duplicates(self : DispositionParse) -> Array[String]

The names of duplicated parameters (compatible mode), in detection order, each at most once.

#
DispositionParse::mode

The parse mode used for this parse.

#
DispositionParse::recoveries

fn DispositionParse::recoveries(self : DispositionParse) -> Array[String]

The compatible recoveries applied during the parse, in application order, each at most once. Empty in strict mode.

#
DispositionType

pub enum DispositionType {
Inline
Attachment
Extension(String)
}

The disposition type of a Content-Disposition field value.

Inline and Attachment are the two registered disposition types (RFC 6266 Section 4.1); anything else is a valid extension token (disp-ext-type), preserved verbatim.

#
DispositionType::extension_name

fn DispositionType::extension_name(self : DispositionType) -> String?

The extension token, if this is an extension disposition type.

#
DispositionType::is_attachment

fn DispositionType::is_attachment(self : DispositionType) -> Bool

Whether the disposition type is the registered attachment type.

#
DispositionType::is_extension

fn DispositionType::is_extension(self : DispositionType) -> Bool

Whether the disposition type is an extension token.

#
DispositionType::is_inline

fn DispositionType::is_inline(self : DispositionType) -> Bool

Whether the disposition type is the registered inline type.

#
DispositionType::matches

fn DispositionType::matches(self : DispositionType, name : String) -> Bool

Whether this disposition type matches the given token, case-insensitively.

#
DispositionType::to_lower_name

fn DispositionType::to_lower_name(self : DispositionType) -> String

The disposition type token, lower-cased for comparison.

#
ExtendedValue

pub struct ExtendedValue {
charset : String
language : String?
value : String
}

An RFC 8187 extended value: a charset name, an optional language tag, and the decoded value. The charset is always a supported charset (UTF-8 or ISO-8859-1, stored in canonical casing) after validation; the value is the fully decoded string.

#
ExtendedValue::charset

fn ExtendedValue::charset(self : ExtendedValue) -> String

The charset of this extended value.

#
ExtendedValue::language

fn ExtendedValue::language(self : ExtendedValue) -> String?

The optional language tag of this extended value.

#
ExtendedValue::semantic_equal

fn ExtendedValue::semantic_equal(self : ExtendedValue, other : ExtendedValue) -> Bool

Semantic equality between two extended values: charset, language and value. Used by round-trip tests.

#
ExtendedValue::value

fn ExtendedValue::value(self : ExtendedValue) -> String

The decoded value of this extended value.

#
ExtensionPolicy

pub struct ExtensionPolicy {
enabled : Bool
allow_list : Array[String]
deny_list : Array[String]
}

Whether an extension is audited. When enabled, the sanitizer checks the filename's extension against an allow list and/or a deny list and records an issue (never an error) for a violation.

#
ExtensionPolicy::allow_list

fn ExtensionPolicy::allow_list(self : ExtensionPolicy) -> Array[String]

The allow list (compared case-insensitively).

#
ExtensionPolicy::deny_list

fn ExtensionPolicy::deny_list(self : ExtensionPolicy) -> Array[String]

The deny list (compared case-insensitively).

#
ExtensionPolicy::disabled

An extension policy with auditing disabled.

#
ExtensionPolicy::enabled

fn ExtensionPolicy::enabled(allow_list : Array[String], deny_list : Array[String]) -> ExtensionPolicy

An extension policy with auditing enabled. Extensions are compared case-insensitively, with or without the leading dot ("exe" and ".exe" both match exe).

#
ExtensionPolicy::is_enabled

fn ExtensionPolicy::is_enabled(self : ExtensionPolicy) -> Bool

Whether extension auditing is enabled.

#
FilenamePolicy

pub struct FilenamePolicy {
profile : PolicyProfile
max_length : Int
extension : ExtensionPolicy
windows_reserved : WindowsReservedPolicy
}

A complete sanitisation policy.

#
FilenamePolicy::default

The default policy: Portable, 255 characters, extension auditing off, Windows reserved-name defusing on.

#
FilenamePolicy::extension

The extension auditing rule of this policy.

#
FilenamePolicy::max_length

fn FilenamePolicy::max_length(self : FilenamePolicy) -> Int

The maximum length in code points. Names longer than this are truncated.

#
FilenamePolicy::portable

fn FilenamePolicy::portable() -> FilenamePolicy

The Portable profile.

#
FilenamePolicy::posix_like

fn FilenamePolicy::posix_like() -> FilenamePolicy

The PosixLike profile.

#
FilenamePolicy::profile

The profile of this policy.

#
FilenamePolicy::windows_like

fn FilenamePolicy::windows_like() -> FilenamePolicy

The WindowsLike profile.

#
FilenamePolicy::windows_reserved

fn FilenamePolicy::windows_reserved(self : FilenamePolicy) -> WindowsReservedPolicy

The Windows reserved-name rule of this policy.

#
FilenamePolicy::with_extension

fn FilenamePolicy::with_extension(self : FilenamePolicy, extension : ExtensionPolicy) -> FilenamePolicy

A copy of this policy with a different extension auditing rule.

#
FilenamePolicy::with_max_length

fn FilenamePolicy::with_max_length(self : FilenamePolicy, max_length : Int) -> FilenamePolicy

A copy of this policy with a different maximum length.

#
FilenamePolicy::with_windows_reserved

fn FilenamePolicy::with_windows_reserved(self : FilenamePolicy, windows_reserved : WindowsReservedPolicy) -> FilenamePolicy

A copy of this policy with a different Windows reserved-name rule.

#
FilenameSelection

pub struct FilenameSelection {
selected : String
source : FilenameSource
fallback : Bool
warnings : Array[String]
}

The result of resolving a download filename from a ContentDisposition: the selected value, its source, whether a fallback was available, and any advisory warnings.

#
FilenameSelection::fallback

fn FilenameSelection::fallback(self : FilenameSelection) -> Bool

Whether a fallback value was present (a plain filename alongside a filename*).

#
FilenameSelection::selected

fn FilenameSelection::selected(self : FilenameSelection) -> String

The selected filename value.

#
FilenameSelection::source

Whether the value came from filename or filename*.

#
FilenameSelection::warnings

fn FilenameSelection::warnings(self : FilenameSelection) -> Array[String]

The advisory warnings produced while resolving, each at most once. The stable keys are no-filename-parameter (not produced here — that is an error), filename-star-precedence, empty-filename, contains-path-separator and contains-control-character.

#
FilenameSource

pub enum FilenameSource {
Filename
FilenameStar
}

Where the selected filename came from.

#
GenerateOptions

pub struct GenerateOptions {
include_filename_star : Bool
always_filename_star : Bool
language : String?
}

Options controlling one generation.

#
GenerateOptions::always_filename_star

fn GenerateOptions::always_filename_star(self : GenerateOptions) -> Bool

Whether filename* is emitted even for pure-ASCII filenames (default false; it is emitted only when the filename contains non-ASCII).

#
GenerateOptions::default

Default generation options: emit filename* when the filename contains non-ASCII characters (RFC 8187 Section 5), no language tag.

#
GenerateOptions::include_filename_star

fn GenerateOptions::include_filename_star(self : GenerateOptions) -> Bool

Whether the RFC 8187 filename* parameter is emitted at all.

#
GenerateOptions::language

fn GenerateOptions::language(self : GenerateOptions) -> String?

The RFC 5646 language tag attached to filename*, if any.

#
GenerateOptions::with_language

fn GenerateOptions::with_language(self : GenerateOptions, language : String?) -> GenerateOptions

A copy of these options with the given language tag.

#
GenerateOptions::without_filename_star

fn GenerateOptions::without_filename_star(self : GenerateOptions) -> GenerateOptions

A copy of these options with filename* disabled.

#
Limits

pub struct Limits {
max_input_bytes : Int
max_parameters : Int
max_parameter_name_bytes : Int
max_parameter_value_bytes : Int
max_filename_bytes : Int
max_extended_value_bytes : Int
max_context_bytes : Int
}

Bounds applied while parsing or generating Content-Disposition data.

#
Limits::default

fn Limits::default() -> Limits

Default limits. Intended for interactive and typical server use.

#
Limits::max_context_bytes

fn Limits::max_context_bytes(self : Limits) -> Int

The error context excerpt bound.

#
Limits::max_extended_value_bytes

fn Limits::max_extended_value_bytes(self : Limits) -> Int

The RFC 8187 extended value byte bound.

#
Limits::max_filename_bytes

fn Limits::max_filename_bytes(self : Limits) -> Int

The filename byte bound, used by the resolver, the policy and the generator.

#
Limits::max_input_bytes

fn Limits::max_input_bytes(self : Limits) -> Int

The input byte bound.

#
Limits::max_parameter_name_bytes

fn Limits::max_parameter_name_bytes(self : Limits) -> Int

The parameter name byte bound.

#
Limits::max_parameter_value_bytes

fn Limits::max_parameter_value_bytes(self : Limits) -> Int

The plain parameter value byte bound.

#
Limits::max_parameters

fn Limits::max_parameters(self : Limits) -> Int

The parameter count bound.

#
Limits::permissive

fn Limits::permissive() -> Limits

Permissive limits. Intended for batch processing of trusted data with very large headers.

#
Limits::strict

fn Limits::strict() -> Limits

Strict limits. Intended for constrained deployments that expect small, well-formed headers.

#
ParameterValue

pub enum ParameterValue {
Token(String)
Quoted(String)
Extended(ExtendedValue)
}

The value of a single disposition parameter.

Token is the unquoted token form, Quoted is the quoted-string form, and Extended is an RFC 8187 extended value (used when the parameter name ends with *).

#
ParameterValue::extended

The extended value, if this is the extended form.

#
ParameterValue::is_extended

fn ParameterValue::is_extended(self : ParameterValue) -> Bool

Whether this value is the RFC 8187 extended form.

#
ParameterValue::plain

fn ParameterValue::plain(self : ParameterValue) -> String?

The plain string content of a non-extended parameter value, if this value is not the extended form.

#
ParameterValue::semantic_equal

fn ParameterValue::semantic_equal(self : ParameterValue, other : ParameterValue) -> Bool

Semantic equality between two parameter values. Token and Quoted forms of the same string are considered different forms but semantically equal content; extended values are compared structurally.

#
ParseCollector

pub struct ParseCollector {
recoveries : Array[String]
duplicates : Array[String]
seen_names : Array[String]
}

Mutable bookkeeping used while parsing one Content-Disposition value: the compatible recoveries applied, the names of duplicated parameters (preserved in compatible mode), and the parameter names seen so far (used for duplicate detection).

#
ParseCollector::duplicates

fn ParseCollector::duplicates(self : ParseCollector) -> Array[String]

The names of duplicated parameters, in first-duplicate-detection order (each name at most once).

#
ParseCollector::has_seen

fn ParseCollector::has_seen(self : ParseCollector, name : String) -> Bool

Whether a parameter name has already been seen during this parse.

#
ParseCollector::mark_seen

fn ParseCollector::mark_seen(self : ParseCollector, name : String) -> Unit

Marks a parameter name as seen.

#
ParseCollector::new

Constructs an empty collector.

#
ParseCollector::record_duplicate

fn ParseCollector::record_duplicate(self : ParseCollector, name : String) -> Unit

Records a duplicated parameter name (compatible mode only).

#
ParseCollector::record_recovery

fn ParseCollector::record_recovery(self : ParseCollector, note : String) -> Unit

Records a compatible recovery by stable name, deduplicating so that a recovery is reported at most once per parse.

#
ParseCollector::recoveries

fn ParseCollector::recoveries(self : ParseCollector) -> Array[String]

The recoveries applied during the parse, in application order (each name at most once).

#
ParseMode

pub enum ParseMode {
Strict
Compatible
} derive(Eq)

The parse mode.

Strict follows the project-defined RFC 6266 / RFC 8187 scope without leniency. Compatible additionally accepts a small, individually documented set of real-world behaviours; every compatible recovery is recorded in the parse result and in the audit report so that no deviation is silent. Compatible mode never relaxes security-critical validation (control-character rejection, percent-encoding validation, charset validation).

#
ParseMode::to_string

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

A stable programmatic name for a parse mode.

#
ParseOptions

pub struct ParseOptions {
mode : ParseMode
limits : Limits
}

Options controlling one parse (or canonicalisation) operation.

#
ParseOptions::compatible

fn ParseOptions::compatible() -> ParseOptions

Compatible mode with Limits::default().

#
ParseOptions::default

fn ParseOptions::default() -> ParseOptions

Default parse options: Strict mode with Limits::default().

#
ParseOptions::limits

fn ParseOptions::limits(self : ParseOptions) -> Limits

The limits of these options.

#
ParseOptions::mode

fn ParseOptions::mode(self : ParseOptions) -> ParseMode

The parse mode of these options.

#
ParseOptions::new

Constructs default parse options: Strict mode with Limits::default().

#
ParseOptions::permissive

fn ParseOptions::permissive() -> ParseOptions

Strict mode with Limits::permissive().

#
ParseOptions::strict

fn ParseOptions::strict() -> ParseOptions

Strict mode with Limits::strict().

#
ParseOptions::with_limits

fn ParseOptions::with_limits(self : ParseOptions, limits : Limits) -> ParseOptions

A copy of these options with the given limits.

#
ParseOptions::with_mode

fn ParseOptions::with_mode(self : ParseOptions, mode : ParseMode) -> ParseOptions

A copy of these options with the given mode.

#
PolicyProfile

pub enum PolicyProfile {
Portable
WindowsLike
PosixLike
} derive(Eq)

The three named sanitisation profiles.

#
PolicyProfile::to_string

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

A stable programmatic name for a profile.

#
SafeFilenameResult

pub struct SafeFilenameResult {
original : String
safe : String
changed : Bool
issues : Array[String]
}

The result of sanitising a filename: the original value, the safe value, whether anything changed, and the list of issues (each stable key at most once).

#
SafeFilenameResult::changed

fn SafeFilenameResult::changed(self : SafeFilenameResult) -> Bool

Whether the sanitised name differs from the original.

#
SafeFilenameResult::issues

fn SafeFilenameResult::issues(self : SafeFilenameResult) -> Array[String]

The stable issue keys produced while sanitising, each at most once: replaced-path-separator, replaced-unsafe-character, defused-dot-name, prefixed-reserved-name, trimmed-trailing-char, truncated-to-max-length, denied-extension, extension-not-in-allow-list.

#
SafeFilenameResult::original

fn SafeFilenameResult::original(self : SafeFilenameResult) -> String

The original (unsanitised) filename.

#
SafeFilenameResult::safe

fn SafeFilenameResult::safe(self : SafeFilenameResult) -> String

The sanitised filename, safe to use as a single path component.

#
Scanner

pub struct Scanner {
bytes : Bytes
len : Int
pos : Int
}

A bounds-checked byte cursor over an input string.

#
Scanner::byte_at

fn Scanner::byte_at(self : Scanner, idx : Int) -> Byte?

The byte at an absolute offset, or None when out of bounds.

#
Scanner::consume_char

fn Scanner::consume_char(self : Scanner, b : Byte) -> Bool

If the current byte equals b, advances and returns true.

#
Scanner::consume_token

fn Scanner::consume_token(self : Scanner) -> (Int, Int)

Consumes a run of HTTP token characters and returns the (start, end) byte range. The run may be empty.

#
Scanner::consume_value_chars

fn Scanner::consume_value_chars(self : Scanner) -> (Int, Int)

Consumes a run of attr-char / percent-encoded bytes and returns the (start, end) byte range of the raw value-chars (before percent decoding). The run may be empty. Used to locate the raw extent of an RFC 8187 extended value.

#
Scanner::consume_while

fn Scanner::consume_while(self : Scanner, pred : (Byte) -> Bool) -> (Int, Int)

Consumes a run of bytes satisfying pred and returns the (start, end) byte range of the run. The run may be empty.

#
Scanner::context_string

fn Scanner::context_string(self : Scanner) -> String

A short, bounded excerpt of the input around the current position, for use in error context strings. Never longer than max_context_bytes().

#
Scanner::context_string_limited

fn Scanner::context_string_limited(self : Scanner, limit : Int) -> String

An excerpt of the input around the current position using an explicit context bound (from the active Limits).

#
Scanner::eof

fn Scanner::eof(self : Scanner) -> Bool

Whether the cursor is at (or past) the end of the input.

#
Scanner::find_byte

fn Scanner::find_byte(self : Scanner, b : Byte) -> Int?

The index of the next occurrence of byte b at or after the current position, or None.

#
Scanner::is_ows

fn Scanner::is_ows(self : Scanner) -> Bool

Whether the current byte is optional whitespace under this grammar.

#
Scanner::new

fn Scanner::new(input : String) -> Scanner

Creates a scanner for a Content-Disposition header field value.

#
Scanner::next_byte

fn Scanner::next_byte(self : Scanner) -> Byte?

Returns the current byte and advances the cursor by one. Returns None at end of input (and does not advance).

#
Scanner::peek_at

fn Scanner::peek_at(self : Scanner, rel : Int) -> Byte?

The byte at pos + rel, or None when out of bounds. rel may be negative to look behind the current position.

#
Scanner::peek_byte

fn Scanner::peek_byte(self : Scanner) -> Byte?

The byte at the current position, or None at end of input.

#
Scanner::position

fn Scanner::position(self : Scanner) -> Int

The current position, as a UTF-8 byte offset.

#
Scanner::remaining

fn Scanner::remaining(self : Scanner) -> Int

The number of bytes remaining from the current position.

#
Scanner::seek

fn Scanner::seek(self : Scanner, pos : Int) -> Unit

Moves the cursor to an absolute byte offset, clamping out-of-range positions to the ends of the input.

#
Scanner::skip_ows

fn Scanner::skip_ows(self : Scanner) -> Unit

Skips optional whitespace (OWS): SP (0x20) and HTAB (0x09). CR and LF are never skipped.

#
Scanner::take_string

fn Scanner::take_string(self : Scanner, start : Int, end : Int) -> String

Decodes the byte range [start, end) back into a String. Valid UTF-8 is decoded normally; a range that is not valid UTF-8 is preserved byte-for-byte so that quoting round-trips never lose data.

#
Scanner::total_bytes

fn Scanner::total_bytes(self : Scanner) -> Int

The total length of the input in bytes.

#
WindowsReservedPolicy

pub struct WindowsReservedPolicy {
enabled : Bool
}

Whether Windows reserved-name defusing is applied.

#
WindowsReservedPolicy::disabled

A Windows reserved-name policy with defusing disabled.

#
WindowsReservedPolicy::enabled

A Windows reserved-name policy with defusing enabled.

#
WindowsReservedPolicy::is_enabled

fn WindowsReservedPolicy::is_enabled(self : WindowsReservedPolicy) -> Bool

Whether reserved-name defusing is enabled.

#
attr_char

fn attr_char(b : Byte) -> Bool

RFC 8187 attr-char: token except *, ', %.

#
audit_content_disposition

fn audit_content_disposition(cd : ContentDisposition) -> AuditReport

Audits a parsed ContentDisposition for the RFC 6266 Section 7 concerns. Never raises; the issues are advisory.

#
audit_disposition_parse

fn audit_disposition_parse(parse : DispositionParse) -> AuditReport

Turns the diagnostics of a detailed parse (duplicate names and compatible recoveries) into audit issues.

#
audit_filename

fn audit_filename(name : String, policy : FilenamePolicy) -> AuditReport

Audits a filename against a policy. The issues mirror the sanitizer's concerns; this is the advisory side, sanitize_filename is the corrective side.

#
audit_filename_with_media_type

fn audit_filename_with_media_type(name : String, media_type : String, policy : FilenamePolicy) -> AuditReport

Audits a filename together with its declared media type. The media type strengthens the extension-risk finding: an executable-looking extension combined with an executable media type is High, on its own it is Warning.

#
audit_header

fn audit_header(input : String) -> Result[AuditReport, DispositionError]

Audits a raw header value in one step: parse strictly, then combine the parse diagnostics with the model audit. Errors when the value does not parse.

#
can_be_token

fn can_be_token(value : String) -> Bool

Whether a string can be emitted as an unquoted token (all bytes are tchar and the string is non-empty). Used by the serializer to decide between the token and quoted forms.

#
canonical_charset

fn canonical_charset(charset : String) -> String?

The canonical form of a supported charset name, or None for an unsupported charset. The canonical form is what the model stores and what the serializer emits.

#
canonicalize_content_disposition

fn canonicalize_content_disposition(input : String) -> Result[String, DispositionError]

Parses and re-serialises a Content-Disposition header value into a canonical form. The canonical form lower-cases the disposition type and every parameter name, preserves parameter order except that a plain filename parameter is emitted before a filename* parameter when both are present, and re-encodes values deterministically. The operation is idempotent: canonicalising the canonical form is a no-op.

Errors: any error that parse_content_disposition can raise.

#
compatible_recovery_names

fn compatible_recovery_names() -> Array[String]

The stable names of every compatible-mode recovery this library can apply, in documentation order. See docs/compatibility.md.

#
content_disposition

fn content_disposition(disposition_type : DispositionType) -> ContentDisposition

Constructs a ContentDisposition with the given disposition type and no parameters. Hand-built values have no raw disposition type; the preserve-case serializer falls back to the canonical spelling.

#
content_disposition_with_raw

fn content_disposition_with_raw(disposition_type : DispositionType, raw : String) -> ContentDisposition

Constructs a ContentDisposition while remembering the exact disposition type token as it appeared on the wire, so that a preserve-case serialisation can emit it verbatim. Used by the parser.

#
decode_bytes

fn decode_bytes(charset : String, bytes : Bytes) -> Result[String, DispositionError]

Decodes a raw byte array using the given charset name. For UTF-8 the bytes must form valid UTF-8; for ISO-8859-1 every byte maps to the code point with the same value.

Errors: Charset::UnsupportedCharset (unknown charset) and Charset::InvalidUtf8 (bytes are not valid UTF-8).

#
disposition_error

fn disposition_error(stage : DispositionErrorStage, kind : DispositionErrorKind, context : String) -> DispositionError

Constructs a DispositionError with the given stage, kind and context and a zero offset.

#
disposition_error_at

fn disposition_error_at(stage : DispositionErrorStage, kind : DispositionErrorKind, offset : Int, context : String) -> DispositionError

Constructs a DispositionError carrying an explicit UTF-8 byte offset.

#
disposition_parameter

fn disposition_parameter(name : String, value : ParameterValue) -> DispositionParameter

Constructs a DispositionParameter from its parts.

#
encode_to_bytes

fn encode_to_bytes(charset : String, value : String) -> Result[Bytes, DispositionError]

Encodes a value string to raw bytes using the given charset name, for serialisation. UTF-8 always succeeds; ISO-8859-1 succeeds only when every code point in the value fits in Latin-1 (U+0000-U+00FF), otherwise the encoder falls back to UTF-8 (a documented, deterministic normalisation).

Errors: Charset::UnsupportedCharset (unknown charset).

#
extended_value

fn extended_value(charset : String, language : String?, value : String) -> ExtendedValue

Constructs an ExtendedValue from its parts.

#
fits_iso8859_1

fn fits_iso8859_1(value : String) -> Bool

Whether every code point of value can be represented in ISO-8859-1.

#
generate_attachment

fn generate_attachment(filename : String) -> Result[String, DispositionError]

Generates an attachment Content-Disposition value for a filename.

Errors: QuotedString::InvalidControlCharacter (the filename contains a control character — CR/LF/NUL injection defence).

#
generate_attachment_with_options

fn generate_attachment_with_options(filename : String, options : GenerateOptions) -> Result[String, DispositionError]

Generates an attachment Content-Disposition value with explicit options.

#
generate_content_disposition

fn generate_content_disposition(disposition_type : DispositionType, filename : String, options : GenerateOptions) -> Result[String, DispositionError]

Generates a Content-Disposition value for the given disposition type and filename with explicit options.

Errors: QuotedString::InvalidControlCharacter (control character in the filename) and ExtendedValue::InvalidLanguage (a language tag was given but is not a valid RFC 5646 tag).

#
generate_inline

fn generate_inline(filename : String) -> Result[String, DispositionError]

Generates an inline Content-Disposition value for a filename.

Errors: QuotedString::InvalidControlCharacter.

#
generate_inline_with_options

fn generate_inline_with_options(filename : String, options : GenerateOptions) -> Result[String, DispositionError]

Generates an inline Content-Disposition value with explicit options.

#
has_percent_encoding

fn has_percent_encoding(value : String) -> Bool

Whether a string contains any percent-encoded octets (used by tests).

#
hex_char

fn hex_char(nibble : Int) -> Char

Uppercase hex digit character for a nibble (0-15).

#
hex_value

fn hex_value(b : Byte) -> Int

Numeric value of a hex digit byte, or -1 for a non-hex byte.

#
is_alpha

fn is_alpha(b : Byte) -> Bool

ALPHA — ASCII letters.

#
is_control_byte

fn is_control_byte(b : Byte) -> Bool

Whether a byte is a C0 control character (0x00-0x1F, including NUL, CR and LF) or DEL (0x7F). These must never appear unquoted in a Content-Disposition value and are stripped or rejected by the filename policy.

#
is_digit

fn is_digit(b : Byte) -> Bool

DIGIT — ASCII digits.

#
is_extended_param_name

fn is_extended_param_name(name : String) -> Bool

Whether a parameter name denotes an RFC 8187 extended value (a non-empty base name followed by *).

#
is_header_safe

fn is_header_safe(value : String) -> Bool

Whether a string is free of characters that would be dangerous inside a header value: NUL, CR, LF and other C0 controls. Used by the generator before emitting a filename.

#
is_hexdigit

fn is_hexdigit(b : Byte) -> Bool

HEXDIG — ASCII hex digits.

#
is_obs_text

fn is_obs_text(b : Byte) -> Bool

HTTP obs-text: bytes 0x80-0xFF. Allowed inside quoted-strings and quoted-pairs per RFC 7230; in compatible mode also tolerated inside unquoted parameter values so that legacy non-ASCII headers can be read.

#
is_ows_byte

fn is_ows_byte(b : Byte) -> Bool

Whether a byte is optional whitespace (SP / HTAB only). CR and LF are never treated as whitespace here: a bare CR or LF inside a header value is a security-relevant control character, not ignorable whitespace.

#
is_path_separator

fn is_path_separator(b : Byte) -> Bool

Whether a byte is a path separator (/ or \).

#
is_separator_byte

fn is_separator_byte(b : Byte) -> Bool

Whether a byte is one of the RFC 7230 separators (structural punctuation). Used to decide whether an unquoted run ends at a separator.

#
is_supported_charset

fn is_supported_charset(charset : String) -> Bool

Whether a charset name is supported (case-insensitive UTF-8 or ISO-8859-1).

#
is_token_char

fn is_token_char(b : Byte) -> Bool

Alias for token_char, matching the RFC 7230 tchar definition.

#
library_version

fn library_version() -> String

The single library version string, used by moon --version-style reporting, the CLI and the module metadata. Kept in one place so the final maintainer can update it (together with moon.mod) in one step.

#
max_context_bytes

fn max_context_bytes() -> Int

The context excerpt bound used when building error messages. This is a module-level convenience returning the default() limit so that callers that construct errors directly do not need to thread a Limits value.

#
parse_content_disposition

fn parse_content_disposition(input : String) -> Result[ContentDisposition, DispositionError]

Parses a Content-Disposition header field value in strict mode with default limits.

Errors: Input::EmptyInput (empty value), Input::LimitExceeded (input larger than max_input_bytes), Limit::LimitExceeded, plus the DispositionType, ParameterName, ParameterValue, QuotedString, ExtendedValue, Charset and PercentEncoding errors raised while parsing.

#
parse_content_disposition_detailed

fn parse_content_disposition_detailed(input : String, options : ParseOptions) -> Result[DispositionParse, DispositionError]

Parses a Content-Disposition header field value with explicit options, also returning duplicate names and compatible recoveries.

#
parse_content_disposition_with_options

fn parse_content_disposition_with_options(input : String, options : ParseOptions) -> Result[ContentDisposition, DispositionError]

Parses a Content-Disposition header field value with explicit options.

#
parse_extended_value

fn parse_extended_value(cursor : Scanner, limits : Limits) -> Result[ExtendedValue, DispositionError] raise

Parses an extended value starting at the current scanner position. On success the cursor is positioned immediately after the last value-char.

Errors: ExtendedValue::MissingCharset (no charset before the first apostrophe), ExtendedValue::InvalidCharset (malformed charset), ExtendedValue::InvalidExtendedValue (missing separator), ExtendedValue::InvalidLanguage, ExtendedValue::InvalidPercentEncoding, ExtendedValue::InvalidUtf8, Charset::UnsupportedCharset, and Limit::LimitExceeded.

#
parse_extended_value_default

fn parse_extended_value_default(input : String) -> Result[ExtendedValue, DispositionError]

Parses a complete extended value from a string with default limits.

#
parse_extended_value_string

fn parse_extended_value_string(input : String, limits : Limits) -> Result[ExtendedValue, DispositionError]

Parses a complete extended value from a string (the value of a * parameter, without the name or =).

#
parse_parameter

fn parse_parameter(cursor : Scanner, limits : Limits, mode : ParseMode, collector : ParseCollector) -> Result[DispositionParameter, DispositionError]

Parses a single parameter starting at the current scanner position (which must be the first byte of the parameter name). On success the cursor is positioned after the parameter value. Compatible-mode recoveries are recorded on the collector.

#
parse_quoted_string

fn parse_quoted_string(cursor : Scanner, limits : Limits) -> Result[String, DispositionError]

Parses a quoted-string starting at the current scanner position (which must be the opening DQUOTE). On success the cursor is positioned after the closing DQUOTE. The returned string is the unquoted content with quoted-pairs resolved (\" becomes ", \\ becomes \).

Errors: QuotedString::UnterminatedQuotedString (no closing DQUOTE), QuotedString::InvalidQuotedPair (backslash not followed by a valid quoted-pair byte), QuotedString::InvalidControlCharacter (a control character that is not HTAB inside the string), Limit::LimitExceeded (the string is longer than max_parameter_value_bytes).

#
percent_decode_bytes

fn percent_decode_bytes(bytes : Bytes, start : Int, end : Int) -> Result[Array[Byte], DispositionError]

Decodes the percent-encoded octets in bytes[start, end) into a raw byte array. Every % must be followed by exactly two hex digits.

Errors: PercentEncoding::InvalidPercentEncoding (% not followed by two hex digits, or a truncated escape at the end of the range).

#
percent_decode_string

fn percent_decode_string(input : String) -> Result[String, DispositionError]

Percent-decodes a whole string and validates the decoded bytes as UTF-8.

Errors: PercentEncoding::InvalidPercentEncoding and PercentEncoding::InvalidUtf8.

#
percent_encode_attr_value

fn percent_encode_attr_value(value : String) -> String

Percent-encodes a string for use as an RFC 8187 value. The string is UTF-8 encoded; every byte that is not an attr-char is percent-encoded with uppercase hex digits. The result is deterministic.

#
qdtext_char

fn qdtext_char(b : Byte) -> Bool

RFC 7230 qdtext: HTAB / SP / ! / #-[ / ]-~ / obs-text. Notably excludes " and \ and control characters.

#
quoted_pair_ok

fn quoted_pair_ok(b : Byte) -> Bool

A valid quoted-pair second byte: \ followed by HTAB / SP / VCHAR / obs-text (RFC 7230).

#
resolve_filename

fn resolve_filename(cd : ContentDisposition) -> Result[FilenameSelection, DispositionError]

Resolves the download filename from a ContentDisposition, applying the RFC 6266 Section 4.3 precedence rule: filename* wins over filename.

Errors: FilenameResolution::InvalidFilename when neither filename nor filename* is present.

#
run_cli

fn run_cli(args : Array[String]) -> String

Runs the CLI with the given arguments (excluding the program name) and returns the JSON output. Never raises, never unwraps.

#
sanitize_filename

fn sanitize_filename(name : String, policy : FilenamePolicy) -> Result[SafeFilenameResult, DispositionError]

Sanitises a filename with an explicit policy. The returned name is a single path component: it contains no / or \ and cannot be . or .., and it is never empty or a dot-only name. A non-empty input always produces a safe name (. and .. become _; a name made only of dots and spaces falls back to _). See FilenamePolicy for the per-profile character rules.

Errors: FilenamePolicy::UnsafeFilename when the input is empty.

#
sanitize_portable_filename

fn sanitize_portable_filename(name : String) -> Result[SafeFilenameResult, DispositionError]

Sanitises a filename with the default (Portable) policy. Equivalent to sanitize_filename(name, FilenamePolicy::default()).

Errors: FilenamePolicy::UnsafeFilename when the input is empty.

#
serialize_content_disposition

fn serialize_content_disposition(cd : ContentDisposition) -> Result[String, DispositionError]

Serialises a ContentDisposition to a header field value, deterministically.

The disposition type and every parameter name are emitted in canonical lowercase; parameters keep their model order. Token values must be valid tokens; a hand-built model with an invalid token value is rejected rather than silently re-quoted.

Errors: Serialization::UnexpectedCharacter (a token value is not a valid token).

#
serialize_content_disposition_preserve_case

fn serialize_content_disposition_preserve_case(cd : ContentDisposition) -> Result[String, DispositionError]

Serialises a ContentDisposition preserving the original casing of the disposition type and parameter names. Used by round-trip tests to check that parsing preserves information. Not recommended for output: use serialize_content_disposition for canonical output.

#
serialize_extended_value

fn serialize_extended_value(ev : ExtendedValue) -> String

Serialises an extended value deterministically. The charset is emitted in canonical form (UTF-8, or ISO-8859-1 when the value is entirely representable in Latin-1); the language is emitted when present; every value byte outside attr-char is percent-encoded with uppercase hex digits.

#
serialize_quoted_string

fn serialize_quoted_string(value : String) -> String

Serialises a string as a quoted-string, deterministically. Only " and \ are escaped (with a backslash); every other character is emitted as-is. The result always starts and ends with DQUOTE.

#
token_char

fn token_char(b : Byte) -> Bool

RFC 7230 tchar — allowed token characters.

#
valid_language_tag

fn valid_language_tag(tag : String) -> Bool

Validates an RFC 5646 Language-Tag. Implements the pragmatic subset used by RFC 8187 recipients: one or more subtags separated by -; the primary subtag is 2-8 letters (or the single letter x for private use); each extended subtag is 1-8 alphanumeric characters.

#
validate_token

fn validate_token(value : String) -> Bool

Validates that a whole string is a non-empty HTTP token: every byte is a tchar.