Skip to content

Commit 7660f80

Browse files
committed
Convert uint8Array to Base64 on the native side
1 parent 30cae3d commit 7660f80

File tree

9 files changed

+75
-53
lines changed

9 files changed

+75
-53
lines changed

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.facebook.react.bridge.Arguments;
2121
import com.facebook.react.bridge.Promise;
2222
import com.facebook.react.bridge.ReactApplicationContext;
23+
import com.facebook.react.bridge.ReadableArray;
2324
import com.facebook.react.bridge.ReadableMap;
2425
import com.facebook.react.bridge.ReadableMapKeySetIterator;
2526
import com.facebook.react.bridge.ReadableType;
@@ -1027,6 +1028,15 @@ public void getDataFromUri(String uri, Promise promise) {
10271028
}
10281029
}
10291030

1031+
public void encodeToBase64(ReadableArray array, Promise promise) {
1032+
byte[] bytes = new byte[array.size()];
1033+
for (int i = 0; i < array.size(); i++) {
1034+
bytes[i] = (byte) array.getInt(i);
1035+
}
1036+
String base64String = android.util.Base64.encodeToString(bytes, android.util.Base64.DEFAULT);
1037+
promise.resolve(base64String);
1038+
}
1039+
10301040
public void crashedLastRun(Promise promise) {
10311041
promise.resolve(Sentry.isCrashedLastRun());
10321042
}

packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ public void getDataFromUri(String uri, Promise promise) {
183183
this.impl.getDataFromUri(uri, promise);
184184
}
185185

186+
@Override
187+
public void encodeToBase64(ReadableArray array, Promise promise) {
188+
this.impl.encodeToBase64(array, promise);
189+
}
190+
186191
@Override
187192
public void popTimeToDisplayFor(String key, Promise promise) {
188193
this.impl.popTimeToDisplayFor(key, promise);

packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ public void getDataFromUri(String uri, Promise promise) {
157157
this.impl.getDataFromUri(uri, promise);
158158
}
159159

160+
@ReactMethod
161+
public void encodeToBase64(ReadableArray array, Promise promise) {
162+
this.impl.encodeToBase64(array, promise);
163+
}
164+
160165
@ReactMethod(isBlockingSynchronousMethod = true)
161166
public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
162167
// Not used on Android

packages/core/ios/RNSentry.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,4 +970,28 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
970970
return @YES; // The return ensures that the method is synchronous
971971
}
972972

973+
RCT_EXPORT_METHOD(encodeToBase64
974+
: (NSArray *)array resolver
975+
: (RCTPromiseResolveBlock)resolve rejecter
976+
: (RCTPromiseRejectBlock)reject)
977+
{
978+
NSUInteger count = array.count;
979+
uint8_t *bytes = (uint8_t *)malloc(count);
980+
981+
if (!bytes) {
982+
reject(@"encodeToBase64", @"Memory allocation failed", nil);
983+
return;
984+
}
985+
986+
for (NSUInteger i = 0; i < count; i++) {
987+
bytes[i] = (uint8_t)[array[i] unsignedCharValue];
988+
}
989+
990+
NSData *data = [NSData dataWithBytes:bytes length:count];
991+
free(bytes);
992+
993+
NSString *base64String = [data base64EncodedStringWithOptions:0];
994+
resolve(base64String);
995+
}
996+
973997
@end

packages/core/src/js/NativeRNSentry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface Spec extends TurboModule {
5151
getDataFromUri(uri: string): Promise<number[]>;
5252
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
5353
setActiveSpanId(spanId: string): boolean;
54+
encodeToBase64(data: number[]): Promise<string | undefined | null>;
5455
}
5556

5657
export type NativeStackFrame = {

packages/core/src/js/feedback/FeedbackWidget.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ import {
1717

1818
import { isWeb, notWeb } from '../utils/environment';
1919
import type { Screenshot } from '../wrapper';
20-
import { getDataFromUri } from '../wrapper';
20+
import { getDataFromUri, NATIVE } from '../wrapper';
2121
import { sentryLogo } from './branding';
2222
import { defaultConfiguration } from './defaults';
2323
import defaultStyles from './FeedbackWidget.styles';
2424
import { getTheme } from './FeedbackWidget.theme';
2525
import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types';
2626
import { lazyLoadFeedbackIntegration } from './lazy';
2727
import { getCapturedScreenshot } from './ScreenshotButton';
28-
import { base64ToUint8Array, feedbackAlertDialog, isValidEmail,uint8ArrayToBase64 } from './utils';
28+
import { base64ToUint8Array, feedbackAlertDialog, isValidEmail } from './utils';
2929

3030
/**
3131
* @beta
@@ -344,9 +344,16 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
344344
private _setCapturedScreenshot = (screenshot: Screenshot): void => {
345345
if (screenshot.data != null) {
346346
logger.debug('Setting captured screenshot:', screenshot.filename);
347-
const base64String: string = uint8ArrayToBase64(screenshot.data);
348-
const dataUri = `data:${screenshot.contentType};base64,${base64String}`;
349-
this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri });
347+
NATIVE.encodeToBase64(screenshot.data).then((base64String) => {
348+
if (base64String != null) {
349+
const dataUri = `data:${screenshot.contentType};base64,${base64String}`;
350+
this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri });
351+
} else {
352+
logger.error('Failed to read image data from:', screenshot.filename);
353+
}
354+
}).catch((error) => {
355+
logger.error('Failed to read image data from:', screenshot.filename, 'error: ', error);
356+
});
350357
} else {
351358
logger.error('Failed to read image data from:', screenshot.filename);
352359
}

packages/core/src/js/feedback/utils.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -37,54 +37,6 @@ export const base64ToUint8Array = (base64: string): Uint8Array => {
3737
return new Uint8Array([...binaryString].map(char => char.charCodeAt(0)));
3838
};
3939

40-
/**
41-
* Converts a Uint8Array to a Base64 string
42-
* @param {Uint8Array} uint8Array - The Uint8Array containing image data
43-
* @returns {string} - Base64 string representation of the data
44-
*/
45-
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => {
46-
/* eslint-disable no-bitwise */
47-
let base64 = '';
48-
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
49-
const bytes = uint8Array;
50-
const byteLength = bytes.byteLength;
51-
const byteRemainder = byteLength % 3;
52-
const mainLength = byteLength - byteRemainder;
53-
54-
for (let i = 0; i < mainLength; i = i + 3) {
55-
const chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
56-
57-
// Use bitmasks to extract 6-bit segments from the triplet
58-
const a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
59-
const b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
60-
const c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
61-
const d = chunk & 63; // 63 = 2^6 - 1
62-
63-
// Convert the raw binary segments to the appropriate ASCII encoding
64-
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
65-
}
66-
67-
// Deal with the remaining bytes and padding
68-
if (byteRemainder === 1) {
69-
const chunk = bytes[mainLength];
70-
71-
const a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
72-
const b = (chunk & 3) << 4; // 3 = 2^2 - 1
73-
74-
base64 += `${encodings[a] + encodings[b]}==`;
75-
} else if (byteRemainder === 2) {
76-
const chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
77-
78-
const a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
79-
const b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
80-
const c = (chunk & 15) << 2; // 15 = 2^4 - 1
81-
82-
base64 += `${encodings[a] + encodings[b] + encodings[c]}=`;
83-
}
84-
/* eslint-enable no-bitwise */
85-
return base64;
86-
};
87-
8840
export const feedbackAlertDialog = (title: string, message: string): void => {
8941
if (isWeb() && typeof RN_GLOBAL_OBJ.alert !== 'undefined') {
9042
RN_GLOBAL_OBJ.alert(`${title}\n${message}`);

packages/core/src/js/wrapper.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ interface SentryNativeWrapper {
125125
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
126126

127127
setActiveSpanId(spanId: string): void;
128+
129+
encodeToBase64(data: Uint8Array): Promise<string | null>;
128130
}
129131

130132
const EOL = utf8ToBytes('\n');
@@ -746,6 +748,21 @@ export const NATIVE: SentryNativeWrapper = {
746748
}
747749
},
748750

751+
async encodeToBase64(data: Uint8Array): Promise<string | null> {
752+
if (!this.enableNative || !this._isModuleLoaded(RNSentry)) {
753+
return Promise.resolve(null);
754+
}
755+
756+
try {
757+
const byteArray = Array.from(data);
758+
const base64 = await RNSentry.encodeToBase64(byteArray);
759+
return base64 || null;
760+
} catch (error) {
761+
logger.error('Error:', error);
762+
return Promise.resolve(null);
763+
}
764+
},
765+
749766
/**
750767
* Gets the event from envelopeItem and applies the level filter to the selected event.
751768
* @param data An envelope item containing the event.

packages/core/test/mockWrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const NATIVE: MockInterface<NativeType> = {
6262
getDataFromUri: jest.fn(),
6363
popTimeToDisplayFor: jest.fn(),
6464
setActiveSpanId: jest.fn(),
65+
encodeToBase64: jest.fn(),
6566
};
6667

6768
NATIVE.isNativeAvailable.mockReturnValue(true);

0 commit comments

Comments
 (0)