Skip to content

Commit 99821e1

Browse files
committed
add base64 encoding and decoding methods
1 parent 9b6c741 commit 99821e1

File tree

1 file changed

+113
-0
lines changed

1 file changed

+113
-0
lines changed

packages/utils/src/string.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import { getGlobalObject } from './compat';
2+
import { SentryError } from './error';
13
import { isRegExp, isString } from './is';
24

5+
// credit to https://rgxdb.com/; see https://tools.ietf.org/html/rfc4648#section-4 for base64 spec
6+
// eslint-disable-next-line no-useless-escape
7+
export const BASE64_REGEX = /^(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))$/;
8+
39
/**
410
* Truncates given string to the maximum characters count
511
*
@@ -101,3 +107,110 @@ export function isMatchingPattern(value: string, pattern: RegExp | string): bool
101107
}
102108
return false;
103109
}
110+
111+
// TODO: Base64 crossed with different character encodings turns out to be a ridiculous can of worms. Base64 expects
112+
// 8-bit data. JS only uses UTF-16. We need a way to be sure that every SDK is speaking the same language and can decode
113+
// values base64-encoded by other SDKs. The current proposal is to use UTF-8 as the common standard, and then
114+
// base64-encode that (meaning in JS we need to get there first). Doing it that way makes a whole lot of sense but is a
115+
// work in progress which isn't yet actually working. Leaving the current solution for now and will come back to it.
116+
117+
/**
118+
* Convert a Unicode string to a string in which each 16-bit unit occupies only one byte, which makes it safe to use as
119+
* input to `btoa`.
120+
*
121+
* Copied from https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings.
122+
*
123+
* @param unicodeString The string to convert
124+
* @returns A btoa-compatible encoding of the string
125+
*/
126+
function unicodeToBinary(unicodeString: string): string {
127+
const codeUnits = new Uint16Array(unicodeString.length);
128+
for (let i = 0; i < codeUnits.length; i++) {
129+
codeUnits[i] = unicodeString.charCodeAt(i);
130+
}
131+
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
132+
}
133+
134+
/**
135+
* Convert a binary string (such as one would get from `atob`) into a Unicode string.
136+
*
137+
* Copied from https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings.
138+
*
139+
* @param binaryString The string to convert
140+
* @returns A btoa-compatible encoding of the string
141+
*/
142+
function binaryToUnicode(binaryString: string): string {
143+
const bytes = new Uint8Array(binaryString.length);
144+
for (let i = 0; i < bytes.length; i++) {
145+
bytes[i] = binaryString.charCodeAt(i);
146+
}
147+
return String.fromCharCode(...new Uint16Array(bytes.buffer));
148+
}
149+
150+
/**
151+
* Convert a base64 string to a Unicode (UTF-16) string.
152+
*
153+
* @param base64String The string to decode.
154+
* @throws SentryError (because using the logger creates a circular dependency)
155+
* @returns A Unicode string
156+
*/
157+
export function base64ToUnicode(base64String: string): string {
158+
if (typeof base64String !== 'string' || !BASE64_REGEX.test(base64String)) {
159+
throw new SentryError(`Unable to convert from base64. Input either isn't a string or isn't valid base64.`);
160+
}
161+
162+
const errMsg = `Unable to convert string from base64: ${
163+
base64String.length > 256 ? `${base64String.slice(0, 256)}...` : base64String
164+
}`;
165+
166+
try {
167+
// browsers have atob built in
168+
if ('atob' in getGlobalObject()) {
169+
// atob takes base64 (written in (a)scii) to (b)inary
170+
return binaryToUnicode(atob(base64String));
171+
}
172+
173+
// Buffer only exists in node
174+
if ('Buffer' in getGlobalObject()) {
175+
return Buffer.from(base64String, 'base64').toString('utf16le');
176+
}
177+
} catch (err) {
178+
throw new SentryError(`${errMsg} Got error: ${err}`);
179+
}
180+
181+
throw new SentryError(errMsg);
182+
}
183+
184+
/**
185+
* Convert a Unicode (UTF-16) string to a base64 string.
186+
*
187+
* @param unicodeString The string to encode
188+
* @throws SentryError (because using the logger creates a circular dependency)
189+
* @returns A base64-encoded version of the string
190+
*/
191+
export function unicodeToBase64(unicodeString: string): string {
192+
if (typeof unicodeString !== 'string') {
193+
throw new SentryError(`Unable to convert to base64. Input isn't a string.`);
194+
}
195+
196+
const errMsg = `Unable to convert string to base64: ${
197+
unicodeString.length > 256 ? `${unicodeString.slice(0, 256)}...` : unicodeString
198+
}`;
199+
200+
try {
201+
// browsers have btoa built in
202+
if ('btoa' in getGlobalObject()) {
203+
// btoa takes (b)inary to base64 (written in (a)scii)
204+
return btoa(unicodeToBinary(unicodeString));
205+
}
206+
207+
// Buffer only exists in node
208+
if ('Buffer' in getGlobalObject()) {
209+
return Buffer.from(unicodeString, 'utf16le').toString('base64');
210+
}
211+
} catch (err) {
212+
throw new SentryError(`${errMsg} Got error: ${err}`);
213+
}
214+
215+
throw new SentryError(errMsg);
216+
}

0 commit comments

Comments
 (0)