/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
/**
* @file
*
* This file defines some base64 convert methods.
*
*/
package stdx.encoding.base64
const SIXTEEN_BIT = 16
const EIGHT_BIT = 8
let BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toArray()
let ArrayBASE64: Array<Int64> = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1]
/*
* Converting Base64 arrays into UInt8 sets (Base64ToString decoding).
*
* @Param data of Array<UInt8>.
* @return Parameters of Option<Array<UInt8>>.
*
* @since 0.17.4
*/
func fromBase64(data: Array<UInt8>): Option<Array<UInt8>> {
if (!verifyBase64(data)) {
if (data.size == 0) {
return Option<Array<UInt8>>.Some(Array<UInt8>())
}
return Option<Array<UInt8>>.None
}
var padding: Int64 = 0
if (data[data.size - 1] == b'=') {
padding++
}
if (data[data.size - 2] == b'=') {
padding++
}
var output: Array<UInt8> = Array<UInt8>((data.size / 4 * 3) - padding, repeat: 0)
var i: Int64 = 0
var index: Int64 = 0
while (i < data.size) {
var chr1: UInt8 = 0
var chr2: UInt8 = 0
var chr3: UInt8 = 0
/*
* Get values for each group of four base 64 characters
* If data[i] is not in the range, an error will be reported during transformation
*/
var num: Int64 = Int64(UInt32(data[i]))
var c1: Int64 = ArrayBASE64[num]
var enc1: UInt8 = UInt8(c1)
i++
var num2: Int64 = Int64(UInt32(data[i]))
var c2: Int64 = ArrayBASE64[num2]
var enc2: UInt8 = UInt8(c2)
i++
var num3: Int64 = Int64(UInt32(data[i]))
var c3: Int64 = ArrayBASE64[num3]
var enc3: UInt8 = UInt8(c3)
i++
var num4: Int64 = Int64(UInt32(data[i]))
var c4: Int64 = ArrayBASE64[num4]
var enc4: UInt8 = UInt8(c4)
i++
/* Take the first 2 bits of enc1 + enc2 to form 8 bits or 1 byte */
chr1 = UInt8((enc1 << 2) | (enc2 >> 4))
/* Take the last 4 bits of enc2 + the first 4 bits of enc3 to form 8 bits or 1 byte */
chr2 = UInt8(((enc2 & 15) << 4) | (enc3 >> 2))
/* Take the first 2 bits of enc3 + enc4 to form 8 bits or 1 byte */
chr3 = UInt8(((enc3 & 3) << 6) | enc4)
/* Add the byte to the return value if it isn't part of an r'=' character (indicated by 64) */
if (enc2 != 64) {
output[index] = chr1
index++
}
if (enc3 != 64) {
output[index] = chr2
index++
}
if (enc4 != UInt8(64)) {
output[index] = chr3
index++
}
}
return Option<Array<UInt8>>.Some(output)
}
func verifyBase64(value: Array<UInt8>): Bool {
if (value.size == 0 || value.size % 4 != 0) {
return false
}
/* 98% of all non base64 values are invalidated by this time. */
var index: Int64 = value.size - 1
/* if there is padding step back */
if (value[index] == b'=') {
index--
}
/* if there are two padding chars step back a second time */
if (value[index] == b'=') {
index--
}
/* Back-to-front traversal can reduce boundary checks and improve performance */
for (i in index..=0 : -1) {
/* If any of the character is not from the allowed list */
var num: Int64 = Int64(UInt32(value[i]))
if (num < 0 || num > 123) {
return false
}
if (num == 61) {
return false
}
var c1: Int64 = ArrayBASE64[num]
if (c1 == Int64(-1)) {
return false
}
}
let last = value.size - 1
if (value[last] == b'=' && value[last - 1] == b'=') {
let second = ArrayBASE64[Int64(UInt32(value[last - 2]))] // End with "=="
if ((second & 0x0F) != 0) { // 0b00001111: If lower 4 bits of the 2st character are not 0, then it's not valid
return false
}
} else if (value[last] == b'=') {
let third = ArrayBASE64[Int64(UInt32(value[last - 1]))] // End with "="
if ((third & 0x03) != 0) { // 0b00000011: If lower 2 bits of the 3rd character are not 0, then it's not valid
return false
}
}
return true
}
// cjlint-ignore -start !G.OTH.02
/*
* Convert the UInt8 set to a character string (Base64ToString encoding).
* In step 1, the ASCII values of "M", "a", and "n" are 77, 97, and 110 respectively
* and the corresponding binary values are 01001101, 011000001,
* and 01101110. Concatenate them into a 24-bit binary string 01001011010110000101110.
* Step 2: Divide the 24-bit binary string into four groups with six binary bits in each group: 010011, 010110, 000101, and 101110.
* Step 3: Add two 00s before each group to expand the group into 32 binary bits,
* that is, four bytes: 00010011, 00010110, 00000101, and 00101110. Their decimal values are 19, 22, 5, and 46.
* Step 4: According to the preceding table, obtain the Base64 codes corresponding to each value, that is, T, W, F, and u.
*
* @Param data of Array<UInt8>
* @return Parameters of String
*
* @since 0.17.4
*/
// cjlint-ignore -end
func toBase64(data: Array<UInt8>): String {
if (data.size <= 0) {
return ""
}
var lengthDataBits: Int64 = data.size * 8
/* Whether the length of the encrypted string exceeds 24 */
var fewerThan24bits: Int64 = lengthDataBits % 24
var numberTriplets: Int64 = lengthDataBits / 24
/* Calculate the total number of characters after string encryption */
var number: Int64 = numberTriplets
if (fewerThan24bits != 0) {
number = numberTriplets + 1
}
/* Used to save the results */
var toBase64Text: Array<Byte> = Array<Byte>(number * 4, repeat: 0)
var index: Int64 = 0
var order: Int64 = 0
for (_ in 0..numberTriplets) {
var s1: UInt8 = data[index]
index++
var s2: UInt8 = data[index]
index++
var s3: UInt8 = data[index]
index++
/* The first 6 digits */
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s1 & 0xFC) >> 2)]
order++
/* The second 6 digits */
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s1 & 0x03) << 4) + ((s2 & 0xF0) >> 4))]
order++
/* The Third 6 digits */
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s2 & 0x0F) << 2) + ((s3 & 0xC0) >> 6))]
order++
/* The fourth 6 digits */
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(s3 & 0x3f)]
order++
}
/*
* In the case of one byte: Add two 0s to the last group of the 8 binary bits of this byte,
* and add 4 0s to the back. In this way, a two-bit Base64 encoding is obtained
* Add two "=" signs at the end
*/
if (fewerThan24bits == EIGHT_BIT) {
var last: UInt8 = data[index]
index++
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((last & 0xFC) >> 2)]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((last & 0x03) << 4)]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[64]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[64]
order++
}
/*
* In the case of two bytes: convert a total of 16 binary bits of these two bytes into three groups.
* In addition to the two 0s in the front, the last group also needs to be added with two 0s
* In this way, a three-digit Base64 encoding is obtained, and a "=" sign is added at the end
*/
if (fewerThan24bits == SIXTEEN_BIT) {
var s1: UInt8 = data[index]
index++
var s2: UInt8 = data[index]
index++
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s1 & 0xFC) >> 2)]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s1 & 0x03) << 4) + ((s2 & 0xF0) >> 4))]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s2 & 0x0f) << 2)]
order++
toBase64Text[order] = BASE64_ENCODE_CHARS[64]
order++
}
return unsafe { String.fromUtf8Unchecked(toBase64Text) }
}
/**
* Provides the Base64 encoding conversion function and the Base64String to ByteArray function.
* If decoding fails, Option is returned.
*
* @param data of String.
* @return Parameters of Option<Array<UInt8>>
*
* @since 0.17.4
*/
public func fromBase64String(data: String): Option<Array<Byte>> {
return fromBase64(unsafe { data.rawData() })
}
/**
* Provides the Base64 encoding conversion function and the ByteArray to Base64String function.
* If the encoding fails, Option is returned.
*
* @param data of String.
* @return Parameters of String.
*
* @since 0.17.4
*/
public func toBase64String(data: Array<Byte>): String {
return toBase64(data)
}