// quoted_string.mbt — RFC 7230 quoted-string parsing and serialisation.
//
// The RFC 8288 link grammar uses RFC 7230 quoted-strings for parameter
// values such as `rel="next prev"`, `title="..."`, and `anchor="..."`.
// A quoted-string is a byte sequence delimited by DQUOTE that may contain
// any `qdtext` byte and any `quoted-pair` (`\` followed by a valid byte).
// Commas and semicolons inside a quoted-string are data, not delimiters —
// which is exactly why the scanner is a cursor and not `split`.
///|
/// 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: `UnterminatedQuotedString` (no closing DQUOTE), `InvalidQuotedPair`
/// (backslash not followed by a valid quoted-pair byte), `UnexpectedCharacter`
/// (a control character that is not HTAB inside the string),
/// `LimitExceeded` (the string is longer than `max_quoted_string_bytes`).
pub fn parse_quoted_string(cursor : Scanner, limits : Limits) -> String raise {
if !cursor.consume_char(34) {
raise link_error_at(
QuotedString,
ExpectedAngleBracket,
cursor.position(),
"expected opening DQUOTE",
)
}
let start = cursor.position()
let sb = StringBuilder()
let mut done = false
while !done {
if cursor.eof() {
raise link_error_at(
QuotedString,
UnterminatedQuotedString,
start,
cursor.context_string(),
)
}
let b = cursor.peek_byte().unwrap()
if b == 34 {
cursor.pos = cursor.pos + 1
done = true
} else if b == 92 {
// backslash: quoted-pair
let pair_pos = cursor.position()
cursor.pos = cursor.pos + 1
if cursor.eof() {
raise link_error_at(
QuotedString,
UnterminatedQuotedString,
pair_pos,
cursor.context_string(),
)
}
let next = cursor.next_byte().unwrap()
if !quoted_pair_ok(next) {
raise link_error_at(
QuotedString,
InvalidQuotedPair,
pair_pos,
cursor.context_string(),
)
}
sb.write_char(next.to_char())
} else if qdtext_char(b) {
sb.write_char(b.to_char())
cursor.pos = cursor.pos + 1
} else {
raise link_error_at(
QuotedString,
UnexpectedCharacter,
cursor.position(),
cursor.context_string(),
)
}
if cursor.position() - start > limits.max_quoted_string_bytes() {
raise link_error_at(
Limit,
LimitExceeded,
cursor.position(),
"quoted-string exceeds max_quoted_string_bytes",
)
}
}
sb.to_string()
}
///|
/// Serialises a string as a quoted-string, deterministically. Only `"` and
/// `\` are escaped (with a backslash); every other byte is emitted as-is.
/// The result always starts and ends with DQUOTE.
pub fn serialize_quoted_string(value : String) -> String {
let sb = StringBuilder()
sb.write_char('"')
for ch in value {
if ch == '"' || ch == '\\' {
sb.write_char('\\')
}
sb.write_char(ch)
}
sb.write_char('"')
sb.to_string()
}
///|
/// 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.
pub fn can_be_token(value : String) -> Bool {
if value.length() == 0 {
return false
}
let bytes = @utf8.encode(value)
let mut ok = true
for i = 0; i < bytes.length(); i = i + 1 {
if !token_char(bytes[i]) {
ok = false
}
}
ok
}