Skip to content

Commit 9d02dfd

Browse files
authored
Merge pull request #37 from boorad/feat/expose-url-flag-for-encoding
Expose url flag for encoding, removeLinebreaks for decoding
2 parents 40fc1d0 + f8edf9b commit 9d02dfd

11 files changed

+188
-35
lines changed

README.md

+15-11
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,34 @@ const decoded = atob(base64)
2828

2929
Compatible with [base64-js](https://github.com/beatgammit/base64-js).
3030

31-
### `byteLength(b64: string): number`
31+
#### `byteLength(b64: string): number`
3232

33-
Takes a base64 string and returns length of byte array
33+
Takes a base64 string and returns length of byte array.
3434

35-
### `toByteArray(b64: string): Uint8Array`
35+
#### `toByteArray(b64: string, removeLinebreaks: boolean = false): Uint8Array`
3636

37-
Takes a base64 string and returns a byte array
37+
Takes a base64 string and returns a byte array. Optional `removeLinebreaks` removes all `\n` characters.
3838

39-
### `fromByteArray(uint8: Uint8Array): string`
39+
#### `fromByteArray(uint8: Uint8Array, urlSafe: boolean = false): string`
4040

41-
Takes a byte array and returns a base64 string
41+
Takes a byte array and returns a base64 string. Optional `urlSafe` flag `true` allows for use in URLs.
4242

43-
### `btoa(data: string): string`
43+
#### `btoa(data: string): string`
4444

45-
Encodes a string in base64
45+
Encodes a string in base64.
4646

47-
### `atob(b64: string): string`
47+
#### `atob(b64: string): string`
4848

49-
Decodes a base64 encoded string
49+
Decodes a base64 encoded string.
5050

51-
### `shim()`
51+
#### `shim()`
5252

5353
Adds `btoa` and `atob` functions to `global`.
5454

55+
#### `trimBase64Padding = (str: string): string`
56+
57+
Trims the `=` padding character(s) off of the end of a base64 encoded string. Also, for base64url encoded strings, it will trim off the trailing `.` character(s).
58+
5559
## Contributing
5660

5761
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

cpp/base64.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ inline RetString encode_mime(String s) {
160160

161161
template <typename RetString, typename String>
162162
inline RetString encode(String s, bool url) {
163-
return base64_encode<RetString>(reinterpret_cast<const unsigned char*>(s.data()), s.size(), url);
163+
return base64_encode<RetString>(reinterpret_cast<const unsigned char*>(s.data()), s.size(), url);
164164
}
165165

166166
} // namespace detail

cpp/react-native-quick-base64.cpp

+28-11
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,18 @@ void installBase64(jsi::Runtime& jsiRuntime) {
3838
if(!valueToString(runtime, arguments[0], &str)) {
3939
return jsi::Value(-1);
4040
}
41-
std::string strBase64 = base64_encode(str);
42-
43-
return jsi::Value(jsi::String::createFromUtf8(runtime, strBase64));
41+
bool url = false;
42+
if (arguments[1].isBool()) {
43+
url = arguments[1].asBool();
44+
}
45+
try {
46+
std::string strBase64 = base64_encode(str, url);
47+
return jsi::Value(jsi::String::createFromUtf8(runtime, strBase64));
48+
} catch (const std::runtime_error& error) {
49+
throw jsi::JSError(runtime, error.what());
50+
} catch (...) {
51+
throw jsi::JSError(runtime, "unknown encoding error");
52+
}
4453
}
4554
);
4655
jsiRuntime.global().setProperty(jsiRuntime, "base64FromArrayBuffer", std::move(base64FromArrayBuffer));
@@ -55,14 +64,22 @@ void installBase64(jsi::Runtime& jsiRuntime) {
5564
}
5665

5766
std::string strBase64 = arguments[0].getString(runtime).utf8(runtime);
58-
std::string str = base64_decode(strBase64);
59-
60-
jsi::Function arrayBufferCtor = runtime.global().getPropertyAsFunction(runtime, "ArrayBuffer");
61-
jsi::Object o = arrayBufferCtor.callAsConstructor(runtime, (int)str.length()).getObject(runtime);
62-
jsi::ArrayBuffer buf = o.getArrayBuffer(runtime);
63-
memcpy(buf.data(runtime), str.c_str(), str.size());
64-
65-
return o;
67+
bool removeLinebreaks = false;
68+
if (arguments[1].isBool()) {
69+
removeLinebreaks = arguments[1].asBool();
70+
}
71+
try {
72+
std::string str = base64_decode(strBase64, removeLinebreaks);
73+
jsi::Function arrayBufferCtor = runtime.global().getPropertyAsFunction(runtime, "ArrayBuffer");
74+
jsi::Object o = arrayBufferCtor.callAsConstructor(runtime, (int)str.length()).getObject(runtime);
75+
jsi::ArrayBuffer buf = o.getArrayBuffer(runtime);
76+
memcpy(buf.data(runtime), str.c_str(), str.size());
77+
return o;
78+
} catch (const std::runtime_error& error) {
79+
throw jsi::JSError(runtime, error.what());
80+
} catch (...) {
81+
throw jsi::JSError(runtime, "unknown decoding error");
82+
}
6683
}
6784
);
6885
jsiRuntime.global().setProperty(jsiRuntime, "base64ToArrayBuffer", std::move(base64ToArrayBuffer));

example/ios/Podfile.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ PODS:
886886
- React-Mapbuffer (0.73.6):
887887
- glog
888888
- React-debug
889-
- react-native-quick-base64 (2.1.0):
889+
- react-native-quick-base64 (2.1.1):
890890
- glog
891891
- RCT-Folly (= 2022.05.16.00)
892892
- React-Core
@@ -1251,7 +1251,7 @@ SPEC CHECKSUMS:
12511251
React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066
12521252
React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec
12531253
React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab
1254-
react-native-quick-base64: cff21e7f1a145a63da9d71638fa1b592f08b4ef1
1254+
react-native-quick-base64: d35b481623c0004a82e4f15991a82f411761d95e
12551255
React-nativeconfig: b4d4e9901d4cabb57be63053fd2aa6086eb3c85f
12561256
React-NativeModulesApple: cd26e56d56350e123da0c1e3e4c76cb58a05e1ee
12571257
React-perflogger: 5f49905de275bac07ac7ea7f575a70611fa988f2

example/src/MochaRNAdapter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ let only = false;
77

88
export const resetRootSuite = (): void => {
99
rootSuite = new Mocha.Suite('') as MochaTypes.Suite;
10-
rootSuite.timeout(15 * 1000);
10+
rootSuite.timeout(30 * 1000); // big-data test can be time-consuming :|
1111
mochaContext = rootSuite;
1212
};
1313

example/src/tests/linebreaks.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {expect} from 'chai';
2+
import {toByteArray} from 'react-native-quick-base64';
3+
import {describe, it} from '../MochaRNAdapter';
4+
import {mapArr} from './util';
5+
6+
describe('linebreaks', () => {
7+
// encoded `one\ntwo\nthree\nfour` in base64 online tool
8+
const str = 'b25lCnR3bw==\ndGhyZWUKZm91cg==';
9+
10+
it('with linebreaks, leave them', () => {
11+
expect(() => toByteArray(str)).to.throw(
12+
/Input is not valid base64-encoded data/,
13+
);
14+
});
15+
16+
it('with linebreaks, remove them', () => {
17+
const arr = toByteArray(str, true);
18+
const actual = mapArr(arr, (byte: number) => String.fromCharCode(byte));
19+
const expected = 'one\ntwothree\nfour';
20+
expect(actual).to.equal(expected);
21+
});
22+
});

example/src/tests/url-safe.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {expect} from 'chai';
2-
import {byteLength, toByteArray} from 'react-native-quick-base64';
2+
import {
3+
byteLength,
4+
fromByteArray,
5+
toByteArray,
6+
trimBase64Padding,
7+
} from 'react-native-quick-base64';
38
import {describe, it} from '../MochaRNAdapter';
49

510
// from base64-js library's test suite
@@ -23,4 +28,34 @@ describe('url-safe', () => {
2328

2429
expect(actual.length).to.equal(byteLength(str));
2530
});
31+
32+
// test vector string comes from
33+
// https://gist.github.com/pedrouid/b4056fd1f754918ddae86b32cf7d803e#aes-gcm---importkey
34+
it('encode/decode base64url string w padding', async () => {
35+
const expected = 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE';
36+
const ba = toByteArray(expected);
37+
const actual = fromByteArray(ba, true);
38+
expect(trimBase64Padding(actual)).to.equal(
39+
expected,
40+
'base64 encode (url=true, trimmed)',
41+
);
42+
expect(actual).to.equal(
43+
expected + '.',
44+
'base64 encode (url=true, not trimmed)',
45+
);
46+
});
47+
48+
it('encode/decode base64 string w padding', async () => {
49+
const expected = 'Y0zt37HgOx+BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE';
50+
const ba = toByteArray(expected);
51+
const actual = fromByteArray(ba, false);
52+
expect(trimBase64Padding(actual)).to.equal(
53+
expected,
54+
'base64 encode (url=false, trimmed)',
55+
);
56+
expect(actual).to.equal(
57+
expected + '=',
58+
'base64 encode (url=false, not trimmed)',
59+
);
60+
});
2661
});

example/src/tests/zero.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {expect} from 'chai';
2+
import {
3+
byteLength,
4+
fromByteArray,
5+
toByteArray,
6+
} from 'react-native-quick-base64';
7+
import {describe, it} from '../MochaRNAdapter';
8+
import {mapArr, mapStr} from './util';
9+
10+
const checks: string[] = [
11+
'no zero',
12+
'contains a \0zero somewhere',
13+
'\0starts with a zero',
14+
'ends with a zero\0',
15+
];
16+
17+
describe('zero (\\0)', () => {
18+
for (let i = 0; i < checks.length; i++) {
19+
const check = checks[i] as string;
20+
it(`convert to base64 and back: '${check}'`, async () => {
21+
const b64Str = fromByteArray(
22+
mapStr(check, (char: string) => char.charCodeAt(0)),
23+
);
24+
25+
const arr = toByteArray(b64Str);
26+
const str = mapArr(arr, (byte: number) => String.fromCharCode(byte));
27+
expect(str).to.equal(check);
28+
expect(byteLength(b64Str)).to.equal(arr.length);
29+
});
30+
}
31+
32+
const test = (data: Uint8Array, expected: string, descr: string) => {
33+
it(`known zero values: ${descr}`, async () => {
34+
const actual = fromByteArray(data);
35+
expect(actual).to.equal(expected);
36+
});
37+
};
38+
39+
// zero
40+
const zero = new Uint8Array([122, 101, 114, 111]);
41+
test(zero, 'emVybw==', 'zero');
42+
43+
// zer\0
44+
const zer0 = new Uint8Array([122, 101, 114, 0]);
45+
test(zer0, 'emVyAA==', 'zer\\0');
46+
47+
// \0er0
48+
const ero = new Uint8Array([0, 101, 114, 111]);
49+
test(ero, 'AGVybw==', '\\0ero');
50+
51+
// zer\0_value
52+
const zer0_value = new Uint8Array([
53+
122, 101, 114, 0, 95, 118, 97, 108, 117, 101,
54+
]);
55+
test(zer0_value, 'emVyAF92YWx1ZQ==', 'zer\\0_value');
56+
});

example/src/useTestList.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import './tests/basics';
88
import './tests/convert';
99
import './tests/corrupt';
1010
import './tests/url-safe';
11+
import './tests/linebreaks';
12+
import './tests/zero';
1113
import './tests/big-data';
1214

1315
export const useTestList = (): Suites => {

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-quick-base64",
3-
"version": "2.1.0",
3+
"version": "2.1.1",
44
"description": "A native implementation of base64 in C++ for React Native",
55
"main": "lib/commonjs/index",
66
"module": "lib/module/index",

src/index.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ if (Base64Module && typeof Base64Module.install === 'function') {
77
Base64Module.install()
88
}
99

10-
type FuncBase64ToArrayBuffer = (data: string) => ArrayBuffer
11-
type FuncBase64FromArrayBuffer = (data: string | ArrayBuffer) => string
10+
type FuncBase64ToArrayBuffer = (
11+
data: string,
12+
removeLinebreaks?: boolean
13+
) => ArrayBuffer
14+
type FuncBase64FromArrayBuffer = (
15+
data: string | ArrayBuffer,
16+
urlSafe?: boolean
17+
) => string
1218

1319
declare var base64ToArrayBuffer: FuncBase64ToArrayBuffer | undefined
1420
declare const base64FromArrayBuffer: FuncBase64FromArrayBuffer | undefined
@@ -56,25 +62,32 @@ export function byteLength(b64: string): number {
5662
return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen
5763
}
5864

59-
export function toByteArray(b64: string): Uint8Array {
65+
export function toByteArray(
66+
b64: string,
67+
removeLinebreaks: boolean = false
68+
): Uint8Array {
6069
if (typeof base64ToArrayBuffer !== 'undefined') {
61-
return new Uint8Array(base64ToArrayBuffer(b64))
70+
return new Uint8Array(base64ToArrayBuffer(b64, removeLinebreaks))
6271
} else {
6372
return fallback.toByteArray(b64)
6473
}
6574
}
6675

67-
export function fromByteArray(uint8: Uint8Array): string {
76+
export function fromByteArray(
77+
uint8: Uint8Array,
78+
urlSafe: boolean = false
79+
): string {
6880
if (typeof base64FromArrayBuffer !== 'undefined') {
6981
if (uint8.buffer.byteLength > uint8.byteLength || uint8.byteOffset > 0) {
7082
return base64FromArrayBuffer(
7183
uint8.buffer.slice(
7284
uint8.byteOffset,
7385
uint8.byteOffset + uint8.byteLength
74-
)
86+
),
87+
urlSafe
7588
)
7689
}
77-
return base64FromArrayBuffer(uint8.buffer)
90+
return base64FromArrayBuffer(uint8.buffer, urlSafe)
7891
} else {
7992
return fallback.fromByteArray(uint8)
8093
}
@@ -103,3 +116,7 @@ export const getNative = () => ({
103116
base64FromArrayBuffer,
104117
base64ToArrayBuffer,
105118
})
119+
120+
export const trimBase64Padding = (str: string): string => {
121+
return str.replace(/[.=]{1,2}$/, '')
122+
}

0 commit comments

Comments
 (0)