// quoted_string.mbt — RFC 7230 quoted-string parsing and serialisation.
//
// The RFC 6266 `value` rule uses the RFC 7230 quoted-string form: a byte
// sequence delimited by DQUOTE that may contain any `qdtext` byte and any
// `quoted-pair` (`\` followed by a valid byte). Semicolons and equals signs
// 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: `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`).
pub fn parse_quoted_string(
  cursor : Scanner,
  limits : Limits
) -> Result[String, DispositionError] {
  try {
    Ok(parse_quoted_string_inner(cursor, limits))
  } catch {
    e => Err(unwrap_disposition_error(e))
  }
}

fn parse_quoted_string_inner(cursor : Scanner, limits : Limits) -> String raise {
  if !cursor.consume_char(34) {
    raise disposition_error_at(
      QuotedString,
      UnexpectedCharacter,
      cursor.position(),
      "expected opening DQUOTE",
    )
  }
  let start = cursor.position()
  let sb = StringBuilder()
  let mut done = false
  while !done {
    if cursor.eof() {
      raise disposition_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 disposition_error_at(
          QuotedString,
          InvalidQuotedPair,
          pair_pos,
          "dangling escape at end of quoted-string",
        )
      }
      let next = cursor.next_byte().unwrap()
      if !quoted_pair_ok(next) {
        raise disposition_error_at(
          QuotedString,
          InvalidQuotedPair,
          pair_pos,
          cursor.context_string(),
        )
      }
      sb.write_char(next.to_char())
    } else if qdtext_char(b) {
      // obs-text bytes (>= 0x80) can start a multi-byte UTF-8 sequence: our
      // strings are UTF-8, so decode the whole sequence when its continuation
      // bytes are present rather than emitting one Latin-1 character per byte.
      let p = cursor.pos
      if b.to_int() >= 128 {
        let seq_len = utf8_sequence_len(b)
        if seq_len > 1 && has_continuation_bytes(cursor, p, seq_len) {
          sb.write_string(cursor.take_string(p, p + seq_len))
          cursor.pos = p + seq_len
        } else {
          sb.write_char(b.to_char())
          cursor.pos = p + 1
        }
      } else {
        sb.write_char(b.to_char())
        cursor.pos = p + 1
      }
    } else if is_control_byte(b) {
      raise disposition_error_at(
        QuotedString,
        InvalidControlCharacter,
        cursor.position(),
        cursor.context_string(),
      )
    } else {
      raise disposition_error_at(
        QuotedString,
        UnexpectedCharacter,
        cursor.position(),
        cursor.context_string(),
      )
    }
    if cursor.position() - start > limits.max_parameter_value_bytes() {
      raise disposition_error_at(
        Limit,
        LimitExceeded,
        cursor.position(),
        "quoted-string exceeds max_parameter_value_bytes",
      )
    }
  }
  sb.to_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.
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()
}

// The number of bytes in the UTF-8 sequence a lead byte announces (RFC 3629),
// or 1 when the byte is not a valid lead byte.
fn utf8_sequence_len(lead : Byte) -> Int {
  let v = lead.to_int()
  if v >= 0xC2 && v <= 0xDF {
    2
  } else if v >= 0xE0 && v <= 0xEF {
    3
  } else if v >= 0xF0 && v <= 0xF4 {
    4
  } else {
    1
  }
}

// Whether the bytes following `start` (offsets 1..len-1) are UTF-8
// continuation bytes (0x80-0xBF). DQUOTE and backslash can never be
// continuation bytes, so this cannot run past a delimiter.
fn has_continuation_bytes(cursor : Scanner, start : Int, len : Int) -> Bool {
  for j = 1; j < len; j = j + 1 {
    match cursor.byte_at(start + j) {
      Some(b) if b.to_int() >= 128 && b.to_int() <= 191 => ()
      _ => return false
    }
  }
  true
}

///|
/// 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.char_length() == 0 {
    return false
  }
  let bytes = @utf8.encode(value)
  for i = 0; i < bytes.length(); i = i + 1 {
    if !token_char(bytes[i]) {
      return false
    }
  }
  true
}

///|
/// 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.
pub fn is_header_safe(value : String) -> Bool {
  let bytes = @utf8.encode(value)
  for i = 0; i < bytes.length(); i = i + 1 {
    if is_control_byte(bytes[i]) {
      return false
    }
  }
  true
}