Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 7 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Try the benchmarks in the [example](./example) app.
- 🧩 Drop-in replacement for `base64-js` with matching API
- 🔒 No additional native setup or linking required

> ℹ️ **Heads-up**:
> Starting with recent versions of **Hermes**, `btoa` and `atob` are **natively available in the JS runtime**.
> You likely don't need to use the versions provided by this library anymore unless you're running on an older engine or want consistent behavior across platforms.
> These methods will remain in the package for compatibility but are considered **deprecated**.
> ⚠️ **Breaking change** (#53):
> The `btoa`, `atob`, and `shim()` polyfills have been **removed**.
> Recent versions of **Hermes** provide `btoa` and `atob` natively in the JS runtime, so the polyfills are no longer needed.
> If you need string ⇄ base64 conversion, use `TextEncoder` / `TextDecoder` together with `fromByteArray` / `toByteArray` (see Usage below).

---

Expand Down Expand Up @@ -50,10 +50,10 @@ import { fromByteArray, toByteArray } from 'react-native-quick-base64'
### Usage

```tsx
import { btoa, atob } from 'react-native-quick-base64'
import { fromByteArray, toByteArray } from 'react-native-quick-base64'

const base64 = btoa('foo')
const decoded = atob(base64)
const base64 = fromByteArray(new TextEncoder().encode('foo'))
const decoded = new TextDecoder().decode(toByteArray(base64))
```

---
Expand All @@ -76,32 +76,6 @@ If `removeLinebreaks` is `true`, all `\n` characters are removed first.
Converts a byte array into a base64 string.
If `urlSafe` is `true`, the output uses a URL-safe base64 charset.

### `btoa(data: string): string` ⚠️ Deprecated

Encodes a string into base64 format.

> **Avoid using this unless you're on an older JS engine.**
> Use `fromByteArray(new TextEncoder().encode(...))` instead for better encoding control.

### `atob(b64: string): string` ⚠️ Deprecated

Decodes a base64 string into a UTF-8 string.

> **Avoid using this unless you're on an older JS engine.**
> Use `TextDecoder + toByteArray()` for more robust decoding.

### `shim()`

Adds global `btoa` and `atob` functions

```ts
import { shim } from 'react-native-quick-base64'

shim()

btoa('foo') // available globally
```

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

Removes trailing `=` or `.` padding from base64 or base64url-encoded strings.
Expand Down
1 change: 1 addition & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ set (CMAKE_VERBOSE_MAKEFILE ON)
add_library(
${PACKAGE_NAME} STATIC
../cpp/QuickBase64Impl.cpp
../cpp/simdutf.cpp
)

set_target_properties(
Expand Down
45 changes: 41 additions & 4 deletions cpp/QuickBase64Impl.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#include "QuickBase64Impl.h"
#include "base64.h"
#include "simdutf.h"

#include <algorithm>
#include <memory>
#include <stdexcept>
#include <string>

namespace facebook::react {

namespace {

// Owns a decoded std::string. JSI holds the shared_ptr alive for the
// ArrayBuffer's lifetime — no memcpy needed.
namespace {
class DecodedBuffer final : public jsi::MutableBuffer {
public:
explicit DecodedBuffer(std::string&& s) noexcept : data_(std::move(s)) {}
Expand All @@ -19,6 +22,27 @@ class DecodedBuffer final : public jsi::MutableBuffer {
private:
std::string data_;
};

// Mirrors V8's Uint8Array.fromBase64 (TC39-aligned):
// - last_chunk_handling_options::loose accepts inputs without '=' padding
// - decode_up_to_bad_char mirrors V8's ArrayBufferFromBase64
// - base64_default_or_url accepts both standard (+/) and URL-safe (-_) alphabets
std::string decodeBase64(const std::string& input) {
size_t max_len = simdutf::maximal_binary_length_from_base64(input.data(), input.size());
std::string result(max_len, '\0');
size_t out_len = max_len;
auto r = simdutf::base64_to_binary_safe(
input.data(), input.size(), result.data(), out_len,
simdutf::base64_default_or_url,
simdutf::last_chunk_handling_options::loose,
/*decode_up_to_bad_char*/ true);
if (r.error != simdutf::error_code::SUCCESS) {
throw std::runtime_error("Input is not valid base64-encoded data");
}
result.resize(out_len);
return result;
}

} // namespace

QuickBase64Impl::QuickBase64Impl(std::shared_ptr<CallInvoker> jsInvoker)
Expand All @@ -34,7 +58,14 @@ jsi::String QuickBase64Impl::base64FromArrayBuffer(
}
auto arrayBuffer = buf.getArrayBuffer(rt);
try {
std::string encoded = base64_encode(arrayBuffer.data(rt), arrayBuffer.size(rt), urlSafe);
auto opts = urlSafe ? simdutf::base64_url : simdutf::base64_default;
size_t in_len = arrayBuffer.size(rt);
std::string encoded(simdutf::base64_length_from_binary(in_len, opts), '\0');
simdutf::binary_to_base64(
reinterpret_cast<const char*>(arrayBuffer.data(rt)),
in_len,
encoded.data(),
opts);
return jsi::String::createFromUtf8(rt, encoded);
} catch (const std::runtime_error& e) {
throw jsi::JSError(rt, e.what());
Expand All @@ -49,7 +80,13 @@ jsi::Object QuickBase64Impl::base64ToArrayBuffer(
bool removeLinebreaks
) {
try {
std::string decoded = base64_decode(b64.utf8(rt), removeLinebreaks);
std::string input = b64.utf8(rt);
if (removeLinebreaks) {
input.erase(std::remove(input.begin(), input.end(), '\n'), input.end());
} else if (input.find('\n') != std::string::npos) {
throw std::runtime_error("Input is not valid base64-encoded data");
}
std::string decoded = decodeBase64(input);
auto buf = std::make_shared<DecodedBuffer>(std::move(decoded));
return jsi::ArrayBuffer(rt, std::move(buf));
} catch (const std::runtime_error& e) {
Expand Down
Loading
Loading