|
| 1 | +import { getGlobalObject } from './compat'; |
| 2 | +import { SentryError } from './error'; |
1 | 3 | import { isRegExp, isString } from './is';
|
2 | 4 |
|
| 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 | + |
3 | 9 | /**
|
4 | 10 | * Truncates given string to the maximum characters count
|
5 | 11 | *
|
@@ -101,3 +107,110 @@ export function isMatchingPattern(value: string, pattern: RegExp | string): bool
|
101 | 107 | }
|
102 | 108 | return false;
|
103 | 109 | }
|
| 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