b642u8arr.mjs

import encbase64 from 'crypto-js/enc-base64.js'
import get from 'lodash-es/get.js'
import isbol from './isbol.mjs'
import isstr from './isstr.mjs'


//crypto-js沒有支援chunk或stream機制, 無法處理大量資料


//本函數之驗證與解碼語意對齊RFC 4648與TC39之proposal-arraybuffer-base64(已進ES2026), 等同Uint8Array.fromBase64之預設模式(alphabet: 'base64'、lastChunkHandling: 'loose')
//nodejs 25起原生內建Uint8Array.fromBase64, 屆時本函數之驗證與解碼段可整段換為: return Uint8Array.fromBase64(b64), 並以try catch接住其SyntaxError轉為本函數之失敗回傳; 現行nodejs 24尚無此API故自行實作
//驗證規則取自規範並以core-js之規範實作對拍, 見test/b642u8arr.test.mjs之[oracle]案


//reWhitespace, 規範所定之ASCII空白: TAB(0x09)、LF(0x0A)、FF(0x0C)、CR(0x0D)、SPACE(0x20)
//RFC 4648與TC39之Uint8Array.fromBase64皆規定解碼前忽略此類空白, 折行之base64(MIME/PEM)方能正確解出
let reWhitespace = /[\t\n\f\r ]/g

//reB64, base64字母表, padding僅可出現於末端且至多2個
//RFC 4648 §3.3為MUST等級: 含字母表外之字元一律拒絕, 否則忽略非法字元會形成隱蔽通道、可繞過字串相等比較, 亦可能觸發實作缺陷
let reB64 = /^[A-Za-z0-9+/]*={0,2}$/

//rePadTail, 取末端連續padding
let rePadTail = /=+$/


/**
 * base64字串轉Uint8Array
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/b642u8arr.test.mjs Github}
 * @memberOf wsemi
 * @param {String} b64 輸入base64字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.returnWithStateAndMsg=false] 輸入是否回傳含狀態與訊息物件布林值,若為true則回傳{ state, msg }物件,state為'success'或'error',msg於success時為回傳結果、於error時為錯誤訊息字串,預設false
 * @returns {Uint8Array|Object} 回傳Uint8Array,輸入非字串或非合法base64時回傳空Uint8Array;若opt.returnWithStateAndMsg為true則回傳{ state, msg }物件
 * @example
 *
 * console.log(b642u8arr('AQItAA=='))
 * // => new Uint8Array([1, 2.3, '45', 'abc'])
 *
 */
function b642u8arr(b64, opt = {}) {

    //returnWithStateAndMsg
    let returnWithStateAndMsg = get(opt, 'returnWithStateAndMsg', null)
    if (!isbol(returnWithStateAndMsg)) {
        returnWithStateAndMsg = false
    }

    //retError
    let retError = (msg) => {
        if (returnWithStateAndMsg) {
            return {
                state: 'error',
                msg,
            }
        }
        else {
            return new Uint8Array()
        }
    }

    //check
    if (!isstr(b64)) {
        return retError('invalid b64')
    }

    //注意: Nodejs 25起原生內建Uint8Array.fromBase64, 待升版後再改用

    //b64c, 先剝除ASCII空白再驗證; 底層crypto-js對字母表外之字元查表得undefined並當成0位元, 不剝不驗則空白與非法字元皆會靜默解出錯誤位元組
    let b64c = b64.replace(reWhitespace, '')

    //check, 字母表與padding位置
    if (!reB64.test(b64c)) {
        return retError('invalid b64, contains characters outside the base64 alphabet or misplaced padding')
    }

    //check, 長度與padding數須配對; 末段採loose(即Uint8Array.fromBase64之預設, 允許末段2或3字元不補padding), 但長度模4為1之末段一律無效
    let nPad = b64c.length - b64c.replace(rePadTail, '').length
    let nBody = b64c.length - nPad
    let mod = nBody % 4
    if (nPad === 0 && mod === 1) {
        return retError(`invalid b64, the last chunk has only 1 character[${b64c.length}]`)
    }
    if (nPad === 1 && mod !== 3) {
        return retError(`invalid b64, malformed padding, 1 padding requires the last chunk to have 3 characters`)
    }
    if (nPad === 2 && mod !== 2) {
        return retError(`invalid b64, malformed padding, 2 paddings require the last chunk to have 2 characters`)
    }

    //u8a, 須攔截非預期錯誤, 否則會外拋至呼叫端
    let u8a = null
    try {

        let wa = encbase64.parse(b64c)

        //words, sigBytes
        let words = wa.words
        let sigBytes = wa.sigBytes

        //u8a
        u8a = new Uint8Array(sigBytes)
        for (let i = 0; i < sigBytes; i++) {
            let byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff
            u8a[i] = byte
        }

    }
    catch (err) {
        return retError(err.toString())
    }

    if (returnWithStateAndMsg) {
        return {
            state: 'success',
            msg: u8a,
        }
    }
    else {
        return u8a
    }
}


export default b642u8arr