Skip to main content

src/internal/encoder/percent_encoding.gleam

//// Percent Encodes strings according to RFC 3986.
////
//// See the following links for reference:
//// - <https://tools.ietf.org/html/rfc3986>

import gleam/bit_array
import gleam/bool
import gleam/list
import gleam/order
import gleam/pair
import gleam/result
import gleam/string

import internal/encoder/encoding

/// Estimates the percent encoded size of a string in bytes, taking
/// maximum size of a line into account.
pub fn estimate_encoded_size(
  of string: String,
  maximum_size maximum_size: Int,
) -> Result(Int, encoding.EncoderError) {
  string
  |> string.split("")
  |> list.try_fold(0, fn(size, character) {
    let character_size = string.byte_size(character)

    case should_encode_character(character) {
      True -> Ok(size + { 3 * character_size })
      False -> Ok(size + character_size)
    }
    |> result.try(fn(size) {
      case size <= maximum_size {
        True -> Ok(size)
        False -> Error(encoding.MaximumSizeExceeded(maximum_size))
      }
    })
  })
}

/// Percent encode a string, taking the maximum line size into
/// account and pretending to start the first line at the passed
/// start position.
pub fn encode_string(
  encode string: String,
  start position: Int,
  maximum_size maximum_size: Int,
) -> Result(List(String), encoding.EncoderError) {
  string
  |> string.split("")
  |> do_encode_string(position, maximum_size)
}

fn do_encode_string(
  characters: List(String),
  count: Int,
  maximum_size: Int,
) -> Result(List(String), encoding.EncoderError) {
  characters
  |> list.try_fold(#(count, []), fn(accumulator, character) {
    let #(used_size, lines) = accumulator

    let encoded_character = encode_character(character)
    let encoded_character_size = string.byte_size(encoded_character)

    case lines {
      [] -> Ok(#(encoded_character_size, [encoded_character]))
      [first_line, ..other_lines]
        if used_size + encoded_character_size <= maximum_size
      ->
        Ok(
          #(used_size + encoded_character_size, [
            first_line <> encoded_character,
            ..other_lines
          ]),
        )
      _otherwise -> Error(encoding.MaximumSizeExceeded(maximum_size))
    }
  })
  |> result.map(pair.second)
}

fn should_encode_character(character: String) -> Bool {
  {
    string.compare(character, "0") == order.Lt
    || string.compare(character, "9") == order.Gt
  }
  && {
    string.compare(character, "A") == order.Lt
    || string.compare(character, "Z") == order.Gt
  }
  && {
    string.compare(character, "a") == order.Lt
    || string.compare(character, "z") == order.Gt
  }
  && !string.contains("-_.~", character)
}

fn encode_character(character: String) -> String {
  use <- bool.guard(
    when: !should_encode_character(character),
    return: character,
  )
  do_encode_bytes(bit_array.from_string(character), [])
}

fn do_encode_bytes(bytes: BitArray, acc: List(String)) -> String {
  case bytes {
    <<byte:bytes-size(1), rest:bytes>> ->
      do_encode_bytes(rest, ["%" <> bit_array.base16_encode(byte), ..acc])
    <<_:bits>> -> acc |> list.reverse() |> string.join("")
  }
}