├── .gitignore ├── binding.gyp ├── package.json ├── LICENSE ├── fast-slow.js ├── benchmark.js ├── README.md ├── binding.cc ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | binding.node 3 | build 4 | node_modules 5 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "binding", 5 | "sources": [ "binding.cc" ], 6 | "include_dirs": [ " 0) { 67 | var decoded = crypto.randomBytes(size); 68 | encode.push(decoded); 69 | // Decoder should not switch to slow-mode permanently on any white space: 70 | var encoded = Buffer.concat([ 71 | Buffer.from(' ', 'ascii'), 72 | Base64.encode(decoded) 73 | ]); 74 | decode.push(encoded); 75 | } 76 | vectors[size] = { 77 | encode: encode, 78 | decode: decode 79 | }; 80 | } 81 | ); 82 | 83 | function benchmark(binding, method, buffers, end) { 84 | var now = Date.now(); 85 | var sum = 0; 86 | var time = 0; 87 | var count = 0; 88 | var queue = new Queue(1); 89 | queue.onData = function(buffer, end) { 90 | var hrtime = process.hrtime(); 91 | binding[method](buffer, 92 | function(error) { 93 | if (error) return end(error); 94 | var difference = process.hrtime(hrtime); 95 | var ns = (difference[0] * 1e9) + difference[1]; 96 | // Count the number of data bytes that can be processed per second: 97 | sum += buffer.length; 98 | time += ns; 99 | count++; 100 | end(); 101 | } 102 | ); 103 | }; 104 | queue.onEnd = function(error) { 105 | if (error) return end(error); 106 | var elapsed = Date.now() - now; 107 | var latency = (time / count) / 1000000; 108 | var throughput = sum / elapsed / 1000; 109 | display([ 110 | binding.name + ':', 111 | 'Latency:', 112 | latency.toFixed(3) + 'ms', 113 | 'Throughput:', 114 | throughput.toFixed(2) + ' MB/s' 115 | ]); 116 | end(); 117 | }; 118 | queue.concat(buffers); 119 | queue.end(); 120 | } 121 | 122 | function display(columns) { 123 | var string = columns[0]; 124 | while (string.length < 15) string = ' ' + string; 125 | string += ' ' + columns.slice(1).join(' '); 126 | console.log(string); 127 | } 128 | 129 | console.log(''); 130 | display([ 'CPU:', cpu ]); 131 | display([ 'Cores:', cores ]); 132 | display([ 'Threads:', concurrency ]); 133 | 134 | var queue = new Queue(); 135 | queue.onData = function(method, end) { 136 | console.log(''); 137 | console.log('============================================================'); 138 | var queue = new Queue(); 139 | queue.onData = function(size, end) { 140 | var buffers = vectors[size][method]; 141 | console.log(''); 142 | display([ 143 | method.slice(0, 1).toUpperCase() + method.slice(1) + ':', 144 | buffers.length + ' x ' + size + ' Bytes' 145 | ]); 146 | var queue = new Queue(); 147 | queue.onData = function(binding, end) { 148 | benchmark(binding, method, buffers, end); 149 | }; 150 | queue.onEnd = end; 151 | queue.concat(bindings); 152 | queue.end(); 153 | }; 154 | queue.onEnd = end; 155 | queue.concat(sizes); 156 | queue.end(); 157 | }; 158 | queue.onEnd = function(error) { 159 | if (error) throw error; 160 | console.log(''); 161 | }; 162 | queue.push('encode'); 163 | queue.push('decode'); 164 | queue.end(); 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # base64 2 | Fast, robust Base64 encoder/decoder for Buffers in C++. Ignores whitespace. Detects corruption and truncation. Ships with extensive tests, a fuzz test and a benchmark. 3 | 4 | ## Motivation 5 | 6 | Node has native Base64 methods, but these either encode a buffer and return a string (`buffer.toString('base64')`) or decode a string and return a buffer (`Buffer.from(string, 'base64')`). Node currently has no efficient way to encode a buffer as a Base64 buffer without creating an interim V8 String. Node currently has no efficient way to decode a Base64 buffer without creating an interim V8 String. This costs a few hundred megabytes per second of throughput for email servers which send and receive large email attachments. See [issue 11866](https://github.com/nodejs/node/issues/11866) for more information. 7 | 8 | Node also silently ignores obviously corrupted or truncated Base64 data, which is conflated with whitespace or missing padding and handled by Node in the same way. While it is right that whitespace or missing padding should be ignored when decoding, illegal code points or truncated Base64 data should raise an exception to avoid data loss. See [issue 8569](https://github.com/nodejs/node/issues/8569) for more information. 9 | 10 | ## Installation 11 | 12 | #### Linux, OS X 13 | This will compile the native binding automatically: 14 | ``` 15 | npm install @ronomon/base64 16 | ``` 17 | 18 | #### Windows 19 | This will skip compiling the native binding automatically: 20 | ``` 21 | npm install --ignore-scripts @ronomon/base64 22 | ``` 23 | 24 | ## Performance 25 | ``` 26 | CPU: Intel(R) Xeon(R) CPU E5-1620 v3 @ 3.50GHz 27 | Cores: 8 28 | Threads: 1 29 | 30 | ============================================================ 31 | 32 | Encode: 8192 x 128 Bytes 33 | Javascript: Latency: 0.003ms Throughput: 34.95 MB/s 34 | Native: Latency: 0.001ms Throughput: 74.90 MB/s 35 | Node: Latency: 0.001ms Throughput: 87.38 MB/s 36 | 37 | Encode: 1024 x 1024 Bytes 38 | Javascript: Latency: 0.005ms Throughput: 174.76 MB/s 39 | Native: Latency: 0.002ms Throughput: 524.29 MB/s 40 | Node: Latency: 0.003ms Throughput: 262.14 MB/s 41 | 42 | Encode: 32 x 32768 Bytes 43 | Javascript: Latency: 0.055ms Throughput: 524.29 MB/s 44 | Native: Latency: 0.029ms Throughput: 1048.58 MB/s 45 | Node: Latency: 0.037ms Throughput: 1048.58 MB/s 46 | 47 | Encode: 10 x 1048576 Bytes 48 | Javascript: Latency: 1.907ms Throughput: 551.88 MB/s 49 | Native: Latency: 1.104ms Throughput: 953.25 MB/s 50 | Node: Latency: 1.554ms Throughput: 655.36 MB/s 51 | 52 | Encode: 10 x 4194304 Bytes 53 | Javascript: Latency: 7.605ms Throughput: 551.88 MB/s 54 | Native: Latency: 4.350ms Throughput: 953.25 MB/s 55 | Node: Latency: 6.420ms Throughput: 655.36 MB/s 56 | 57 | ============================================================ 58 | 59 | Decode: 8192 x 128 Bytes 60 | Javascript: Latency: 0.002ms Throughput: 64.42 MB/s 61 | Native: Latency: 0.003ms Throughput: 52.49 MB/s 62 | Node: Latency: 0.001ms Throughput: 101.23 MB/s 63 | 64 | Decode: 1024 x 1024 Bytes 65 | Javascript: Latency: 0.008ms Throughput: 155.76 MB/s 66 | Native: Latency: 0.002ms Throughput: 467.29 MB/s 67 | Node: Latency: 0.003ms Throughput: 467.29 MB/s 68 | 69 | Decode: 32 x 32768 Bytes 70 | Javascript: Latency: 0.150ms Throughput: 279.64 MB/s 71 | Native: Latency: 0.046ms Throughput: 1398.18 MB/s 72 | Node: Latency: 0.094ms Throughput: 466.06 MB/s 73 | 74 | Decode: 10 x 1048576 Bytes 75 | Javascript: Latency: 4.523ms Throughput: 303.94 MB/s 76 | Native: Latency: 1.402ms Throughput: 998.65 MB/s 77 | Node: Latency: 1.986ms Throughput: 699.05 MB/s 78 | 79 | Decode: 10 x 4194304 Bytes 80 | Javascript: Latency: 18.337ms Throughput: 305.60 MB/s 81 | Native: Latency: 5.575ms Throughput: 998.64 MB/s 82 | Node: Latency: 8.018ms Throughput: 699.05 MB/s 83 | ``` 84 | 85 | ## Native Binding (Optional) 86 | The native binding will be installed automatically when installing `@ronomon/base64` without the `--ignore-scripts` argument. The Javascript binding will be used if the native binding could not be compiled or is not available. To compile the native binding manually after installing, install [node-gyp](https://www.npmjs.com/package/node-gyp) globally: 87 | ``` 88 | sudo npm install node-gyp -g 89 | ``` 90 | Then build the binding from within the `@ronomon/base64` module directory: 91 | ``` 92 | cd node_modules/@ronomon/base64 93 | node-gyp rebuild 94 | ``` 95 | 96 | ## Usage 97 | 98 | #### Encoding 99 | ```javascript 100 | var Base64 = require('@ronomon/base64'); 101 | var buffer = Buffer.from('Ecclesiastes 9:11-18', 'utf-8'); 102 | var bufferEncoded = Base64.encode(buffer); 103 | console.log(bufferEncoded.toString('ascii')); 104 | // "RWNjbGVzaWFzdGVzIDk6MTEtMTg=" 105 | ``` 106 | 107 | #### Encoding 76 characters per line 108 | ```javascript 109 | var bufferEncoded = Base64.encode(buffer, { wrap: true }); 110 | ``` 111 | 112 | #### Decoding 113 | ```javascript 114 | var Base64 = require('@ronomon/base64'); 115 | var bufferEncoded = Buffer.from('RWNjbGVzaWFzdGVzIDk6MTEtMTg=', 'ascii'); 116 | var buffer = Base64.decode(bufferEncoded); 117 | console.log(buffer.toString('utf-8')); 118 | // "Ecclesiastes 9:11-18" 119 | ``` 120 | 121 | #### Decoding corrupt or truncated data 122 | Base64 will raise an exception for corrupt or truncated data by default as a defensive measure to prevent data loss and security vulnerabilities. To silence these exceptions and continue decoding in the face of bad data (not recommended), use `options.silent`: 123 | ```javascript 124 | var Base64 = require('@ronomon/base64'); 125 | var bufferEncoded = Buffer.from('...RWNjbGVzaWFzdGVzIDk6MTEtMTg=', 'ascii'); 126 | var buffer = Base64.decode(bufferEncoded, { silent: true }); 127 | console.log(buffer.toString('utf-8')); 128 | // "Ecclesiastes 9:11-18" 129 | ``` 130 | 131 | ## Tests 132 | To test the native and Javascript bindings: 133 | ``` 134 | node test.js 135 | ``` 136 | 137 | ## Benchmark 138 | To benchmark the native and Javascript bindings: 139 | ``` 140 | node benchmark.js 141 | ``` 142 | -------------------------------------------------------------------------------- /binding.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint32_t ceil_div(uint32_t x, uint32_t y) { 5 | return (x % y) ? ((x / y) + 1) : (x / y); 6 | } 7 | 8 | const uint8_t SILENT = 1; 9 | const uint8_t WRAP = 2; 10 | 11 | const uint32_t SPECIAL = 1 << 24; 12 | const uint32_t ILLEGAL = 1 << 25; 13 | const uint8_t PADDING = 61; // "="; 14 | const unsigned char SYMBOLS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 15 | "abcdefghijklmnopqrstuvwxyz" 16 | "0123456789+/"; 17 | 18 | uint32_t decode_table_0[256]; 19 | uint32_t decode_table_1[256]; 20 | uint32_t decode_table_2[256]; 21 | uint32_t decode_table_3[256]; 22 | 23 | void init_decode_table(uint32_t table[], const int shift) { 24 | // Illegal: 25 | for (int index = 0; index < 256; index++) { 26 | table[index] = SPECIAL | ILLEGAL; 27 | } 28 | // Special: 29 | static const unsigned char special[] = "\t\n\r ="; 30 | for (int index = 0; index < 5; index++) { 31 | table[special[index]] = SPECIAL; 32 | } 33 | // Symbols: 34 | for (int index = 0; index < 64; index++) { 35 | table[SYMBOLS[index]] = index << shift; 36 | } 37 | // Standard 'base64url' with URL and Filename Safe Alphabet (RFC 4648): 38 | table[45] = 62 << shift; // "-" 39 | table[95] = 63 << shift; // "_" 40 | // Modified Base64 encoding for IMAP mailbox names (RFC 3501): 41 | table[44] = 63 << shift; // "," 42 | } 43 | 44 | uint8_t encode_table_0[256]; 45 | uint8_t encode_table_1[4096]; 46 | 47 | void init_encode_table_0() { 48 | for (int index = 0; index < 256; index++) { 49 | encode_table_0[index] = SYMBOLS[index >> 2]; 50 | } 51 | } 52 | 53 | void init_encode_table_1() { 54 | for (int index = 0; index < 4096; index++) { 55 | encode_table_1[index] = SYMBOLS[index & 63]; 56 | } 57 | } 58 | 59 | int decode_step( 60 | const uint32_t flags, 61 | const uint8_t* source, 62 | uint8_t* target, 63 | const uint32_t sourceLength, 64 | uint32_t &sourceIndex, 65 | uint32_t &targetIndex, 66 | uint8_t temp[], 67 | uint8_t &tempIndex 68 | ) { 69 | while (sourceIndex < sourceLength) { 70 | if (decode_table_3[source[sourceIndex]] & SPECIAL) { 71 | if (decode_table_3[source[sourceIndex]] & ILLEGAL) { 72 | if (!(flags & SILENT)) { 73 | return 0; 74 | } 75 | } 76 | sourceIndex++; 77 | } else { 78 | temp[tempIndex++] = source[sourceIndex++]; 79 | if (tempIndex == 4) { 80 | const uint32_t word = ( 81 | decode_table_0[temp[0]] | 82 | decode_table_1[temp[1]] | 83 | decode_table_2[temp[2]] | 84 | decode_table_3[temp[3]] 85 | ); 86 | target[targetIndex + 0] = (word >> 16) & 0xFF; 87 | target[targetIndex + 1] = (word >> 8) & 0xFF; 88 | target[targetIndex + 2] = word & 0xFF; 89 | targetIndex += 3; 90 | tempIndex = 0; 91 | break; 92 | } 93 | } 94 | } 95 | return 1; 96 | } 97 | 98 | uint32_t encodeTargetLength(const uint32_t sourceLength, const uint32_t flags) { 99 | uint32_t symbols = ceil_div(sourceLength, 3) * 4; 100 | if (flags & WRAP) { 101 | uint32_t lines = ceil_div(symbols, 76); 102 | uint32_t breaks = lines > 0 ? lines - 1 : 0; 103 | return symbols + (breaks * 2); 104 | } else { 105 | return symbols; 106 | } 107 | } 108 | 109 | NAN_METHOD(decode) { 110 | if (info.Length() != 3) { 111 | return Nan::ThrowError("bad number of arguments"); 112 | } 113 | if (!node::Buffer::HasInstance(info[0])) { 114 | return Nan::ThrowError("source must be a buffer"); 115 | } 116 | if (!node::Buffer::HasInstance(info[1])) { 117 | return Nan::ThrowError("target must be a buffer"); 118 | } 119 | if (!info[2]->IsUint32()) { 120 | return Nan::ThrowError("flags must be an integer"); 121 | } 122 | v8::Local sourceHandle = info[0].As(); 123 | v8::Local targetHandle = info[1].As(); 124 | const uint32_t flags = info[2]->Uint32Value(); 125 | const uint32_t sourceLength = node::Buffer::Length(sourceHandle); 126 | const uint32_t targetLength = node::Buffer::Length(targetHandle); 127 | if (targetLength < (ceil_div(sourceLength, 4) * 3)) { 128 | return Nan::ThrowError("target too small"); 129 | } 130 | const uint8_t* source = reinterpret_cast( 131 | node::Buffer::Data(sourceHandle) 132 | ); 133 | uint8_t* target = reinterpret_cast( 134 | node::Buffer::Data(targetHandle) 135 | ); 136 | uint8_t temp[4]; 137 | uint8_t tempIndex = 0; 138 | uint32_t sourceIndex = 0; 139 | uint32_t targetIndex = 0; 140 | uint32_t sourceSubset = sourceLength >= 3 ? sourceLength - 3 : 0; 141 | while (sourceIndex < sourceSubset) { 142 | const uint32_t word = ( 143 | decode_table_0[source[sourceIndex + 0]] | 144 | decode_table_1[source[sourceIndex + 1]] | 145 | decode_table_2[source[sourceIndex + 2]] | 146 | decode_table_3[source[sourceIndex + 3]] 147 | ); 148 | if (word & SPECIAL) { 149 | if ( 150 | !decode_step( 151 | flags, 152 | source, 153 | target, 154 | sourceLength, 155 | sourceIndex, 156 | targetIndex, 157 | temp, 158 | tempIndex 159 | ) 160 | ) { 161 | return Nan::ThrowError("source is corrupt"); 162 | } 163 | } else { 164 | target[targetIndex + 0] = (word >> 16) & 0xFF; 165 | target[targetIndex + 1] = (word >> 8) & 0xFF; 166 | target[targetIndex + 2] = word & 0xFF; 167 | sourceIndex += 4; 168 | targetIndex += 3; 169 | } 170 | } 171 | if ( 172 | !decode_step( 173 | flags, 174 | source, 175 | target, 176 | sourceLength, 177 | sourceIndex, 178 | targetIndex, 179 | temp, 180 | tempIndex 181 | ) 182 | ) { 183 | return Nan::ThrowError("source is corrupt"); 184 | } 185 | if (tempIndex != 0) { 186 | if (tempIndex == 1) { 187 | if (!(flags & SILENT)) { 188 | return Nan::ThrowError("source is truncated"); 189 | } 190 | } else if (tempIndex == 2) { 191 | const uint32_t word = ( 192 | decode_table_0[temp[0]] | 193 | decode_table_1[temp[1]] 194 | ); 195 | target[targetIndex + 0] = (word >> 16) & 0xFF; 196 | targetIndex += 1; 197 | } else if (tempIndex == 3) { 198 | const uint32_t word = ( 199 | decode_table_0[temp[0]] | 200 | decode_table_1[temp[1]] | 201 | decode_table_2[temp[2]] 202 | ); 203 | target[targetIndex + 0] = (word >> 16) & 0xFF; 204 | target[targetIndex + 1] = (word >> 8) & 0xFF; 205 | targetIndex += 2; 206 | } else { 207 | return Nan::ThrowError("tempIndex > 3"); 208 | } 209 | } 210 | if (sourceIndex > sourceLength) { 211 | return Nan::ThrowError("source overflow"); 212 | } 213 | if (targetIndex > targetLength) { 214 | return Nan::ThrowError("target overflow"); 215 | } 216 | info.GetReturnValue().Set(targetIndex); 217 | } 218 | 219 | NAN_METHOD(encode) { 220 | if (info.Length() != 3) { 221 | return Nan::ThrowError("bad number of arguments"); 222 | } 223 | if (!node::Buffer::HasInstance(info[0])) { 224 | return Nan::ThrowError("source must be a buffer"); 225 | } 226 | if (!node::Buffer::HasInstance(info[1])) { 227 | return Nan::ThrowError("target must be a buffer"); 228 | } 229 | if (!info[2]->IsUint32()) { 230 | return Nan::ThrowError("flags must be an integer"); 231 | } 232 | v8::Local sourceHandle = info[0].As(); 233 | v8::Local targetHandle = info[1].As(); 234 | const uint32_t flags = info[2]->Uint32Value(); 235 | const uint32_t sourceLength = node::Buffer::Length(sourceHandle); 236 | const uint32_t targetLength = node::Buffer::Length(targetHandle); 237 | if (targetLength < encodeTargetLength(sourceLength, flags)) { 238 | return Nan::ThrowError("target too small"); 239 | } 240 | const uint8_t* source = reinterpret_cast( 241 | node::Buffer::Data(sourceHandle) 242 | ); 243 | uint8_t* target = reinterpret_cast( 244 | node::Buffer::Data(targetHandle) 245 | ); 246 | uint8_t a; 247 | uint8_t b; 248 | uint8_t c; 249 | uint8_t line = 0; 250 | uint32_t sourceIndex = 0; 251 | uint32_t targetIndex = 0; 252 | uint32_t sourceSubset = (sourceLength / 3) * 3; 253 | if (flags & WRAP) { 254 | while (sourceIndex < sourceSubset) { 255 | a = source[sourceIndex + 0]; 256 | b = source[sourceIndex + 1]; 257 | c = source[sourceIndex + 2]; 258 | target[targetIndex + 0] = encode_table_0[a]; 259 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 260 | target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)]; 261 | target[targetIndex + 3] = encode_table_1[c]; 262 | sourceIndex += 3; 263 | targetIndex += 4; 264 | line += 4; 265 | if (line == 76 && sourceIndex < sourceLength) { 266 | target[targetIndex + 0] = 13; 267 | target[targetIndex + 1] = 10; 268 | targetIndex += 2; 269 | line = 0; 270 | } 271 | } 272 | } else { 273 | while (sourceIndex < sourceSubset) { 274 | a = source[sourceIndex + 0]; 275 | b = source[sourceIndex + 1]; 276 | c = source[sourceIndex + 2]; 277 | target[targetIndex + 0] = encode_table_0[a]; 278 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 279 | target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)]; 280 | target[targetIndex + 3] = encode_table_1[c]; 281 | sourceIndex += 3; 282 | targetIndex += 4; 283 | } 284 | } 285 | switch (sourceLength - sourceSubset) { 286 | case 1: 287 | a = source[sourceIndex + 0]; 288 | target[targetIndex + 0] = encode_table_0[a]; 289 | target[targetIndex + 1] = encode_table_1[(a << 4)]; 290 | target[targetIndex + 2] = PADDING; 291 | target[targetIndex + 3] = PADDING; 292 | targetIndex += 4; 293 | break; 294 | case 2: 295 | a = source[sourceIndex + 0]; 296 | b = source[sourceIndex + 1]; 297 | target[targetIndex + 0] = encode_table_0[a]; 298 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 299 | target[targetIndex + 2] = encode_table_1[(b << 2)]; 300 | target[targetIndex + 3] = PADDING; 301 | targetIndex += 4; 302 | break; 303 | } 304 | if (sourceIndex > sourceLength) { 305 | return Nan::ThrowError("source overflow"); 306 | } 307 | if (targetIndex > targetLength) { 308 | return Nan::ThrowError("target overflow"); 309 | } 310 | info.GetReturnValue().Set(targetIndex); 311 | } 312 | 313 | NAN_MODULE_INIT(Init) { 314 | init_decode_table(decode_table_0, 0 + 6 + 6 + 6); 315 | init_decode_table(decode_table_1, 0 + 6 + 6); 316 | init_decode_table(decode_table_2, 0 + 6); 317 | init_decode_table(decode_table_3, 0); 318 | init_encode_table_0(); 319 | init_encode_table_1(); 320 | NAN_EXPORT(target, decode); 321 | NAN_EXPORT(target, encode); 322 | } 323 | 324 | NODE_MODULE(binding, Init) 325 | 326 | // S.D.G. 327 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Base64 = {}; 4 | 5 | Base64.SILENT = 1; 6 | Base64.WRAP = 2; 7 | 8 | Base64.assertBinding = function(binding) { 9 | if (!binding) throw new Error('binding must be defined'); 10 | if (!binding.decode) throw new Error('binding.decode must be defined'); 11 | if (!binding.encode) throw new Error('binding.encode must be defined'); 12 | if (typeof binding.decode != 'function') { 13 | throw new Error('binding.decode must be a function'); 14 | } 15 | if (typeof binding.encode != 'function') { 16 | throw new Error('binding.encode must be a function'); 17 | } 18 | }; 19 | 20 | Base64.assertBoolean = function(key, value) { 21 | if (value !== true && value !== false) { 22 | throw new Error(key + ' must be a boolean'); 23 | } 24 | }; 25 | 26 | Base64.assertInteger = function(key, value) { 27 | if (typeof value !== 'number' || Math.floor(value) !== value || value < 0) { 28 | throw new Error(key + ' must be an integer'); 29 | } 30 | }; 31 | 32 | Base64.binding = {}; 33 | 34 | Base64.binding.javascript = (function() { 35 | 36 | var SILENT = Base64.SILENT; 37 | var WRAP = Base64.WRAP; 38 | 39 | var SPECIAL = 1 << 24; 40 | var ILLEGAL = 1 << 25; 41 | var PADDING = '='.charCodeAt(0); 42 | var SYMBOLS = (function() { 43 | var map = ''; 44 | map += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 45 | map += 'abcdefghijklmnopqrstuvwxyz'; 46 | map += '0123456789+/'; 47 | var array = new Uint8Array(map.length); 48 | for (var index = 0, length = map.length; index < length; index++) { 49 | array[index] = map.charCodeAt(index); 50 | } 51 | return array; 52 | })(); 53 | 54 | var decode_table_0 = new Array(256); 55 | var decode_table_1 = new Array(256); 56 | var decode_table_2 = new Array(256); 57 | var decode_table_3 = new Array(256); 58 | 59 | function init_decode_table(table, shift) { 60 | // Illegal: 61 | for (var index = 0; index < table.length; index++) { 62 | table[index] = SPECIAL | ILLEGAL; 63 | } 64 | // Special: 65 | var special = '\t\n\r ='; 66 | for (var index = 0, length = special.length; index < length; index++) { 67 | table[special.charCodeAt(index)] = SPECIAL; 68 | } 69 | // Symbols: 70 | for (var index = 0, length = SYMBOLS.length; index < length; index++) { 71 | table[SYMBOLS[index]] = index << shift; 72 | } 73 | // Standard 'base64url' with URL and Filename Safe Alphabet (RFC 4648): 74 | table[45] = 62 << shift; // "-" 75 | table[95] = 63 << shift; // "_" 76 | // Modified Base64 encoding for IMAP mailbox names (RFC 3501): 77 | table[44] = 63 << shift; // "," 78 | } 79 | 80 | init_decode_table(decode_table_0, 0 + 6 + 6 + 6); 81 | init_decode_table(decode_table_1, 0 + 6 + 6); 82 | init_decode_table(decode_table_2, 0 + 6); 83 | init_decode_table(decode_table_3, 0); 84 | 85 | var encode_table_0 = new Uint8Array(256); 86 | var encode_table_1 = new Uint8Array(4096); 87 | 88 | function init_encode_table_0() { 89 | for (var index = 0; index < 256; index++) { 90 | encode_table_0[index] = SYMBOLS[index >> 2]; 91 | } 92 | } 93 | 94 | function init_encode_table_1() { 95 | for (var index = 0; index < 4096; index++) { 96 | encode_table_1[index] = SYMBOLS[index & 63]; 97 | } 98 | } 99 | 100 | init_encode_table_0(); 101 | init_encode_table_1(); 102 | 103 | function decode(source, target, flags) { 104 | if (!Buffer.isBuffer(source)) { 105 | throw new Error('source must be a buffer'); 106 | } 107 | if (!Buffer.isBuffer(target)) { 108 | throw new Error('target must be a buffer'); 109 | } 110 | Base64.assertInteger('flags', flags); 111 | var targetLength = target.length; 112 | var sourceLength = source.length; 113 | if (targetLength < (Math.ceil(sourceLength / 4) * 3)) { 114 | throw new Error('target too small'); 115 | } 116 | function step() { 117 | while (sourceIndex < sourceLength) { 118 | if (decode_table_3[source[sourceIndex]] & SPECIAL) { 119 | if (decode_table_3[source[sourceIndex]] & ILLEGAL) { 120 | if (!(flags & SILENT)) { 121 | throw new Error('source is corrupt'); 122 | } 123 | } 124 | sourceIndex++; 125 | } else { 126 | temp[tempIndex++] = source[sourceIndex++]; 127 | if (tempIndex === 4) { 128 | word = ( 129 | decode_table_0[temp[0]] | 130 | decode_table_1[temp[1]] | 131 | decode_table_2[temp[2]] | 132 | decode_table_3[temp[3]] 133 | ); 134 | target[targetIndex + 0] = (word >>> 16) & 0xFF; 135 | target[targetIndex + 1] = (word >>> 8) & 0xFF; 136 | target[targetIndex + 2] = word & 0xFF; 137 | targetIndex += 3; 138 | tempIndex = 0; 139 | break; 140 | } 141 | } 142 | } 143 | } 144 | var word = 0; 145 | var temp = new Array(4); 146 | temp[0] = 0; 147 | temp[1] = 0; 148 | temp[2] = 0; 149 | temp[3] = 0; 150 | var tempIndex = 0; 151 | var sourceIndex = 0; 152 | var targetIndex = 0; 153 | var sourceSubset = sourceLength - 3; 154 | while (sourceIndex < sourceSubset) { 155 | word = ( 156 | decode_table_0[source[sourceIndex + 0]] | 157 | decode_table_1[source[sourceIndex + 1]] | 158 | decode_table_2[source[sourceIndex + 2]] | 159 | decode_table_3[source[sourceIndex + 3]] 160 | ); 161 | if (word & SPECIAL) { 162 | if (word & ILLEGAL) { 163 | if (!(flags & SILENT)) { 164 | throw new Error('source is corrupt'); 165 | } 166 | } 167 | step(); 168 | } else { 169 | target[targetIndex + 0] = (word >>> 16) & 0xFF; 170 | target[targetIndex + 1] = (word >>> 8) & 0xFF; 171 | target[targetIndex + 2] = word & 0xFF; 172 | sourceIndex += 4; 173 | targetIndex += 3; 174 | } 175 | } 176 | step(); 177 | if (tempIndex !== 0) { 178 | if (tempIndex === 1) { 179 | if (!(flags & SILENT)) { 180 | throw new Error('source is truncated'); 181 | } 182 | } else if (tempIndex === 2) { 183 | word = ( 184 | decode_table_0[temp[0]] | 185 | decode_table_1[temp[1]] 186 | ); 187 | target[targetIndex + 0] = (word >>> 16) & 0xFF; 188 | targetIndex += 1; 189 | } else if (tempIndex === 3) { 190 | word = ( 191 | decode_table_0[temp[0]] | 192 | decode_table_1[temp[1]] | 193 | decode_table_2[temp[2]] 194 | ); 195 | target[targetIndex + 0] = (word >>> 16) & 0xFF; 196 | target[targetIndex + 1] = (word >>> 8) & 0xFF; 197 | targetIndex += 2; 198 | } else { 199 | throw new Error('tempIndex > 3'); 200 | } 201 | } 202 | return targetIndex; 203 | } 204 | 205 | function encode(source, target, flags) { 206 | if (!Buffer.isBuffer(source)) { 207 | throw new Error('source must be a buffer'); 208 | } 209 | if (!Buffer.isBuffer(target)) { 210 | throw new Error('target must be a buffer'); 211 | } 212 | Base64.assertInteger('flags', flags); 213 | var sourceLength = source.length; 214 | var targetLength = target.length; 215 | if (targetLength < Base64.encodeTargetLength(sourceLength, flags)) { 216 | throw new Error('target too small'); 217 | } 218 | var a; 219 | var b; 220 | var c; 221 | var line = 0; 222 | var targetIndex = 0; 223 | var sourceIndex = 0; 224 | var sourceSubset = Math.floor(sourceLength / 3) * 3; 225 | if (flags & WRAP) { 226 | while (sourceIndex < sourceSubset) { 227 | a = source[sourceIndex + 0]; 228 | b = source[sourceIndex + 1]; 229 | c = source[sourceIndex + 2]; 230 | target[targetIndex + 0] = encode_table_0[a]; 231 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 232 | target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)]; 233 | target[targetIndex + 3] = encode_table_1[c]; 234 | sourceIndex += 3; 235 | targetIndex += 4; 236 | line += 4; 237 | if (line === 76 && sourceIndex < sourceLength) { 238 | target[targetIndex + 0] = 13; 239 | target[targetIndex + 1] = 10; 240 | targetIndex += 2; 241 | line = 0; 242 | } 243 | } 244 | } else { 245 | while (sourceIndex < sourceSubset) { 246 | a = source[sourceIndex + 0]; 247 | b = source[sourceIndex + 1]; 248 | c = source[sourceIndex + 2]; 249 | target[targetIndex + 0] = encode_table_0[a]; 250 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 251 | target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)]; 252 | target[targetIndex + 3] = encode_table_1[c]; 253 | sourceIndex += 3; 254 | targetIndex += 4; 255 | } 256 | } 257 | switch (sourceLength - sourceSubset) { 258 | case 1: 259 | a = source[sourceIndex + 0]; 260 | target[targetIndex + 0] = encode_table_0[a]; 261 | target[targetIndex + 1] = encode_table_1[(a << 4)]; 262 | target[targetIndex + 2] = PADDING; 263 | target[targetIndex + 3] = PADDING; 264 | targetIndex += 4; 265 | break; 266 | case 2: 267 | a = source[sourceIndex + 0]; 268 | b = source[sourceIndex + 1]; 269 | target[targetIndex + 0] = encode_table_0[a]; 270 | target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)]; 271 | target[targetIndex + 2] = encode_table_1[(b << 2)]; 272 | target[targetIndex + 3] = PADDING; 273 | targetIndex += 4; 274 | break; 275 | } 276 | if (sourceIndex > sourceLength) { 277 | throw new Error('source overflow'); 278 | } 279 | if (targetIndex > targetLength) { 280 | throw new Error('target overflow'); 281 | } 282 | return targetIndex; 283 | } 284 | 285 | return { 286 | decode: decode, 287 | encode: encode 288 | }; 289 | 290 | })(); 291 | 292 | try { 293 | Base64.binding.native = require('./binding.node'); 294 | Base64.binding.active = Base64.binding.native; 295 | } catch (exception) { 296 | // We use the Javascript binding if the native binding has not been compiled. 297 | Base64.binding.active = Base64.binding.javascript; 298 | } 299 | 300 | Base64.decode = function(source, options) { 301 | var self = this; 302 | var binding = self.binding.active; 303 | var flags = 0; 304 | if (options) { 305 | if (options.hasOwnProperty('binding')) { 306 | self.assertBinding(options.binding); 307 | binding = options.binding; 308 | } 309 | if (options.hasOwnProperty('silent')) { 310 | self.assertBoolean('silent', options.silent); 311 | if (options.silent) flags |= self.SILENT; 312 | } 313 | } 314 | var target = Buffer.alloc(Math.ceil(source.length / 4) * 3); 315 | var targetSize = binding.decode(source, target, flags); 316 | if (targetSize > target.length) throw new Error('target overflow'); 317 | return target.slice(0, targetSize); 318 | }; 319 | 320 | Base64.encode = function(source, options) { 321 | var self = this; 322 | var binding = self.binding.active; 323 | var flags = 0; 324 | if (options) { 325 | if (options.hasOwnProperty('binding')) { 326 | self.assertBinding(options.binding); 327 | binding = options.binding; 328 | } 329 | if (options.hasOwnProperty('wrap')) { 330 | self.assertBoolean('wrap', options.wrap); 331 | if (options.wrap) flags |= self.WRAP; 332 | } 333 | } 334 | var target = Buffer.alloc(self.encodeTargetLength(source.length, flags)); 335 | var targetSize = binding.encode(source, target, flags); 336 | if (targetSize > target.length) throw new Error('target overflow'); 337 | return target.slice(0, targetSize); 338 | }; 339 | 340 | Base64.encodeTargetLength = function(sourceLength, flags) { 341 | var self = this; 342 | self.assertInteger('sourceLength', sourceLength); 343 | self.assertInteger('flags', flags); 344 | var symbols = Math.ceil(sourceLength / 3) * 4; 345 | if (flags & self.WRAP) { 346 | var lines = Math.ceil(symbols / 76); 347 | var breaks = lines > 0 ? lines - 1 : 0; 348 | return symbols + (breaks * 2); 349 | } else { 350 | return symbols; 351 | } 352 | }; 353 | 354 | module.exports = Base64; 355 | 356 | // S.D.G. 357 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var Node = { crypto: require('crypto') }; 2 | 3 | var Test = {}; 4 | 5 | Test.equal = function(value, expected, namespace, description) { 6 | value = JSON.stringify(value) + ''; 7 | expected = JSON.stringify(expected) + ''; 8 | if (value === expected) { 9 | Test.pass(namespace, description, expected); 10 | } else { 11 | Test.fail(namespace, description, value + ' !== ' + expected); 12 | } 13 | }; 14 | 15 | Test.fail = function(namespace, description, message) { 16 | console.log(''); 17 | throw 'FAIL: ' + Test.message(namespace, description, message); 18 | }; 19 | 20 | Test.message = function(namespace, description, message) { 21 | if ((namespace = namespace || '')) namespace += ': '; 22 | if ((description = description || '')) description += ': '; 23 | return namespace + description + (message || ''); 24 | }; 25 | 26 | Test.pass = function(namespace, description, message) { 27 | console.log('PASS: ' + Test.message(namespace, description, message)); 28 | }; 29 | 30 | var RNG = function(seed) { 31 | var self = this; 32 | if (seed === undefined) seed = Date.now(); 33 | if (typeof seed !== 'number' || Math.round(seed) !== seed || seed < 0) { 34 | throw new Error('bad seed'); 35 | } 36 | self.seed = seed % Math.pow(2, 31); 37 | self.hash = self.seed; 38 | }; 39 | 40 | RNG.prototype.random = function() { 41 | var self = this; 42 | self.hash = ((self.hash + 0x7ED55D16) + (self.hash << 12)) & 0xFFFFFFF; 43 | self.hash = ((self.hash ^ 0xC761C23C) ^ (self.hash >>> 19)) & 0xFFFFFFF; 44 | self.hash = ((self.hash + 0x165667B1) + (self.hash << 5)) & 0xFFFFFFF; 45 | self.hash = ((self.hash + 0xD3A2646C) ^ (self.hash << 9)) & 0xFFFFFFF; 46 | self.hash = ((self.hash + 0xFD7046C5) + (self.hash << 3)) & 0xFFFFFFF; 47 | self.hash = ((self.hash ^ 0xB55A4F09) ^ (self.hash >>> 16)) & 0xFFFFFFF; 48 | return (self.hash & 0xFFFFFFF) / 0x10000000; 49 | }; 50 | 51 | var rng = new RNG(1); 52 | var random = rng.random.bind(rng); 53 | 54 | var namespace = 'Base64'; 55 | 56 | var WHITESPACE = '\t\n\r '; 57 | 58 | var VALID = (function() { 59 | var array = new Uint8Array(256); 60 | var map1 = ''; 61 | map1 += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 62 | map1 += 'abcdefghijklmnopqrstuvwxyz'; 63 | map1 += '0123456789+/'; 64 | map1 += '-_,'; 65 | for (var index = 0, length = map1.length; index < length; index++) { 66 | array[map1.charCodeAt(index)] = 1; 67 | } 68 | var map2 = '\t\n\r ='; 69 | for (var index = 0, length = map2.length; index < length; index++) { 70 | array[map2.charCodeAt(index)] = 2; 71 | } 72 | return array; 73 | })(); 74 | 75 | 76 | function assertValidEncoding(buffer) { 77 | for (var index = 0, length = buffer.length; index < length; index++) { 78 | if (!VALID[buffer[index]]) { 79 | throw new Error(buffer[index] + ' is not a valid symbol or whitespace.'); 80 | } 81 | } 82 | } 83 | 84 | function generateBuffer() { 85 | var bins = [ 86 | 8, 87 | 32, 88 | 1024, 89 | 65536 90 | ]; 91 | var bin = bins[Math.floor(random() * bins.length)]; 92 | var length = Math.ceil(random() * bin); 93 | var buffer = Buffer.alloc(length); 94 | while (length--) buffer[length] = Math.floor(random() * 256); 95 | return buffer; 96 | } 97 | 98 | function generateWhitespace() { 99 | var length = Math.ceil(random() * 1); 100 | var buffer = Buffer.alloc(length); 101 | while (length--) { 102 | buffer[length] = WHITESPACE.charCodeAt( 103 | Math.floor(random() * WHITESPACE.length) 104 | ); 105 | } 106 | return buffer; 107 | } 108 | 109 | function hash(buffer) { 110 | var hash = Node.crypto.createHash('SHA256'); 111 | hash.update(buffer); 112 | return hash.digest('hex').slice(0, 32); 113 | } 114 | 115 | function withCorruption(buffer) { 116 | if (buffer.length === 0) return undefined; 117 | var corrupt = Buffer.from(buffer); 118 | var count = Math.min( 119 | Math.max(1, Math.ceil(random() * 100)), 120 | buffer.length 121 | ); 122 | while (count-- > 0) { 123 | var index = Math.floor(random() * buffer.length); 124 | while (VALID[corrupt[index]]) { 125 | corrupt[index] = Math.floor(random() * 256); 126 | } 127 | } 128 | return corrupt; 129 | } 130 | 131 | function withTransportPadding(buffer) { 132 | var buffers = []; 133 | var index = 0; 134 | var length = buffer.length; 135 | if (buffer[length - 1] === 61 && random() < 0.5) { 136 | length--; 137 | if (buffer[length - 1] === 61 && random() < 0.5) { 138 | length--; 139 | } 140 | } 141 | while (index < length) { 142 | var size = Math.round(random() * (length - index)); 143 | if (random() < 0.1) buffers.push(generateWhitespace()); 144 | buffers.push(buffer.slice(index, index += size)); 145 | if (random() < 0.1) buffers.push(generateWhitespace()); 146 | } 147 | function extraPadding() { 148 | var length = Math.round(random() * 7); 149 | var buffer = Buffer.alloc(length); 150 | while (length-- > 0) buffer[length] = '='.charCodeAt(0); 151 | return buffer; 152 | } 153 | if (random() < 0.2) buffers.push(extraPadding()); 154 | return Buffer.concat(buffers); 155 | } 156 | 157 | function withTruncation(buffer) { 158 | if (buffer.length === 0) return undefined; 159 | var truncated = Buffer.from(buffer); 160 | var symbols = 0; 161 | // Count the actual data symbols (as opposed to whitespace or padding): 162 | for (var index = 0, length = truncated.length; index < length; index++) { 163 | if (VALID[truncated[index]] === 1) symbols++; 164 | } 165 | // How many symbols are in the last quartet? 166 | var last = symbols % 4; 167 | if (last === 0) { 168 | if (symbols === 0) return undefined; 169 | var remove = 3; 170 | } else if (last === 2) { 171 | var remove = 1; 172 | } else if (last === 3) { 173 | var remove = 2; 174 | } else { 175 | throw new Error('Last quartet has unexpected number of symbols: ' + last); 176 | } 177 | var length = truncated.length; 178 | while (length-- > 0 && remove > 0) { 179 | if (VALID[truncated[length]] === 1) { 180 | truncated[length] = 32; // Replace symbol with a space. 181 | remove--; 182 | } 183 | } 184 | if (remove > 0) throw new Error('Failed to remove enough symbols.'); 185 | return truncated; 186 | } 187 | 188 | function Encode(buffer, options) { 189 | var base64 = Buffer.from(buffer.toString('base64'), 'ascii'); 190 | if (options.wrap) { 191 | var buffers = []; 192 | var index = 0; 193 | var length = base64.length; 194 | while (index < length) { 195 | buffers.push(base64.slice(index, index += 76)); 196 | buffers.push(Buffer.from('\r\n', 'ascii')); 197 | } 198 | base64 = Buffer.concat(buffers); 199 | base64 = Buffer.from(base64.toString('ascii').trim(), 'ascii'); 200 | } 201 | return base64; 202 | } 203 | 204 | var sources = []; 205 | var length = 1000; 206 | while (length--) sources.push(generateBuffer()); 207 | 208 | var Base64 = require('./index.js'); 209 | var bindingNames = [ 210 | 'javascript' 211 | ]; 212 | if (Base64.binding.native) bindingNames.push('native'); 213 | 214 | var empty = Buffer.alloc(0); 215 | var exceptions = { 216 | decode: [ 217 | { 218 | args: [[], empty, 0], 219 | error: 'source must be a buffer' 220 | }, 221 | { 222 | args: [empty, [], 0], 223 | error: 'target must be a buffer' 224 | }, 225 | { 226 | args: [ 227 | Buffer.alloc(4), 228 | Buffer.alloc(3 - 1), 229 | 0 230 | ], 231 | error: 'target too small' 232 | }, 233 | { 234 | args: [empty, empty, '0'], 235 | error: 'flags must be an integer' 236 | }, 237 | { 238 | args: [empty, empty, -1], 239 | error: 'flags must be an integer' 240 | }, 241 | { 242 | args: [empty, empty, 1.5], 243 | error: 'flags must be an integer' 244 | } 245 | ], 246 | encode: [ 247 | { 248 | args: [[], empty, 0], 249 | error: 'source must be a buffer' 250 | }, 251 | { 252 | args: [empty, [], 0], 253 | error: 'target must be a buffer' 254 | }, 255 | { 256 | args: [ 257 | Buffer.alloc(3), 258 | Buffer.alloc(4 - 1), 259 | 0 260 | ], 261 | error: 'target too small' 262 | }, 263 | { 264 | args: [empty, empty, '0'], 265 | error: 'flags must be an integer' 266 | }, 267 | { 268 | args: [empty, empty, -1], 269 | error: 'flags must be an integer' 270 | }, 271 | { 272 | args: [empty, empty, 1.5], 273 | error: 'flags must be an integer' 274 | } 275 | ] 276 | }; 277 | 278 | bindingNames.forEach( 279 | function(bindingName) { 280 | var binding = Base64.binding[bindingName]; 281 | Test.equal(bindingName, bindingName, namespace, 'binding'); 282 | Object.keys(exceptions).forEach( 283 | function(method) { 284 | exceptions[method].forEach( 285 | function(test) { 286 | try { 287 | binding[method].apply(binding, test.args); 288 | Test.equal('', test.error, namespace, method + ' exception'); 289 | } catch (error) { 290 | Test.equal( 291 | error.message, 292 | test.error, 293 | namespace, 294 | method + ' exception' 295 | ); 296 | } 297 | } 298 | ); 299 | } 300 | ); 301 | sources.forEach( 302 | function(source) { 303 | try { 304 | Test.equal(bindingName, bindingName, namespace, 'binding'); 305 | var options = { binding: binding }; 306 | if (random() < 0.8) { 307 | options.wrap = random() < 0.5; 308 | } 309 | Test.equal(options.wrap, options.wrap, namespace, 'options.wrap'); 310 | Test.equal(source.length, source.length, namespace, 'source.length'); 311 | var sourceHash = hash(source); 312 | var encoding = Base64.encode(source, options); 313 | Test.equal( 314 | hash(encoding), 315 | hash(Encode(source, options)), 316 | namespace, 317 | 'standard implementation' 318 | ); 319 | Test.equal( 320 | hash(source) === sourceHash, 321 | true, 322 | namespace, 323 | 'source unchanged by encode()' 324 | ); 325 | var encodingHash = hash(encoding); 326 | Test.equal( 327 | encoding.length, 328 | encoding.length, 329 | namespace, 330 | 'encoding.length' 331 | ); 332 | Test.equal( 333 | assertValidEncoding(encoding) === undefined, 334 | true, 335 | namespace, 336 | 'valid encoding' 337 | ); 338 | if (hash(encoding) !== encodingHash) { 339 | Test.equal( 340 | hash(encoding) === encodingHash, 341 | true, 342 | namespace, 343 | 'encoding unchanged by assertValidEncoding()' 344 | ); 345 | } 346 | var decoding = Base64.decode(encoding, options); 347 | Test.equal( 348 | hash(encoding) === encodingHash, 349 | true, 350 | namespace, 351 | 'encoding unchanged by decode()' 352 | ); 353 | Test.equal( 354 | decoding.length, 355 | source.length, 356 | namespace, 357 | 'decoding.length' 358 | ); 359 | Test.equal(hash(decoding), sourceHash, namespace, 'decoding'); 360 | var encodingPadding = withTransportPadding(encoding); 361 | if (hash(encoding) !== encodingHash) { 362 | Test.equal( 363 | hash(encoding) === encodingHash, 364 | true, 365 | namespace, 366 | 'encoding unchanged by withTransportPadding()' 367 | ); 368 | } 369 | Test.equal( 370 | encodingPadding.length, 371 | encodingPadding.length, 372 | namespace, 373 | 'encodingPadding.length' 374 | ); 375 | var decodingPadding = Base64.decode(encodingPadding, options); 376 | Test.equal( 377 | decodingPadding.length, 378 | source.length, 379 | namespace, 380 | 'decodingPadding.length' 381 | ); 382 | Test.equal( 383 | hash(decodingPadding), 384 | sourceHash, 385 | namespace, 386 | 'decodingPadding' 387 | ); 388 | var corrupt = withCorruption(encodingPadding); 389 | if (corrupt) { 390 | var corruptException; 391 | try { 392 | Base64.decode(corrupt, options); 393 | } catch (exception) { 394 | corruptException = exception.message; 395 | } 396 | Test.equal( 397 | corruptException, 398 | 'source is corrupt', 399 | namespace, 400 | 'corrupt exception' 401 | ); 402 | } 403 | var truncated = withTruncation(encodingPadding); 404 | if (truncated) { 405 | var truncatedException; 406 | try { 407 | Base64.decode(truncated, options); 408 | } catch (exception) { 409 | truncatedException = exception.message; 410 | } 411 | Test.equal( 412 | truncatedException, 413 | 'source is truncated', 414 | namespace, 415 | 'truncated exception' 416 | ); 417 | } 418 | } catch (error) { 419 | if (source) { 420 | console.log( 421 | ' Source: ' + JSON.stringify(source.toString('binary')) 422 | ); 423 | } 424 | if (decoding) { 425 | console.log(''); 426 | console.log( 427 | 'Decoding: ' + JSON.stringify(decoding.toString('binary')) 428 | ); 429 | } 430 | if (decodingPadding) { 431 | console.log(''); 432 | console.log( 433 | 'DPadding: ' + JSON.stringify(decodingPadding.toString('binary')) 434 | ); 435 | } 436 | if (encoding) { 437 | console.log(''); 438 | console.log( 439 | 'Encoding: ' + JSON.stringify(encoding.toString('binary')) 440 | ); 441 | } 442 | if (encodingPadding) { 443 | console.log(''); 444 | console.log( 445 | 'EPadding: ' + JSON.stringify(encodingPadding.toString('binary')) 446 | ); 447 | } 448 | throw error; 449 | } 450 | } 451 | ); 452 | } 453 | ); 454 | console.log('Bindings Tested: ' + bindingNames.join(', ')); 455 | console.log('================'); 456 | console.log('PASSED ALL TESTS'); 457 | console.log('================'); 458 | --------------------------------------------------------------------------------