// scanner.mbt — Byte-oriented cursor for moon-content-disposition.
//
// A Content-Disposition header value cannot be parsed with `split(";")`: a
// semicolon inside a quoted-string (or inside an RFC 8187 extended value)
// is not a structural separator. This module provides a single,
// bounds-checked cursor over the UTF-8 bytes of the input. Every operation
// is safe at any position; nothing here can index out of bounds or loop
// forever.
//
// Positions are UTF-8 byte offsets into the original input string.
///|
/// A bounds-checked byte cursor over an input string.
pub struct Scanner {
bytes : Bytes
len : Int
mut pos : Int
}
///|
/// Creates a scanner for a Content-Disposition header field value.
pub fn Scanner::new(input : String) -> Scanner {
let bytes = @utf8.encode(input)
{ bytes, len: bytes.length(), pos: 0 }
}
///|
/// The current position, as a UTF-8 byte offset.
pub fn Scanner::position(self : Scanner) -> Int {
self.pos
}
///|
/// Whether the cursor is at (or past) the end of the input.
pub fn Scanner::eof(self : Scanner) -> Bool {
self.pos >= self.len
}
///|
/// The number of bytes remaining from the current position.
pub fn Scanner::remaining(self : Scanner) -> Int {
if self.len > self.pos {
self.len - self.pos
} else {
0
}
}
///|
/// The total length of the input in bytes.
pub fn Scanner::total_bytes(self : Scanner) -> Int {
self.len
}
///|
/// The byte at the current position, or `None` at end of input.
pub fn Scanner::peek_byte(self : Scanner) -> Byte? {
if self.pos >= self.len {
None
} else {
Some(self.bytes[self.pos])
}
}
///|
/// The byte at `pos + rel`, or `None` when out of bounds. `rel` may be
/// negative to look behind the current position.
pub fn Scanner::peek_at(self : Scanner, rel : Int) -> Byte? {
let idx = self.pos + rel
if idx < 0 || idx >= self.len {
None
} else {
Some(self.bytes[idx])
}
}
///|
/// The byte at an absolute offset, or `None` when out of bounds.
pub fn Scanner::byte_at(self : Scanner, idx : Int) -> Byte? {
if idx < 0 || idx >= self.len {
None
} else {
Some(self.bytes[idx])
}
}
///|
/// Returns the current byte and advances the cursor by one. Returns `None`
/// at end of input (and does not advance).
pub fn Scanner::next_byte(self : Scanner) -> Byte? {
if self.pos >= self.len {
None
} else {
let b = self.bytes[self.pos]
self.pos = self.pos + 1
Some(b)
}
}
///|
/// If the current byte equals `b`, advances and returns `true`.
pub fn Scanner::consume_char(self : Scanner, b : Byte) -> Bool {
match self.peek_byte() {
Some(c) if c == b => {
self.pos = self.pos + 1
true
}
_ => false
}
}
///|
/// Skips optional whitespace (OWS): SP (0x20) and HTAB (0x09). CR and LF
/// are never skipped.
pub fn Scanner::skip_ows(self : Scanner) -> Unit {
let mut done = false
while !done {
match self.peek_byte() {
Some(b) if b == 32 || b == 9 => self.pos = self.pos + 1
_ => done = true
}
}
}
///|
/// Whether the current byte is optional whitespace under this grammar.
pub fn Scanner::is_ows(self : Scanner) -> Bool {
match self.peek_byte() {
Some(b) => b == 32 || b == 9
None => false
}
}
///|
/// Moves the cursor to an absolute byte offset, clamping out-of-range
/// positions to the ends of the input.
pub fn Scanner::seek(self : Scanner, pos : Int) -> Unit {
if pos < 0 {
self.pos = 0
} else if pos > self.len {
self.pos = self.len
} else {
self.pos = pos
}
}
///|
/// Consumes a run of bytes satisfying `pred` and returns the `(start, end)`
/// byte range of the run. The run may be empty.
pub fn Scanner::consume_while(self : Scanner, pred : (Byte) -> Bool) -> (Int, Int) {
let start = self.pos
let mut done = false
while !done {
match self.peek_byte() {
Some(b) if pred(b) => self.pos = self.pos + 1
_ => done = true
}
}
(start, self.pos)
}
///|
/// Consumes a run of HTTP token characters and returns the `(start, end)`
/// byte range. The run may be empty.
pub fn Scanner::consume_token(self : Scanner) -> (Int, Int) {
self.consume_while(fn(b) { token_char(b) })
}
///|
/// 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.
pub fn Scanner::consume_value_chars(self : Scanner) -> (Int, Int) {
let start = self.pos
let mut done = false
while !done {
match self.peek_byte() {
Some(b) if attr_char(b) || b == 37 => self.pos = self.pos + 1
_ => done = true
}
}
(start, self.pos)
}
///|
/// The index of the next occurrence of byte `b` at or after the current
/// position, or `None`.
pub fn Scanner::find_byte(self : Scanner, b : Byte) -> Int? {
let mut i = self.pos
while i < self.len {
if self.bytes[i] == b {
return Some(i)
}
i = i + 1
}
None
}
///|
/// 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.
pub fn Scanner::take_string(self : Scanner, start : Int, end : Int) -> String {
let hi = if end > self.len { self.len } else { end }
let lo = if start < 0 { 0 } else { start }
if hi <= lo {
return ""
}
try {
@utf8.decode(self.bytes.view(start=lo, end=hi))
} catch {
_ => self.bytes.view(start=lo, end=hi).to_owned().to_unchecked_string()
}
}
///|
/// A short, bounded excerpt of the input around the current position, for
/// use in error context strings. Never longer than `max_context_bytes()`.
pub fn Scanner::context_string(self : Scanner) -> String {
let window = max_context_bytes()
let mut start = self.pos - 24
if start < 0 {
start = 0
}
let mut take = window - 8
if take > self.len - start {
take = self.len - start
}
if take < 0 {
take = 0
}
let excerpt = self.take_string(start, start + take)
let prefix = if start > 0 { "..." } else { "" }
let suffix = if start + take < self.len { "..." } else { "" }
prefix + excerpt + suffix
}
///|
/// An excerpt of the input around the current position using an explicit
/// context bound (from the active `Limits`).
pub fn Scanner::context_string_limited(self : Scanner, limit : Int) -> String {
let window = if limit < 16 { 16 } else { limit }
let mut start = self.pos - 24
if start < 0 {
start = 0
}
let mut take = window - 8
if take > self.len - start {
take = self.len - start
}
if take < 0 {
take = 0
}
let excerpt = self.take_string(start, start + take)
let prefix = if start > 0 { "..." } else { "" }
let suffix = if start + take < self.len { "..." } else { "" }
prefix + excerpt + suffix
}