From fe7a5197a4c913584e833a7f3aa782ec5ad75d01 Mon Sep 17 00:00:00 2001 From: wangfandev Date: Wed, 28 Jan 2026 18:29:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20react-native-quick-base64=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E5=BA=93Base64=E6=8E=A5=E5=8F=A3=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: wangfandev --- harmony/rn_quick_base64/src/main/cpp/base64.h | 18 ++- .../main/cpp/react-native-quick-base64.cpp | 8 +- package.json | 28 ++-- react-native-quick-base64.podspec | 12 +- src/index.ts | 130 +++++++++++------- tsconfig.json | 4 +- 6 files changed, 113 insertions(+), 87 deletions(-) diff --git a/harmony/rn_quick_base64/src/main/cpp/base64.h b/harmony/rn_quick_base64/src/main/cpp/base64.h index 157e681..0b607ac 100644 --- a/harmony/rn_quick_base64/src/main/cpp/base64.h +++ b/harmony/rn_quick_base64/src/main/cpp/base64.h @@ -169,7 +169,7 @@ template inline RetString base64_encode(const unsigned char* bytes_to_encode, size_t in_len, bool url) { size_t len_encoded = (in_len + 2) / 3 * 4; - unsigned char trailing_char = url ? '.' : '='; + const unsigned char trailing_char = '='; // // Choose set of base64 characters. They differ @@ -200,12 +200,12 @@ inline RetString base64_encode(const unsigned char* bytes_to_encode, size_t in_l ret.push_back(base64_chars_[bytes_to_encode[pos + 2] & 0x3f]); } else { ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); - ret.push_back(trailing_char); + if (!url) ret.push_back(trailing_char); } } else { ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); - ret.push_back(trailing_char); - ret.push_back(trailing_char); + if (!url) ret.push_back(trailing_char); + if (!url) ret.push_back(trailing_char); } pos += 3; @@ -250,12 +250,12 @@ inline RetString decode(const String& encoded_string, bool remove_linebreaks) { RetString ret; ret.reserve(approx_length_of_decoded_string); - while (pos < length_of_string) { + while (pos < length_of_string && encoded_string.at(pos) != '=') { // // Iterate over encoded input string in chunks. The size of all // chunks except the last one is 4 bytes. // - // The last chunk might be padded with equal signs or dots + // The last chunk might be padded with equal signs // in order to make it 4 bytes in size as well, but this // is not required as per RFC 2045. // @@ -273,8 +273,7 @@ inline RetString decode(const String& encoded_string, bool remove_linebreaks) { if ((pos + 2 < length_of_string) && // Check for data that is not padded with equal signs (which is allowed by RFC 2045) - encoded_string.at(pos + 2) != '=' && - encoded_string.at(pos + 2) != '.' ) { // accept URL-safe base 64 strings, too, so check for '.' also. + encoded_string.at(pos + 2) != '=') { // // Emit a chunk's second byte (which might not be produced in the last chunk). // @@ -282,8 +281,7 @@ inline RetString decode(const String& encoded_string, bool remove_linebreaks) { ret.push_back(static_cast(((pos_of_char_1 & 0x0f) << 4) + ((pos_of_char_2 & 0x3c) >> 2))); if ((pos + 3 < length_of_string) && - encoded_string.at(pos + 3) != '=' && - encoded_string.at(pos + 3) != '.' ) { + encoded_string.at(pos + 3) != '=') { // // Emit a chunk's third byte (which might not be produced in the last chunk). // diff --git a/harmony/rn_quick_base64/src/main/cpp/react-native-quick-base64.cpp b/harmony/rn_quick_base64/src/main/cpp/react-native-quick-base64.cpp index 4bf290e..0b72505 100644 --- a/harmony/rn_quick_base64/src/main/cpp/react-native-quick-base64.cpp +++ b/harmony/rn_quick_base64/src/main/cpp/react-native-quick-base64.cpp @@ -35,11 +35,11 @@ void installBase64(jsi::Runtime& jsiRuntime) { 1, // string [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { std::string str; - if(!valueToString(runtime, arguments[0], &str)) { + if(count > 0 && !valueToString(runtime, arguments[0], &str)) { return jsi::Value(-1); } bool url = false; - if (arguments[1].isBool()) { + if (count > 1 && arguments[1].isBool()) { url = arguments[1].asBool(); } try { @@ -59,13 +59,13 @@ void installBase64(jsi::Runtime& jsiRuntime) { jsi::PropNameID::forAscii(jsiRuntime, "base64ToArrayBuffer"), 1, // string [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { - if (!arguments[0].isString()) { + if (count > 0 && !arguments[0].isString()) { return jsi::Value(-1); } std::string strBase64 = arguments[0].getString(runtime).utf8(runtime); bool removeLinebreaks = false; - if (arguments[1].isBool()) { + if (count > 1 && arguments[1].isBool()) { removeLinebreaks = arguments[1].asBool(); } try { diff --git a/package.json b/package.json index 83d5747..fa627e7 100644 --- a/package.json +++ b/package.json @@ -45,24 +45,24 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { - "@commitlint/config-conventional": "^15.0.0", + "@commitlint/config-conventional": "^19.6.0", "@react-native-community/eslint-config": "^3.0.1", "@release-it/conventional-changelog": "^3.3.0", - "@types/jest": "^26.0.0", - "@types/react": "18.2.6", + "@types/jest": "^29.5.13", + "@types/react": "^19.1.0", "commitlint": "^11.0.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^7.2.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint": "^8.19.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-prettier": "^5.2.3", "husky": "^6.0.0", - "jest": "^26.0.1", + "jest": "^29.7.0", "pod-install": "^0.1.0", - "prettier": "^2.4.1", - "react": "18.2.0", - "react-native": "0.73.6", - "react-native-builder-bob": "^0.23.2", + "prettier": "3.0.0", + "react": "19.1.1", + "react-native": "0.82.1", + "react-native-builder-bob": "^0.40.11", "release-it": "^14.2.2", - "typescript": "^4.5.2" + "typescript": "^5.8.3" }, "peerDependencies": { "react": "*", @@ -134,7 +134,5 @@ ] ] }, - "dependencies": { - "base64-js": "^1.5.1" - } + "dependencies": {} } diff --git a/react-native-quick-base64.podspec b/react-native-quick-base64.podspec index e055ef1..d8bab13 100644 --- a/react-native-quick-base64.podspec +++ b/react-native-quick-base64.podspec @@ -15,18 +15,18 @@ Pod::Spec.new do |s| s.source_files = [ "ios/**/*.{h,m,mm}", - "cpp/**/*.{h,c,cpp}", - "ios/QuickBase64Module.h" + "cpp/**/*.{h,cpp}" ] - s.pod_target_xcconfig = { - "USE_HEADERMAP" => "NO", + s.header_mappings_dir = 'ios' + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "NO" } - if defined?(install_modules_dependencies()) != nil + if respond_to?(:install_modules_dependencies) install_modules_dependencies(s) else - s.dependency "React" + s.dependency "React-Core" end end diff --git a/src/index.ts b/src/index.ts index 590a2df..f663278 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ */ import { NativeModules } from 'react-native'; -import fallback from 'base64-js'; + // @ts-ignore We want to check whether __turboModuleProxy exitst, it may not const isTurboModuleEnabled = global.nativeModuleProxy != null; @@ -31,25 +31,30 @@ const Base64Module = isTurboModuleEnabled ? require('./NativeQuickBase64').default : NativeModules.QuickBase64; -if (Base64Module && typeof Base64Module.install === 'function') { - Base64Module.install(); + declare global { + var base64ToArrayBuffer: FuncBase64ToArrayBuffer + var base64FromArrayBuffer: FuncBase64FromArrayBuffer } type FuncBase64ToArrayBuffer = ( data: string, removeLinebreaks?: boolean ) => ArrayBuffer + type FuncBase64FromArrayBuffer = ( data: string | ArrayBuffer, urlSafe?: boolean ) => string -declare var base64ToArrayBuffer: FuncBase64ToArrayBuffer | undefined -declare const base64FromArrayBuffer: FuncBase64FromArrayBuffer | undefined +if (Base64Module && typeof global.base64FromArrayBuffer !== 'function') { + Base64Module.install() +} -// from https://github.com/beatgammit/base64-js/blob/master/index.js +/** + * Calculates valid length and placeholder length for base64 string + */ function getLens(b64: string) { - let len = b64.length + const len = b64.length if (len % 4 > 0) { throw new Error('Invalid string. Length must be a multiple of 4') @@ -58,93 +63,118 @@ function getLens(b64: string) { let validLen = b64.indexOf('=') if (validLen === -1) validLen = len - let placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4) + const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4) - return [validLen, placeHoldersLen] + return [validLen, placeHoldersLen] as const } +/** + * Converts Uint8Array to string + */ function uint8ArrayToString(array: Uint8Array) { - let out = '', - i = 0, - len = array.length - while (i < len) { - const c = array[i++] as number - out += String.fromCharCode(c) + let out = '' + for (let i = 0; i < array.length; i++) { + const charCode = array[i] + if (charCode !== undefined) { + out += String.fromCharCode(charCode) + } } return out } +/** + * Converts string to ArrayBuffer + */ function stringToArrayBuffer(str: string) { const buf = new ArrayBuffer(str.length) const bufView = new Uint8Array(buf) - for (let i = 0, strLen = str.length; i < strLen; i++) { + for (let i = 0; i < str.length; i++) { bufView[i] = str.charCodeAt(i) } return buf } -export function byteLength(b64: string): number { - let lens = getLens(b64) - let validLen = lens[0] as number - let placeHoldersLen = lens[1] as number +/** + * Calculates the byte length of a base64 string + */ +export function byteLength(b64: string) { + const [validLen, placeHoldersLen] = getLens(b64) return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen } +/** + * Converts base64 string to Uint8Array + */ export function toByteArray( b64: string, removeLinebreaks: boolean = false ): Uint8Array { - if (typeof base64ToArrayBuffer !== 'undefined') { - return new Uint8Array(base64ToArrayBuffer(b64, removeLinebreaks)) - } else { - return fallback.toByteArray(b64) - } + return new Uint8Array(global.base64ToArrayBuffer(b64, removeLinebreaks)) } +/** + * Converts Uint8Array to base64 string + */ export function fromByteArray( uint8: Uint8Array, urlSafe: boolean = false ): string { - if (typeof base64FromArrayBuffer !== 'undefined') { - if (uint8.buffer.byteLength > uint8.byteLength || uint8.byteOffset > 0) { - return base64FromArrayBuffer( - uint8.buffer.slice( - uint8.byteOffset, - uint8.byteOffset + uint8.byteLength - ), - urlSafe - ) + if (uint8.buffer.byteLength > uint8.byteLength || uint8.byteOffset > 0) { + const buffer = + uint8.buffer instanceof ArrayBuffer + ? uint8.buffer.slice( + uint8.byteOffset, + uint8.byteOffset + uint8.byteLength + ) + : new ArrayBuffer(uint8.byteLength) + + if (buffer instanceof ArrayBuffer) { + return global.base64FromArrayBuffer(buffer, urlSafe) } - return base64FromArrayBuffer(uint8.buffer, urlSafe) - } else { - return fallback.fromByteArray(uint8) } + + const buffer = + uint8.buffer instanceof ArrayBuffer + ? uint8.buffer + : new ArrayBuffer(uint8.byteLength) + return global.base64FromArrayBuffer(buffer, urlSafe) } -export function btoa(data: string): string { - const ab = stringToArrayBuffer(data) - if (typeof base64FromArrayBuffer !== 'undefined') { - return base64FromArrayBuffer(ab) - } else { - return fallback.fromByteArray(new Uint8Array(ab)) - } +/** + * Base64 encode a string + * @deprecated Use native btoa() instead - now supported in Hermes + */ +export function btoa(data: string) { + return global.base64FromArrayBuffer(stringToArrayBuffer(data), false) } -export function atob(b64: string): string { - const ua = toByteArray(b64) - return uint8ArrayToString(ua) +/** + * Base64 decode a string + * @deprecated Use native atob() instead - now supported in Hermes + */ +export function atob(b64: string) { + return uint8ArrayToString(toByteArray(b64)) } +/** + * Adds btoa and atob to global scope + */ export function shim() { ;(global as any).btoa = btoa ;(global as any).atob = atob } +/** + * Returns native base64 functions + */ export const getNative = () => ({ - base64FromArrayBuffer, - base64ToArrayBuffer, + base64FromArrayBuffer: global.base64FromArrayBuffer, + base64ToArrayBuffer: global.base64ToArrayBuffer }) -export const trimBase64Padding = (str: string): string => { +/** + * Removes padding characters from base64 string + */ +export const trimBase64Padding = (str: string) => { return str.replace(/[.=]{1,2}$/, '') } diff --git a/tsconfig.json b/tsconfig.json index 484f02a..536c4eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,12 +23,12 @@ "skipLibCheck": true, "strict": true, "target": "esnext", - "outDir": "lib", + "outDir": "lib" }, "include": [ "src", ".eslintrc.js", - "babel.config.js", + "babel.config.js" ], "exclude": [ "node_modules",