/* codec.js —— 编码/加解密纯前端算法库 * 提供:UTF-8、Base64、Base32、URL、Unicode、SHA 系列(Web Crypto 原生) * MD5 / SHA-224 依赖 CryptoJS(优先本地 assets/js/crypto-js.min.js,缺失则回退 CDN) */ (function (global) { 'use strict'; /* ---------- UTF-8 工具 ---------- */ function utf8Encode(str) { if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str); // 降级实现 var out = [], ch, i, code; for (i = 0; i < str.length; i++) { code = str.charCodeAt(i); if (code < 0x80) out.push(code); else if (code < 0x800) { out.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); } else if (code < 0xd800 || code >= 0xe000) { out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); } else { // surrogate pair ch = (code - 0xd800) * 0x400 + (str.charCodeAt(++i) - 0xdc00) + 0x10000; out.push(0xf0 | (ch >> 18), 0x80 | ((ch >> 12) & 0x3f), 0x80 | ((ch >> 6) & 0x3f), 0x80 | (ch & 0x3f)); } } return new Uint8Array(out); } function utf8Decode(bytes) { if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes); var out = '', i = 0, b1, b2, b3, b4, code; while (i < bytes.length) { b1 = bytes[i++]; if (b1 < 0x80) { out += String.fromCharCode(b1); } else if (b1 < 0xe0) { b2 = bytes[i++]; out += String.fromCharCode(((b1 & 0x1f) << 6) | (b2 & 0x3f)); } else if (b1 < 0xf0) { b2 = bytes[i++]; b3 = bytes[i++]; out += String.fromCharCode(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)); } else { b2 = bytes[i++]; b3 = bytes[i++]; b4 = bytes[i++]; code = ((b1 & 0x07) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f); code -= 0x10000; out += String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff)); } } return out; } function bytesToBin(bytes) { var bin = '', i; for (i = 0; i < bytes.length; i++) { bin += String.fromCharCode(bytes[i]); } return bin; } function binToBytes(bin) { var out = new Uint8Array(bin.length), i; for (i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } /* ---------- Base64(UTF-8 安全) ---------- */ function b64Encode(str) { var bytes = utf8Encode(str); // 浏览器环境用 btoa if (typeof btoa !== 'undefined') { var bin = ''; for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); return btoa(bin); } return global.Base64 && global.Base64.encode(bytesToBin(bytes)) ? global.Base64.encode(bytesToBin(bytes)) : ''; } function b64Decode(b64) { b64 = String(b64 || '').replace(/\s+/g, ''); var bin; if (typeof atob !== 'undefined') { bin = atob(b64); } else if (global.Base64) { bin = global.Base64.decode(b64); } else { throw new Error('当前环境不支持 Base64 解码'); } return utf8Decode(binToBytes(bin)); } /* ---------- Base32(RFC 4648,UTF-8 安全) ---------- */ var B32_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; function base32Encode(str) { var bytes = utf8Encode(str), out = '', bits = 0, value = 0, i; for (i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; bits += 8; while (bits >= 5) { out += B32_ALPHA[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) out += B32_ALPHA[(value << (5 - bits)) & 31]; while (out.length % 8 !== 0) out += '='; return out; } function base32Decode(b32) { b32 = String(b32 || '').toUpperCase().replace(/[=\s]/g, ''); if (!b32) return ''; var bits = 0, value = 0, out = [], i, idx, b; for (i = 0; i < b32.length; i++) { idx = B32_ALPHA.indexOf(b32[i]); if (idx === -1) throw new Error('Base32 字符串包含非法字符: ' + b32[i]); value = (value << 5) | idx; bits += 5; if (bits >= 8) { b = (value >>> (bits - 8)) & 0xff; out.push(b); bits -= 8; } } return utf8Decode(new Uint8Array(out)); } /* ---------- URL 编解码 ---------- */ function urlEncode(str) { return encodeURIComponent(String(str)); } function urlDecode(str) { return decodeURIComponent(String(str).replace(/\+/g, ' ')); } /* ---------- Unicode 编解码(支持中文、emoji) ---------- */ function unicodeEncode(str) { str = String(str); var out = [], i, code; for (i = 0; i < str.length; i++) { code = str.charCodeAt(i); if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) { // 代理对:按两个 \uXXXX 输出,JS 自动组合 out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0')); out.push('\\u' + str.charCodeAt(++i).toString(16).toUpperCase().padStart(4, '0')); } else { out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0')); } } return out.join(''); } function unicodeDecode(str) { str = String(str); // 兼容 \u{1F600} 与 \uXXXX 两种写法 str = str.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, function (m, h) { var cp = parseInt(h, 16); if (cp > 0xffff) { cp -= 0x10000; return String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)); } return String.fromCharCode(cp); }); str = str.replace(/\\u([0-9a-fA-F]{4})/g, function (m, h) { return String.fromCharCode(parseInt(h, 16)); }); return str; } /* ---------- SHA 系列(Web Crypto 原生) ---------- */ var HASHES = { 'SHA-1': 'SHA-1', 'SHA-256': 'SHA-256', 'SHA-384': 'SHA-384', 'SHA-512': 'SHA-512' }; function hexFromBuffer(buf) { var bytes = new Uint8Array(buf), out = '', i; for (i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, '0'); return out; } function shaDigest(algorithm, text) { var bytes = utf8Encode(text); if (typeof crypto !== 'undefined' && crypto.subtle) { return crypto.subtle.digest(algorithm, bytes).then(hexFromBuffer); } // 非安全上下文(如普通 http)无 Web Crypto:回退 crypto-js return cryptoJsDigest(algorithm, text); } /* ---------- CryptoJS 懒加载(MD5 / SHA-224 等需要;优先本地,其次 CDN) ---------- */ var CDN_CRYPTO_JS = [ '../assets/js/crypto-js.min.js', // 本地内置(离线可用) 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js', // 回退 1 'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js' // 回退 2 ]; var cryptoJSPromise = null; function loadCryptoJS() { if (global.CryptoJS) return Promise.resolve(global.CryptoJS); if (cryptoJSPromise) return cryptoJSPromise; cryptoJSPromise = new Promise(function (resolve) { var load = function (idx) { if (global.CryptoJS) { resolve(global.CryptoJS); return; } if (idx >= CDN_CRYPTO_JS.length) { resolve(null); return; } var s = document.createElement('script'); s.src = CDN_CRYPTO_JS[idx]; s.onload = function () { resolve(global.CryptoJS || null); }; s.onerror = function () { load(idx + 1); }; document.head.appendChild(s); }; load(0); }); return cryptoJSPromise; } var CRYPTO_JS_METHODS = { 'MD5': 'MD5', 'SHA-1': 'SHA1', 'SHA-224': 'SHA224', 'SHA-256': 'SHA256', 'SHA-384': 'SHA384', 'SHA-512': 'SHA512' }; function cryptoJsDigest(algo, text) { return loadCryptoJS().then(function (C) { if (!C) throw new Error('计算 ' + algo + ' 的加密组件加载失败:请将 crypto-js.min.js 放入 assets/js/ 目录离线使用,或检查网络'); var method = CRYPTO_JS_METHODS[algo]; if (!method || typeof C[method] !== 'function') throw new Error('不支持的算法: ' + algo); return C[method](text).toString(C.enc.Hex); }); } function hashText(algo, text) { if (HASHES[algo]) return shaDigest(HASHES[algo], text); if (algo === 'MD5' || algo === 'SHA-224') return cryptoJsDigest(algo, text); return Promise.reject(new Error('未知算法: ' + algo)); } /* ---------- 导出 ---------- */ global.CodecLib = { utf8Encode: utf8Encode, utf8Decode: utf8Decode, b64Encode: b64Encode, b64Decode: b64Decode, base32Encode: base32Encode, base32Decode: base32Decode, urlEncode: urlEncode, urlDecode: urlDecode, unicodeEncode: unicodeEncode, unicodeDecode: unicodeDecode, hashText: hashText, hashes: ['MD5', 'SHA-1', 'SHA-224', 'SHA-256', 'SHA-384', 'SHA-512'] }; })(window);