Skip to content

Commit

Permalink
Merge pull request #37 from boorad/feat/expose-url-flag-for-encoding
Browse files Browse the repository at this point in the history
Expose url flag for encoding, removeLinebreaks for decoding
  • Loading branch information
craftzdog committed Apr 11, 2024
2 parents 40fc1d0 + f8edf9b commit 9d02dfd
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 35 deletions.
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,34 @@ const decoded = atob(base64)

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

### `byteLength(b64: string): number`
#### `byteLength(b64: string): number`

Takes a base64 string and returns length of byte array
Takes a base64 string and returns length of byte array.

### `toByteArray(b64: string): Uint8Array`
#### `toByteArray(b64: string, removeLinebreaks: boolean = false): Uint8Array`

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

### `fromByteArray(uint8: Uint8Array): string`
#### `fromByteArray(uint8: Uint8Array, urlSafe: boolean = false): string`

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

### `btoa(data: string): string`
#### `btoa(data: string): string`

Encodes a string in base64
Encodes a string in base64.

### `atob(b64: string): string`
#### `atob(b64: string): string`

Decodes a base64 encoded string
Decodes a base64 encoded string.

### `shim()`
#### `shim()`

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

#### `trimBase64Padding = (str: string): string`

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).

## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
Expand Down
2 changes: 1 addition & 1 deletion cpp/base64.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ inline RetString encode_mime(String s) {

template <typename RetString, typename String>
inline RetString encode(String s, bool url) {
return base64_encode<RetString>(reinterpret_cast<const unsigned char*>(s.data()), s.size(), url);
return base64_encode<RetString>(reinterpret_cast<const unsigned char*>(s.data()), s.size(), url);
}

} // namespace detail
Expand Down
39 changes: 28 additions & 11 deletions cpp/react-native-quick-base64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,18 @@ void installBase64(jsi::Runtime& jsiRuntime) {
if(!valueToString(runtime, arguments[0], &str)) {
return jsi::Value(-1);
}
std::string strBase64 = base64_encode(str);

return jsi::Value(jsi::String::createFromUtf8(runtime, strBase64));
bool url = false;
if (arguments[1].isBool()) {
url = arguments[1].asBool();
}
try {
std::string strBase64 = base64_encode(str, url);
return jsi::Value(jsi::String::createFromUtf8(runtime, strBase64));
} catch (const std::runtime_error& error) {
throw jsi::JSError(runtime, error.what());
} catch (...) {
throw jsi::JSError(runtime, "unknown encoding error");
}
}
);
jsiRuntime.global().setProperty(jsiRuntime, "base64FromArrayBuffer", std::move(base64FromArrayBuffer));
Expand All @@ -55,14 +64,22 @@ void installBase64(jsi::Runtime& jsiRuntime) {
}

std::string strBase64 = arguments[0].getString(runtime).utf8(runtime);
std::string str = base64_decode(strBase64);

jsi::Function arrayBufferCtor = runtime.global().getPropertyAsFunction(runtime, "ArrayBuffer");
jsi::Object o = arrayBufferCtor.callAsConstructor(runtime, (int)str.length()).getObject(runtime);
jsi::ArrayBuffer buf = o.getArrayBuffer(runtime);
memcpy(buf.data(runtime), str.c_str(), str.size());

return o;
bool removeLinebreaks = false;
if (arguments[1].isBool()) {
removeLinebreaks = arguments[1].asBool();
}
try {
std::string str = base64_decode(strBase64, removeLinebreaks);
jsi::Function arrayBufferCtor = runtime.global().getPropertyAsFunction(runtime, "ArrayBuffer");
jsi::Object o = arrayBufferCtor.callAsConstructor(runtime, (int)str.length()).getObject(runtime);
jsi::ArrayBuffer buf = o.getArrayBuffer(runtime);
memcpy(buf.data(runtime), str.c_str(), str.size());
return o;
} catch (const std::runtime_error& error) {
throw jsi::JSError(runtime, error.what());
} catch (...) {
throw jsi::JSError(runtime, "unknown decoding error");
}
}
);
jsiRuntime.global().setProperty(jsiRuntime, "base64ToArrayBuffer", std::move(base64ToArrayBuffer));
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ PODS:
- React-Mapbuffer (0.73.6):
- glog
- React-debug
- react-native-quick-base64 (2.1.0):
- react-native-quick-base64 (2.1.1):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1251,7 +1251,7 @@ SPEC CHECKSUMS:
React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066
React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec
React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab
react-native-quick-base64: cff21e7f1a145a63da9d71638fa1b592f08b4ef1
react-native-quick-base64: d35b481623c0004a82e4f15991a82f411761d95e
React-nativeconfig: b4d4e9901d4cabb57be63053fd2aa6086eb3c85f
React-NativeModulesApple: cd26e56d56350e123da0c1e3e4c76cb58a05e1ee
React-perflogger: 5f49905de275bac07ac7ea7f575a70611fa988f2
Expand Down
2 changes: 1 addition & 1 deletion example/src/MochaRNAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let only = false;

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

Expand Down
22 changes: 22 additions & 0 deletions example/src/tests/linebreaks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {expect} from 'chai';
import {toByteArray} from 'react-native-quick-base64';
import {describe, it} from '../MochaRNAdapter';
import {mapArr} from './util';

describe('linebreaks', () => {
// encoded `one\ntwo\nthree\nfour` in base64 online tool
const str = 'b25lCnR3bw==\ndGhyZWUKZm91cg==';

it('with linebreaks, leave them', () => {
expect(() => toByteArray(str)).to.throw(
/Input is not valid base64-encoded data/,
);
});

it('with linebreaks, remove them', () => {
const arr = toByteArray(str, true);
const actual = mapArr(arr, (byte: number) => String.fromCharCode(byte));
const expected = 'one\ntwothree\nfour';
expect(actual).to.equal(expected);
});
});
37 changes: 36 additions & 1 deletion example/src/tests/url-safe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {expect} from 'chai';
import {byteLength, toByteArray} from 'react-native-quick-base64';
import {
byteLength,
fromByteArray,
toByteArray,
trimBase64Padding,
} from 'react-native-quick-base64';
import {describe, it} from '../MochaRNAdapter';

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

expect(actual.length).to.equal(byteLength(str));
});

// test vector string comes from
// https://gist.github.com/pedrouid/b4056fd1f754918ddae86b32cf7d803e#aes-gcm---importkey
it('encode/decode base64url string w padding', async () => {
const expected = 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE';
const ba = toByteArray(expected);
const actual = fromByteArray(ba, true);
expect(trimBase64Padding(actual)).to.equal(
expected,
'base64 encode (url=true, trimmed)',
);
expect(actual).to.equal(
expected + '.',
'base64 encode (url=true, not trimmed)',
);
});

it('encode/decode base64 string w padding', async () => {
const expected = 'Y0zt37HgOx+BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE';
const ba = toByteArray(expected);
const actual = fromByteArray(ba, false);
expect(trimBase64Padding(actual)).to.equal(
expected,
'base64 encode (url=false, trimmed)',
);
expect(actual).to.equal(
expected + '=',
'base64 encode (url=false, not trimmed)',
);
});
});
56 changes: 56 additions & 0 deletions example/src/tests/zero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {expect} from 'chai';
import {
byteLength,
fromByteArray,
toByteArray,
} from 'react-native-quick-base64';
import {describe, it} from '../MochaRNAdapter';
import {mapArr, mapStr} from './util';

const checks: string[] = [
'no zero',
'contains a \0zero somewhere',
'\0starts with a zero',
'ends with a zero\0',
];

describe('zero (\\0)', () => {
for (let i = 0; i < checks.length; i++) {
const check = checks[i] as string;
it(`convert to base64 and back: '${check}'`, async () => {
const b64Str = fromByteArray(
mapStr(check, (char: string) => char.charCodeAt(0)),
);

const arr = toByteArray(b64Str);
const str = mapArr(arr, (byte: number) => String.fromCharCode(byte));
expect(str).to.equal(check);
expect(byteLength(b64Str)).to.equal(arr.length);
});
}

const test = (data: Uint8Array, expected: string, descr: string) => {
it(`known zero values: ${descr}`, async () => {
const actual = fromByteArray(data);
expect(actual).to.equal(expected);
});
};

// zero
const zero = new Uint8Array([122, 101, 114, 111]);
test(zero, 'emVybw==', 'zero');

// zer\0
const zer0 = new Uint8Array([122, 101, 114, 0]);
test(zer0, 'emVyAA==', 'zer\\0');

// \0er0
const ero = new Uint8Array([0, 101, 114, 111]);
test(ero, 'AGVybw==', '\\0ero');

// zer\0_value
const zer0_value = new Uint8Array([
122, 101, 114, 0, 95, 118, 97, 108, 117, 101,
]);
test(zer0_value, 'emVyAF92YWx1ZQ==', 'zer\\0_value');
});
2 changes: 2 additions & 0 deletions example/src/useTestList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import './tests/basics';
import './tests/convert';
import './tests/corrupt';
import './tests/url-safe';
import './tests/linebreaks';
import './tests/zero';
import './tests/big-data';

export const useTestList = (): Suites => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-quick-base64",
"version": "2.1.0",
"version": "2.1.1",
"description": "A native implementation of base64 in C++ for React Native",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
31 changes: 24 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ if (Base64Module && typeof Base64Module.install === 'function') {
Base64Module.install()
}

type FuncBase64ToArrayBuffer = (data: string) => ArrayBuffer
type FuncBase64FromArrayBuffer = (data: string | ArrayBuffer) => string
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
Expand Down Expand Up @@ -56,25 +62,32 @@ export function byteLength(b64: string): number {
return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen
}

export function toByteArray(b64: string): Uint8Array {
export function toByteArray(
b64: string,
removeLinebreaks: boolean = false
): Uint8Array {
if (typeof base64ToArrayBuffer !== 'undefined') {
return new Uint8Array(base64ToArrayBuffer(b64))
return new Uint8Array(base64ToArrayBuffer(b64, removeLinebreaks))
} else {
return fallback.toByteArray(b64)
}
}

export function fromByteArray(uint8: Uint8Array): 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
)
}
return base64FromArrayBuffer(uint8.buffer)
return base64FromArrayBuffer(uint8.buffer, urlSafe)
} else {
return fallback.fromByteArray(uint8)
}
Expand Down Expand Up @@ -103,3 +116,7 @@ export const getNative = () => ({
base64FromArrayBuffer,
base64ToArrayBuffer,
})

export const trimBase64Padding = (str: string): string => {
return str.replace(/[.=]{1,2}$/, '')
}

0 comments on commit 9d02dfd

Please sign in to comment.