///|
/// Multibase encodings supported by MoonLoom's first milestone.
pub(all) enum Base {
Base16
Base16Upper
Base32
Base32Upper
Base32Pad
Base32PadUpper
Base58Btc
Base64
Base64Pad
Base64Url
Base64UrlPad
} derive(Eq, @debug.Debug)
///|
/// Result of decoding a Multibase string.
pub(all) struct DecodedMultibase {
base : Base
data : Bytes
} derive(Eq, @debug.Debug)
///|
pub fn Base::prefix(self : Base) -> Char {
match self {
Base16 => 'f'
Base16Upper => 'F'
Base32 => 'b'
Base32Upper => 'B'
Base32Pad => 'c'
Base32PadUpper => 'C'
Base58Btc => 'z'
Base64 => 'm'
Base64Pad => 'M'
Base64Url => 'u'
Base64UrlPad => 'U'
}
}
///|
pub fn Base::from_prefix(prefix : Char) -> Base? {
match prefix {
'f' => Some(Base16)
'F' => Some(Base16Upper)
'b' => Some(Base32)
'B' => Some(Base32Upper)
'c' => Some(Base32Pad)
'C' => Some(Base32PadUpper)
'z' => Some(Base58Btc)
'm' => Some(Base64)
'M' => Some(Base64Pad)
'u' => Some(Base64Url)
'U' => Some(Base64UrlPad)
_ => None
}
}
///|
const HEX_LOWER : Bytes = b"0123456789abcdef"
///|
const HEX_UPPER : Bytes = b"0123456789ABCDEF"
///|
const BASE32_LOWER : Bytes = b"abcdefghijklmnopqrstuvwxyz234567"
///|
const BASE32_UPPER : Bytes = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
///|
const BASE58_BTC : Bytes = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
///|
fn hex_value(code : Int) -> Int {
match code {
48..=57 => code - 48
65..=70 => code - 65 + 10
97..=102 => code - 97 + 10
_ => -1
}
}
///|
fn base32_value(code : Int, upper : Bool) -> Int {
let a = if upper { 65 } else { 97 }
if code >= a && code <= a + 25 {
code - a
} else if code >= 50 && code <= 55 {
code - 50 + 26
} else if !upper && code >= 65 && code <= 90 {
-1
} else if upper && code >= 97 && code <= 122 {
-1
} else {
-1
}
}
///|
fn encode_hex(data : BytesView, upper : Bool) -> String {
let alphabet = if upper { HEX_UPPER } else { HEX_LOWER }
let out = StringBuilder(size_hint=data.length() * 2)
for byte in data {
let value = byte.to_int()
out.write_char(alphabet[(value >> 4) & 0x0F].to_char())
out.write_char(alphabet[value & 0x0F].to_char())
}
out.to_string()
}
///|
fn decode_hex(text : StringView, upper : Bool) -> Result[Bytes, MoonLoomError] {
if text.length() % 2 != 0 {
return Err(InvalidPadding("base16", text.length()))
}
let out = @buffer.Buffer(size_hint=text.length() / 2)
let mut index = 0
while index < text.length() {
let high = hex_value(text.unsafe_get(index).to_int())
let low = hex_value(text.unsafe_get(index + 1).to_int())
if high < 0 || low < 0 {
let prefix = if upper { 'F' } else { 'f' }
return Err(InvalidBaseCharacter(prefix, index))
}
let high_code = text.unsafe_get(index).to_int()
let low_code = text.unsafe_get(index + 1).to_int()
let high_is_alpha = high_code >= 65
let low_is_alpha = low_code >= 65
let high_case_ok = if upper {
high_code >= 65 && high_code <= 70
} else {
high_code >= 97 && high_code <= 102
}
if high_is_alpha && !high_case_ok {
return Err(InvalidBaseCharacter(if upper { 'F' } else { 'f' }, index))
}
let low_case_ok = if upper {
low_code >= 65 && low_code <= 70
} else {
low_code >= 97 && low_code <= 102
}
if low_is_alpha && !low_case_ok {
return Err(InvalidBaseCharacter(if upper { 'F' } else { 'f' }, index + 1))
}
out.write_byte(((high << 4) | low).to_byte())
index += 2
}
Ok(out.to_bytes())
}
///|
fn encode_base32(data : BytesView, alphabet : Bytes, padding : Bool) -> String {
let out = StringBuilder(size_hint=(data.length() + 4) / 5 * 8)
let mut accumulator = 0
let mut bits = 0
for byte in data {
accumulator = (accumulator << 8) | byte.to_int()
bits += 8
while bits >= 5 {
bits -= 5
out.write_char(alphabet[(accumulator >> bits) & 0x1F].to_char())
}
accumulator = if bits == 0 { 0 } else { accumulator & ((1 << bits) - 1) }
}
if bits > 0 {
out.write_char(alphabet[(accumulator << (5 - bits)) & 0x1F].to_char())
}
if padding {
let written = (data.length() * 8 + 4) / 5
let remainder = written % 8
if remainder != 0 {
for _ in 0..<(8 - remainder) {
out.write_char('=')
}
}
}
out.to_string()
}
///|
fn decode_base32(
text : StringView,
prefix : Char,
upper : Bool,
padding : Bool,
) -> Result[Bytes, MoonLoomError] {
let mut data_length = text.length()
if padding {
if text.length() % 8 != 0 {
return Err(InvalidPadding("base32", text.length()))
}
while data_length > 0 && text.unsafe_get(data_length - 1).to_int() == 61 {
data_length -= 1
}
let padding_count = text.length() - data_length
let remainder = data_length % 8
let expected_padding = match remainder {
0 => 0
2 => 6
4 => 4
5 => 3
7 => 1
_ => return Err(InvalidPadding("base32", data_length))
}
if padding_count != expected_padding {
return Err(InvalidPadding("base32", data_length))
}
} else {
if text.length() > 0 && text.unsafe_get(text.length() - 1).to_int() == 61 {
return Err(InvalidPadding("base32", text.length() - 1))
}
match text.length() % 8 {
0 | 2 | 4 | 5 | 7 => ()
_ => return Err(InvalidPadding("base32", text.length()))
}
}
let out = @buffer.Buffer(size_hint=data_length * 5 / 8)
let mut accumulator = 0
let mut bits = 0
for index in 0..= 8 {
bits -= 8
out.write_byte(((accumulator >> bits) & 0xFF).to_byte())
}
accumulator = if bits == 0 { 0 } else { accumulator & ((1 << bits) - 1) }
}
if bits > 0 && accumulator != 0 {
return Err(InvalidPadding("base32", data_length))
}
Ok(out.to_bytes())
}
///|
///|
fn base58_digit_value(char_code : Int) -> Int {
for index, byte in BASE58_BTC {
if byte.to_int() == char_code {
return index
}
}
-1
}
///|
fn encode_base58(data : BytesView) -> String {
let out = StringBuilder()
let mut leading_zeroes = 0
while leading_zeroes < data.length() && data[leading_zeroes].to_int() == 0 {
leading_zeroes += 1
out.write_char('1')
}
if leading_zeroes < data.length() {
let radix = @bigint.BigInt::from_int(58)
let mut value = @bigint.BigInt::from_octets(data)
let digits : Array[Int] = []
while !value.is_zero() {
let quotient = value / radix
let remainder = value % radix
digits.push(remainder.to_int())
value = quotient
}
for index in digits.length()>..0 {
out.write_char(BASE58_BTC[digits[index]].to_char())
}
}
out.to_string()
}
///|
fn decode_base58(text : StringView) -> Result[Bytes, MoonLoomError] {
if text.length() == 0 {
return Ok(b"")
}
let radix = @bigint.BigInt::from_int(58)
let mut value = @bigint.BigInt::from_int(0)
let mut leading_zeroes = 0
let mut saw_non_zero = false
for index, code in text.code_units() {
let digit = base58_digit_value(code.to_int())
if digit < 0 {
return Err(InvalidBaseCharacter('z', index))
}
if !saw_non_zero && digit == 0 {
leading_zeroes += 1
} else {
saw_non_zero = true
value = value * radix + @bigint.BigInt::from_int(digit)
}
}
if !saw_non_zero {
return Ok(Bytes::makei(leading_zeroes, _ => b'\x00'))
}
let octets = value.to_octets()
let out = @buffer.Buffer(size_hint=leading_zeroes + octets.length())
for _ in 0.. String {
let out = StringBuilder(size_hint=text.length())
for code in text.code_units() {
let value = code.to_int()
if url {
if value == 43 {
out.write_char('-')
} else if value == 47 {
out.write_char('_')
} else {
out.write_char(value.unsafe_to_char())
}
} else if value == 45 {
out.write_char('+')
} else if value == 95 {
out.write_char('/')
} else {
out.write_char(value.unsafe_to_char())
}
}
out.to_string()
}
///|
fn base64_from_url(text : StringView) -> String {
let out = StringBuilder(size_hint=text.length())
for code in text.code_units() {
let value = code.to_int()
if value == 45 {
out.write_char('+')
} else if value == 95 {
out.write_char('/')
} else {
out.write_char(value.unsafe_to_char())
}
}
out.to_string()
}
///|
fn decode_base64(
text : StringView,
prefix : Char,
url : Bool,
padding : Bool,
) -> Result[Bytes, MoonLoomError] {
if padding {
if text.length() % 4 != 0 {
return Err(InvalidPadding("base64", text.length()))
}
let remainder = text.length() % 4
if text.length() > 0 {
let has_padding = text.unsafe_get(text.length() - 1).to_int() == 61
if (remainder == 2 || remainder == 3) && !has_padding {
return Err(InvalidPadding("base64", text.length()))
}
}
} else {
if text.length() % 4 == 1 {
return Err(InvalidPadding("base64", text.length()))
}
if !text.is_empty() && text.unsafe_get(text.length() - 1).to_int() == 61 {
return Err(InvalidPadding("base64", text.length() - 1))
}
}
let normalized = if url { base64_from_url(text) } else { text.to_owned() }
let bytes = @base64.decode(normalized, ignore_whitespace=false) catch {
_ => return Err(InvalidBaseCharacter(prefix, 0))
}
Ok(bytes)
}
///|
pub fn multibase_encode(
base : Base,
data : BytesView,
) -> Result[String, MoonLoomError] {
let encoded = match base {
Base16 => encode_hex(data, false)
Base16Upper => encode_hex(data, true)
Base32 => encode_base32(data, BASE32_LOWER, false)
Base32Upper => encode_base32(data, BASE32_UPPER, false)
Base32Pad => encode_base32(data, BASE32_LOWER, true)
Base32PadUpper => encode_base32(data, BASE32_UPPER, true)
Base58Btc => encode_base58(data)
Base64 => @base64.encode(data, padding=false)
Base64Pad => @base64.encode(data, padding=true)
Base64Url => transform_base64(@base64.encode(data, padding=false), true)
Base64UrlPad => transform_base64(@base64.encode(data, padding=true), true)
}
let result = StringBuilder(size_hint=encoded.length() + 1)
result.write_char(base.prefix())
result.write_string(encoded)
Ok(result.to_string())
}
///|
pub fn multibase_decode(
text : StringView,
limits : Limits,
) -> Result[DecodedMultibase, MoonLoomError] {
match limits.check_input("multibase", text.length()) {
Ok(_) => ()
Err(err) => return Err(err)
}
if text.length() == 0 {
return Err(UnknownBase('\u{0}'))
}
let prefix = match text.code_units()[0].to_char() {
Some(value) => value
None => return Err(UnknownBase('\u{0}'))
}
let base = match Base::from_prefix(prefix) {
Some(base) => base
None => return Err(UnknownBase(prefix))
}
let payload = text.exact_view(start=1)
let data = match base {
Base16 => decode_hex(payload, false)
Base16Upper => decode_hex(payload, true)
Base32 => decode_base32(payload, 'b', false, false)
Base32Upper => decode_base32(payload, 'B', true, false)
Base32Pad => decode_base32(payload, 'c', false, true)
Base32PadUpper => decode_base32(payload, 'C', true, true)
Base58Btc => decode_base58(payload)
Base64 => decode_base64(payload, 'm', false, false)
Base64Pad => decode_base64(payload, 'M', false, true)
Base64Url => decode_base64(payload, 'u', true, false)
Base64UrlPad => decode_base64(payload, 'U', true, true)
}
match data {
Ok(bytes) => Ok({ base, data: bytes, })
Err(err) => Err(err)
}
}
///|
pub fn Base::name(self : Base) -> String {
match self {
Base16 => "base16"
Base16Upper => "base16upper"
Base32 => "base32"
Base32Upper => "base32upper"
Base32Pad => "base32pad"
Base32PadUpper => "base32padupper"
Base58Btc => "base58btc"
Base64 => "base64"
Base64Pad => "base64pad"
Base64Url => "base64url"
Base64UrlPad => "base64urlpad"
}
}
///|
pub fn Base::from_name(name : StringView) -> Base? {
match name.to_owned() {
"base16" => Some(Base16)
"base16upper" => Some(Base16Upper)
"base32" => Some(Base32)
"base32upper" => Some(Base32Upper)
"base32pad" => Some(Base32Pad)
"base32padupper" => Some(Base32PadUpper)
"base58btc" => Some(Base58Btc)
"base64" => Some(Base64)
"base64pad" => Some(Base64Pad)
"base64url" => Some(Base64Url)
"base64urlpad" => Some(Base64UrlPad)
_ => None
}
}
///|
pub fn supported_bases() -> Array[Base] {
[
Base16,
Base16Upper,
Base32,
Base32Upper,
Base32Pad,
Base32PadUpper,
Base58Btc,
Base64,
Base64Pad,
Base64Url,
Base64UrlPad,
]
}
///|
pub extend Base with Eq::{not_equal, equal}
///|
pub extend Base with @debug.Debug::{to_repr}
///|
pub extend DecodedMultibase with Eq::{not_equal, equal}
///|
pub extend DecodedMultibase with @debug.Debug::{to_repr}