moon-http-range

RFC 9110 HTTP byte-range parser, resolver, serializer, response planner and audit toolkit for MoonBit.

http
range
byte-range
rfc9110
content-range
partial-content
moonbit
moon add hjn0123/moon-http-range@0.1.0-dev
Download zip
Author
Version
0.1.0-dev
License
Apache-2.0
Last updated
4 days ago
Downloads
1
README

#moon-http-range

moon-http-range is a strict RFC 9110-oriented HTTP byte-range parser, resolver, serializer, response planner, and audit toolkit for MoonBit.

  • Module: hjn0123/moon-http-range
  • Version: 0.1.0-dev
  • Status: local mature MVP
  • Publishing: Mooncakes package hjn0123/moon-http-range
  • Maintainer namespace: hjn0123
  • Source repository: https://github.com/hjn0123/moon-http-range
  • Security contact: 2498311403@qq.com

#Overview

The library implements the range semantics that an HTTP server, object store, download service, media service, or client can reuse. It is deliberately a pure semantic layer: it accepts header values and representation lengths, and returns typed values or plans. It does not open files, send network data, or decide an application's authorization and caching policy.

#Why HTTP Range

Range requests let a recipient ask for selected parts of a representation. Common uses include resumable downloads, seeking in media, serving large static objects, and fetching small regions of an archive. Correct handling requires more than splitting on punctuation: the parser must distinguish invalid syntax from a valid but unsatisfiable range and must resolve suffix and open-ended forms safely.

#RFC 9110 Scope

The implemented scope covers RFC 9110 Sections 14.1–14.4 and planning metadata for 14.6, plus status guidance for 206 and 416 and defensive controls related to Range denial-of-service considerations. See docs/specification-map.md for the implementation and test map.

#Range Model

ByteRangeSpec retains the request form and order:

  • Closed(first, last) for 0-499;
  • OpenEnded(first) for 500-;
  • Suffix(length) for -500.

Resolved ConcreteRange values are inclusive: [first, last]. Their length is last - first + 1. Positions and representation lengths use Int64.

Unknown range units are represented as Other(unit) with their raw range set. The library never applies byte semantics to an unknown unit.

#Parsing Range

let request = @range.parse_range("bytes=0-499,-100").unwrap()
println(@range.serialize_range(request))

parse_range_with_limits accepts an explicit Limits profile. Decimal values are accumulated one digit at a time and checked before multiplication, so values above 9223372036854775807 return IntegerOverflow rather than wrapping.

Errors carry a stage, kind, offset, and context. The offset is a zero-based MoonBit string code-unit offset, not a UTF-8 byte offset.

#Resolving Byte Ranges

let request = @range.parse_range("bytes=500-,-100").unwrap()
let result = @range.resolve_byte_ranges(request, 1_000L).unwrap()

Resolution clips a closed range at the representation end, expands an open-ended range to the end, and maps a suffix range to the final requested bytes. The result includes satisfiable ranges, the original member count, and the count of unsatisfiable members.

#Satisfiability

Syntax and satisfiability are separate:

  • bytes=500-400 is syntactically invalid;
  • bytes=500-600 is valid but unsatisfiable for a 100-byte representation;
  • bytes=0-99,9999-10000 is satisfiable for a 1000-byte representation because one member is satisfiable;
  • bytes=-0 parses but resolves to no range;
  • every byte range is unsatisfiable for a zero-length representation.

#Accept-Ranges

parse_accept_ranges handles none, bytes, and comma-separated range units. Unit comparison is ASCII case-insensitive. none is reserved and cannot be combined with another unit. supports_bytes() and supports(unit) provide queries, while serialize_accept_ranges provides deterministic output.

#Content-Range

parse_content_range and serialize_content_range support:

bytes 0-499/1234 bytes 42-1233/* bytes */1234

For a known complete length, the length must be greater than the last byte position. unsatisfied_content_range(length) creates the value commonly used with a 416 response. Unknown units are retained without invented semantics.

#Response Planning

plan_byte_range_response is a pure helper. Given a parsed request, length, and caller policy flags, it returns Ignore, Single, Multiple, or Unsatisfiable. recommended_status() maps those to 200, 206, 206, and 416. This is guidance after the caller supplies its HTTP policy; the library does not control server behavior.

#Multiple Ranges

plan_multipart_ranges validates a caller-provided deterministic boundary and produces per-part range, Content-Range, and content length metadata. multipart_content_type creates multipart/byteranges; boundary=.... No multipart body is parsed or streamed.

Server-side helpers are explicit and separate from parsing: sort_ranges, merge_overlapping_ranges, merge_adjacent_ranges, and coalesce_ranges. Parsing and canonical serialization preserve request order and never merge members automatically.

#Audit

audit_range_request reports advisory findings including excessive counts, overlap, duplicates, descending order, many small ranges, zero-length suffixes, large values or headers, adjacent ranges, unknown units, and inefficient sets. An audit warning does not change parser validity. Resource limits are enforced separately by Limits::default(), Limits::strict(), and Limits::permissive().

#CLI

The executable package is cmd/range-tool:

moon run cmd/range-tool -- parse --range "bytes=0-499,-100" moon run cmd/range-tool -- resolve --range "bytes=0-499,-100" --length 1000 moon run cmd/range-tool -- content-range --value "bytes 0-499/1000" moon run cmd/range-tool -- accept-ranges --value "bytes, exampleunit" moon run cmd/range-tool -- plan --range "bytes=0-99,900-" --length 1000 moon run cmd/range-tool -- audit --range "bytes=0-0,1-1,2-2,3-3" --length 1000 moon run cmd/range-tool -- stats --range "bytes=0-99" moon run cmd/range-tool -- version

Commands emit deterministic text or JSON. Argument normalization is isolated and tested because native, JavaScript, and WebAssembly launchers can expose the program name differently.

#Examples

Four executable examples live under examples/parse, examples/resolve, examples/content_range, and examples/response_plan.

moon run examples/parse --target wasm-gc moon run examples/resolve --target js moon run examples/content_range --target native moon run examples/response_plan --target wasm-gc

#Testing

The suite contains focused named tests, 900 deterministic generated cases, and every-prefix truncation checks. It covers parsing, overflow, limits, resolution, normalization, headers, planning, audit, CLI normalization, and reconstructed RFC examples on wasm-gc, js, and native.

Run the fail-fast verification script from PowerShell:

powershell -ExecutionPolicy Bypass -File scripts/verify_all.ps1

#Security

Parsing a Range field is not access control. Callers must authorize the selected representation and guard the cost of reading, buffering, compressing, and transmitting each part. Use limits before parsing untrusted fields and use the audit report as one input to server policy. See docs/security.md.

#Limitations

There is no HTTP server or client, file I/O, networking, If-Range, ETag or date evaluation, MIME parser, multipart parser, cache, or authorization system. See docs/limitations.md.

#Release Status

This directory contains the verified MVP source for the hjn0123/moon-http-range module. The development version is intended for the initial Mooncakes release.

#License

Licensed under Apache License 2.0. See LICENSE. RFC-derived test fixture attribution is recorded in THIRD_PARTY_NOTICES.md.

#
RangeError

pub(all) suberror RangeError {
RangeError(RangeErrorStage, RangeErrorKind, Int, String)
}

Offset is a zero-based MoonBit string code-unit offset, not a UTF-8 byte offset.

#
RangeError::context

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

#
RangeError::kind

#
RangeError::offset

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

#
RangeError::stage

#
RangeError::to_string

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

#
AcceptRanges

pub(all) enum AcceptRanges {
NoneAccepted
Units(Array[String])
} derive(Eq,
Debug
)

Parsed Accept-Ranges field. NoneAccepted is the reserved none value.

#
AcceptRanges::supports

fn AcceptRanges::supports(self : AcceptRanges, unit : String) -> Bool

#
AcceptRanges::supports_bytes

fn AcceptRanges::supports_bytes(self : AcceptRanges) -> Bool

#
AcceptRanges::units

fn AcceptRanges::units(self : AcceptRanges) -> Array[String]

#
AuditFinding

pub struct AuditFinding {
kind : AuditFindingKind
context : String
} derive(Eq,
Debug
)

#
AuditFinding::context

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

#
AuditFinding::kind

#
AuditFindingKind

pub(all) enum AuditFindingKind {
TooManyRanges
OverlappingRanges
ManySmallRanges
DescendingOrder
DuplicateRanges
ZeroLengthSuffix
LargeNumericValue
LargeHeader
UnknownRangeUnit
AdjacentRanges
InefficientRangeSet
} derive(Eq,
Debug
)

Advisory finding; audit never changes parser validity.

#
AuditFindingKind::name

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

#
AuditReport

pub struct AuditReport {
findings : Array[AuditFinding]
} derive(Eq,
Debug
)

#
AuditReport::findings

fn AuditReport::findings(self : AuditReport) -> Array[AuditFinding]

#
AuditReport::is_clean

fn AuditReport::is_clean(self : AuditReport) -> Bool

#
ByteRangeSpec

pub(all) enum ByteRangeSpec {
Closed(Int64, Int64)
OpenEnded(Int64)
Suffix(Int64)
} derive(Eq,
Debug
)

One syntactically valid byte-range-spec or suffix-byte-range-spec.

#
ConcreteRange

pub struct ConcreteRange {
first : Int64
last : Int64
} derive(Eq,
Debug
)

Inclusive concrete byte interval [first, last].

#
ConcreteRange::first

fn ConcreteRange::first(self : ConcreteRange) -> Int64

#
ConcreteRange::last

fn ConcreteRange::last(self : ConcreteRange) -> Int64

#
ConcreteRange::length

fn ConcreteRange::length(self : ConcreteRange) -> Int64

#
ContentRangeValue

pub(all) enum ContentRangeValue {
Satisfied(RangeUnit, Int64, Int64, Int64?)
Unsatisfied(RangeUnit, Int64)
OtherContentRange(String, String)
} derive(Eq,
Debug
)

Content-Range value for satisfied or unsatisfied responses.

#
Limits

pub struct Limits {
max_input_bytes : Int
max_ranges : Int
max_digits_per_number : Int
max_unit_bytes : Int
max_raw_other_range_bytes : Int
} derive(Eq,
Debug
)

Resource limits applied before and during header parsing.

#
Limits::default

fn Limits::default() -> Limits

#
Limits::max_digits_per_number

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

#
Limits::max_input_bytes

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

#
Limits::max_ranges

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

#
Limits::max_raw_other_range_bytes

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

#
Limits::max_unit_bytes

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

#
Limits::permissive

fn Limits::permissive() -> Limits

#
Limits::strict

fn Limits::strict() -> Limits

#
MultipartPartPlan

pub struct MultipartPartPlan {
range : ConcreteRange
content_range : ContentRangeValue
content_length : Int64
} derive(Eq,
Debug
)

Per-part metadata for a caller-produced multipart/byteranges body.

#
MultipartPartPlan::content_length

fn MultipartPartPlan::content_length(self : MultipartPartPlan) -> Int64

#
MultipartPartPlan::content_range

#
MultipartPartPlan::range

#
MultipartPlan

pub struct MultipartPlan {
boundary : String
content_type : String
parts : Array[MultipartPartPlan]
} derive(Eq,
Debug
)

#
MultipartPlan::boundary

fn MultipartPlan::boundary(self : MultipartPlan) -> String

#
MultipartPlan::content_type

fn MultipartPlan::content_type(self : MultipartPlan) -> String

#
MultipartPlan::parts

#
RangeErrorKind

pub(all) enum RangeErrorKind {
EmptyInput
MissingEquals
InvalidUnit
EmptyRangeSet
InvalidRange
MissingNumber
InvalidNumber
IntegerOverflow
LastBeforeFirst
InvalidCompleteLength
LimitExceeded
UnexpectedCharacter
InvalidRepresentationLength
UnsupportedRangeUnit
InvalidBoundary
} derive(Eq,
Debug
)

Machine-readable error category.

#
RangeErrorKind::name

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

#
RangeErrorStage

pub(all) enum RangeErrorStage {
Input
Unit
Syntax
Number
RangeSpec
ContentRange
AcceptRanges
Resolve
Limit
Plan
} derive(Eq,
Debug
)

Processing stage for a structured range error.

#
RangeErrorStage::name

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

#
RangeRequest

pub struct RangeRequest {
unit : RangeUnit
specs : Array[ByteRangeSpec]
raw_other_range_set : String?
} derive(Eq,
Debug
)

Parsed Range field. Byte units carry structured specs; other units keep raw data.

#
RangeRequest::raw_other_range_set

fn RangeRequest::raw_other_range_set(self : RangeRequest) -> String?

#
RangeRequest::specs

#
RangeRequest::unit

fn RangeRequest::unit(self : RangeRequest) -> RangeUnit

#
RangeUnit

pub(all) enum RangeUnit {
Bytes
Other(String)
} derive(Eq,
Debug
)

Range unit. Other keeps an unknown unit without inferring semantics.

#
RangeUnit::is_bytes

fn RangeUnit::is_bytes(self : RangeUnit) -> Bool

#
RangeUnit::name

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

#
ResolvedRangeSet

pub struct ResolvedRangeSet {
satisfiable_ranges : Array[ConcreteRange]
unsatisfiable_count : Int
original_count : Int
} derive(Eq,
Debug
)

Resolution preserves how many original members could not be satisfied.

#
ResolvedRangeSet::original_count

fn ResolvedRangeSet::original_count(self : ResolvedRangeSet) -> Int

#
ResolvedRangeSet::ranges

#
ResolvedRangeSet::satisfiable_count

fn ResolvedRangeSet::satisfiable_count(self : ResolvedRangeSet) -> Int

#
ResolvedRangeSet::unsatisfiable_count

fn ResolvedRangeSet::unsatisfiable_count(self : ResolvedRangeSet) -> Int

#
ResponsePlan

pub(all) enum ResponsePlan {
Ignore
Unsatisfiable(Int64)
Single(ConcreteRange)
Multiple(Array[ConcreteRange])
} derive(Eq,
Debug
)

Pure response plan. The caller remains responsible for HTTP policy and I/O.

#
ResponsePlan::ranges

#
ResponsePlan::recommended_status

fn ResponsePlan::recommended_status(self : ResponsePlan) -> Int

Suggested status assuming the caller applies this plan.

#
audit_range_request

fn audit_range_request(request : RangeRequest, raw_input? : String, representation_length? : Int64) -> AuditReport

Audit raw request shape and, when available, resolved byte ranges.

#
canonicalize_range

fn canonicalize_range(input : String) -> Result[String, RangeError]

Parse then serialize; syntax normalization does not merge ranges.

#
coalesce_ranges

fn coalesce_ranges(input : Array[ConcreteRange]) -> Array[ConcreteRange]

Server-side planning helper: sort, then merge overlap and adjacency.

#
concrete_range

fn concrete_range(first : Int64, last : Int64) -> ConcreteRange

#
library_version

fn library_version() -> String

#
merge_adjacent_ranges

fn merge_adjacent_ranges(input : Array[ConcreteRange]) -> Array[ConcreteRange]

Merge ranges that overlap or touch at one byte boundary.

#
merge_overlapping_ranges

fn merge_overlapping_ranges(input : Array[ConcreteRange]) -> Array[ConcreteRange]

Merge overlapping ranges, preserving gaps and not merging adjacency.

#
multipart_content_type

fn multipart_content_type(boundary : String) -> String

#
parse_accept_ranges

fn parse_accept_ranges(input : String) -> Result[AcceptRanges, RangeError]

#
parse_accept_ranges_with_limits

fn parse_accept_ranges_with_limits(input : String, limits : Limits) -> Result[AcceptRanges, RangeError]

#
parse_content_range

fn parse_content_range(input : String) -> Result[ContentRangeValue, RangeError]

#
parse_content_range_with_limits

fn parse_content_range_with_limits(input : String, limits : Limits) -> Result[ContentRangeValue, RangeError]

#
parse_decimal_int64

fn parse_decimal_int64(input : String) -> Result[Int64, RangeError]

Parse a non-empty unsigned decimal into Int64 with pre-multiplication overflow checks.

#
parse_range

fn parse_range(input : String) -> Result[RangeRequest, RangeError]

Parse a Range field value using default resource limits.

#
parse_range_with_limits

fn parse_range_with_limits(input : String, limits : Limits) -> Result[RangeRequest, RangeError]

Strict RFC 9110-oriented Range parser. It preserves byte-spec order.

#
plan_byte_range_response

fn plan_byte_range_response(request : RangeRequest, representation_length : Int64, method_is_get : Bool, range_supported : Bool, apply_ranges : Bool) -> Result[ResponsePlan, RangeError]

#
plan_multipart_ranges

fn plan_multipart_ranges(ranges : Array[ConcreteRange], representation_length : Int64, boundary : String) -> Result[MultipartPlan, RangeError]

#
range_error

fn range_error(stage : RangeErrorStage, kind : RangeErrorKind, offset : Int, context : String) -> RangeError

#
range_request

fn range_request(unit : RangeUnit, specs : Array[ByteRangeSpec], raw_other_range_set? : String) -> RangeRequest

#
resolve_byte_ranges

fn resolve_byte_ranges(request : RangeRequest, representation_length : Int64) -> Result[ResolvedRangeSet, RangeError]

Resolve byte specs against the selected representation length.

#
resolved_range_set

fn resolved_range_set(ranges : Array[ConcreteRange], unsatisfiable_count : Int, original_count : Int) -> ResolvedRangeSet

#
serialize_accept_ranges

fn serialize_accept_ranges(value : AcceptRanges) -> String

#
serialize_content_range

fn serialize_content_range(value : ContentRangeValue) -> String

#
serialize_range

fn serialize_range(request : RangeRequest) -> String

Serialize a parsed Range field without sorting or coalescing specs.

#
sort_ranges

fn sort_ranges(input : Array[ConcreteRange]) -> Array[ConcreteRange]

Return a copy sorted by inclusive first position, then last position.

#
unsatisfied_content_range

fn unsatisfied_content_range(length : Int64) -> Result[ContentRangeValue, RangeError]