//// Encode bit strings according to RFC 2045. //// //// See the following link for reference: //// - //// - import gleam/bool import gleam/float import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/order import gleam/result import gleam/string import internal/encoder/encoding.{Ascii} /// Estimates the `bits' bit encoded size of a string in bytes, taking /// maximum size of a line into account. pub fn estimate_encoded_size( of string: String, in bits: Int, with mode: encoding.EncodingMode, maximum_size maximum_size: Int, ) -> Result(Int, encoding.EncoderError) { let max_ascii = max_ascii_from_bits(bits, mode) string |> string.split("\r\n") |> list.try_fold(0, fn(size, line) { case validate_line(line, max_ascii, maximum_size) { Ok(line_size) -> Ok(size + line_size) Error(_) as error -> error } }) } /// Bit encode a string with the required number of bits, using the /// specified encoding mode and taking into account the maximum allowed /// line size. pub fn encode_string( encode string: String, in bits: Int, with mode: encoding.EncodingMode, maximum_size maximum_size: Int, ) -> Result(List(String), encoding.EncoderError) { case string { "" -> Ok([]) _ -> { let max_ascii = max_ascii_from_bits(bits, mode) string |> string.split("\r\n") |> list.try_map(fn(line) { case validate_line(line, max_ascii, maximum_size) { Ok(_) -> Ok(line) Error(error) -> Error(error) } }) } } } fn max_ascii_from_bits( bits: Int, mode: encoding.EncodingMode, ) -> Option(String) { case bits, mode { 8, encoding.Utf8 -> None _, Ascii -> { int.power(2, int.to_float(bits)) |> result.map(fn(ascii_code) { int.min(float.truncate(ascii_code) - 1, 127) }) |> result.try(string.utf_codepoint) |> result.map(fn(codepoint) { string.from_utf_codepoints([codepoint]) }) |> result.unwrap("") |> Some } _, encoding.Utf8 -> { int.power(2, int.to_float(bits)) |> result.map(fn(ascii_code) { float.truncate(ascii_code) - 1 }) |> result.try(string.utf_codepoint) |> result.map(fn(codepoint) { string.from_utf_codepoints([codepoint]) }) |> result.unwrap("") |> Some } } } fn validate_line( line: String, max_ascii: Option(String), maximum_size: Int, ) -> Result(Int, encoding.EncoderError) { let line_size = string.byte_size(line) use <- bool.guard( when: line_size > maximum_size, return: Error(encoding.MaximumSizeExceeded(maximum_size)), ) let validation = line |> string.split("") |> list.find(fn(character) { !is_valid_character(character, max_ascii) }) case validation { Ok(character) -> Error(encoding.InvalidCharacter(character)) Error(Nil) -> Ok(line_size) } } fn is_valid_character(character: String, max_ascii: Option(String)) -> Bool { string.compare(character, "\r") != order.Eq && string.compare(character, "\n") != order.Eq && string.compare(character, "\u{0}") == order.Gt && { max_ascii |> option.map(fn(ascii) { string.compare(character, ascii) == order.Lt }) |> option.unwrap(True) } }