diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ccfcfe --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# uint8-base64 + +[![NPM version][npm-image]][npm-url] +[![build status][ci-image]][ci-url] +[![Test coverage][codecov-image]][codecov-url] +[![npm download][download-image]][download-url] + +You can find a lot of NPM libraries dealing with base64 encoding and decoding. + +However we could not find one that would have as input AND output an Uint8Array. This library does exactly this. + +This library is pretty fast and will convert over 500 Mb per second in nodejs as well as in the browser. + +## Installation + +`$ npm i uint8-base64` + +## Usage + +```js +import { encode } from 'uint8-base64'; + +const result = myModule(args); +// result is ... +``` + +## License + +The code was largely inspired by: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + +[MIT](./LICENSE) + +[npm-image]: https://img.shields.io/npm/v/uint8-base64.svg +[npm-url]: https://www.npmjs.com/package/uint8-base64 +[ci-image]: https://github.com/cheminfo/uint8-base64/workflows/Node.js%20CI/badge.svg?branch=main +[ci-url]: https://github.com/cheminfo/uint8-base64/actions?query=workflow%3A%22Node.js+CI%22 +[codecov-image]: https://img.shields.io/codecov/c/github/cheminfo/uint8-base64.svg +[codecov-url]: https://codecov.io/gh/cheminfo/uint8-base64 +[download-image]: https://img.shields.io/npm/dm/uint8-base64.svg +[download-url]: https://www.npmjs.com/package/uint8-base64 diff --git a/benchmark/big.js b/benchmark/big.js new file mode 100644 index 0000000..51e3f71 --- /dev/null +++ b/benchmark/big.js @@ -0,0 +1,56 @@ +'use strict'; + +const { Buffer } = require('buffer'); + +const { decode, encode } = require('../lib/'); + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +let string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIOQRSTUVWXYZ'; +for (let i = 0; i < 20; i++) { + string += string; +} + +const uint8 = textEncoder.encode(string); +const buffer = Buffer.from(string, 'utf8'); + +console.time('btoa'); +const btoaString = btoa(string); +console.timeEnd('btoa'); + +console.time('atob'); +const atobString = atob(btoaString); +console.timeEnd('atob'); + +console.time('buffer.toString'); +const bufferToString = buffer.toString('base64'); +console.timeEnd('buffer.toString'); + +console.time('Buffer.from'); +const bufferFrom = Buffer.from(bufferToString, 'base64'); +console.timeEnd('Buffer.from'); + +console.time('encode'); +const bufferBase64 = encode(uint8); +console.timeEnd('encode'); + +console.time('decode'); +const newBuffer = decode(bufferBase64); +console.timeEnd('decode'); + +const newString = textDecoder.decode(newBuffer); + +console.log( + string.length, + uint8.length, + atobString.length, + bufferFrom.length, + bufferBase64.length, + bufferToString.length, + btoaString.length, + btoaString === textDecoder.decode(bufferBase64), + newString === string, + newString === atobString, + newString === textDecoder.decode(bufferFrom), +); diff --git a/package.json b/package.json index b2f76f8..cbc2dd5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "base64-tools", + "name": "uint8-base64", "version": "0.0.0", - "description": "Encode and decode base64 from and to uint8 and arraybuffer", + "description": "Encode and decode base64 from and to Uint8Array", "main": "./lib/index.js", "module": "./lib-esm/index.js", "types": "./lib/index.d.ts", @@ -30,24 +30,28 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/cheminfo/base64-tools.git" + "url": "git+https://github.com/cheminfo/uint8-base64.git" }, "bugs": { - "url": "https://github.com/cheminfo/base64-tools/issues" + "url": "https://github.com/cheminfo/uint8-base64/issues" }, - "homepage": "https://github.com/cheminfo/base64-tools#readme", + "homepage": "https://github.com/cheminfo/uint8-base64#readme", "jest": { "preset": "ts-jest", - "testEnvironment": "node" + "testEnvironment": "node", + "testPathIgnorePatterns": [ + "node_modules", + "data.ts" + ] }, "devDependencies": { "@types/jest": "^27.0.1", "eslint": "^7.32.0", "eslint-config-cheminfo-typescript": "^8.0.9", - "jest": "^27.0.6", + "jest": "^27.1.0", "prettier": "^2.3.2", "rimraf": "^3.0.2", - "ts-jest": "^27.0.3", - "typescript": "^4.3.5" + "ts-jest": "^27.0.5", + "typescript": "^4.4.2" } } diff --git a/src/__tests__/data.ts b/src/__tests__/data.ts new file mode 100644 index 0000000..bf92847 --- /dev/null +++ b/src/__tests__/data.ts @@ -0,0 +1,15 @@ +export const tests = [ + // ['', ''], + // ['TWFu', 'Man'], + ['QQ==', 'A'], + // ['SGVsbG8gd29ybGQ=', 'Hello world'], + //['SGVsbG8gd29ybGRzIQ==', 'Hello worlds!'], +]; + +export const allBytes = new Uint8Array(256); +for (let i = 0; i < 256; i++) { + allBytes[i] = i; +} + +export const base64AllBytes = + 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=='; diff --git a/src/__tests__/decode.test.ts b/src/__tests__/decode.test.ts new file mode 100644 index 0000000..0f1b098 --- /dev/null +++ b/src/__tests__/decode.test.ts @@ -0,0 +1,22 @@ +import { decode } from '..'; + +import { tests, allBytes, base64AllBytes } from './data'; + +const textEncoder = new TextEncoder(); + +describe('decode', () => { + it.each(tests)('%s -> %s', (base64: string, binary: string) => { + const encodedBase64 = textEncoder.encode(base64); + const encodedBinary = textEncoder.encode(binary); + expect(Array.from(decode(encodedBase64))).toStrictEqual( + Array.from(encodedBinary), + ); + }); + + it('All possibles values', () => { + const encodeBase64 = textEncoder.encode(base64AllBytes); + expect(Array.from(decode(encodeBase64))).toStrictEqual( + Array.from(allBytes), + ); + }); +}); diff --git a/src/__tests__/encode.test.ts b/src/__tests__/encode.test.ts new file mode 100644 index 0000000..1c83b37 --- /dev/null +++ b/src/__tests__/encode.test.ts @@ -0,0 +1,22 @@ +import { encode } from '..'; + +import { tests, allBytes, base64AllBytes } from './data'; + +const textEncoder = new TextEncoder(); + +describe('encode', () => { + it.each(tests)('%s -> %s', (base64: string, binary: string) => { + const encodedBinary = textEncoder.encode(binary); + const encodedBase64 = textEncoder.encode(base64); + expect(Array.from(encode(encodedBinary))).toStrictEqual( + Array.from(encodedBase64), + ); + }); + + it('All possibles values', () => { + const encodedBase64 = textEncoder.encode(base64AllBytes); + expect(Array.from(encode(allBytes))).toStrictEqual( + Array.from(encodedBase64), + ); + }); +}); diff --git a/src/decode.ts b/src/decode.ts new file mode 100644 index 0000000..eeac136 --- /dev/null +++ b/src/decode.ts @@ -0,0 +1,44 @@ +const base64codes = Uint8Array.from([ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, + 255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255, + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, +]); + +/** + * Convert a Uint8Array containing a base64 encoded bytes to a Uint8Array containing decoded values + * @returns a Uint8Array containing the decoded bytes + */ + +export function decode( + input: Uint8Array, //| ArrayBuffer, +): Uint8Array { + if (!ArrayBuffer.isView(input)) { + input = new Uint8Array(input); + } + + if (input.length % 4 !== 0) { + throw new Error('Unable to parse base64 string.'); + } + + let output = new Uint8Array(3 * (input.length / 4)); + if (input.length === 0) return output; + + const missingOctets = + input[input.length - 2] === 61 ? 2 : input[input.length - 1] === 61 ? 1 : 0; + + for (let i = 0, j = 0; i < input.length; i += 4, j += 3) { + const buffer = + (base64codes[input[i]] << 18) | + (base64codes[input[i + 1]] << 12) | + (base64codes[input[i + 2]] << 6) | + base64codes[input[i + 3]]; + output[j] = buffer >> 16; + output[j + 1] = (buffer >> 8) & 0xff; + output[j + 2] = buffer & 0xff; + } + return output.subarray(0, output.length - missingOctets); +} diff --git a/src/encode.ts b/src/encode.ts new file mode 100644 index 0000000..73a584c --- /dev/null +++ b/src/encode.ts @@ -0,0 +1,39 @@ +const base64codes = Uint8Array.from([ + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47, +]); + +/** + * Convert a Uint8Array containing bytes to a Uint8Array containing the base64 encoded values + * @returns a Uint8Array containing the encoded bytes + */ + +export function encode(input: Uint8Array): Uint8Array { + const output = new Uint8Array(Math.ceil(input.length / 3) * 4); + let i, j; + for (i = 2, j = 0; i < input.length; i += 3, j += 4) { + output[j] = base64codes[input[i - 2] >> 2]; + output[j + 1] = + base64codes[((input[i - 2] & 0x03) << 4) | (input[i - 1] >> 4)]; + output[j + 2] = base64codes[((input[i - 1] & 0x0f) << 2) | (input[i] >> 6)]; + output[j + 3] = base64codes[input[i] & 0x3f]; + } + if (i === input.length + 1) { + // 1 octet yet to write + output[j] = base64codes[input[i - 2] >> 2]; + output[j + 1] = base64codes[(input[i - 2] & 0x03) << 4]; + output[j + 2] = 61; + output[j + 3] = 61; + } + if (i === input.length) { + // 2 octets yet to write + output[j] = base64codes[input[i - 2] >> 2]; + output[j + 1] = + base64codes[((input[i - 2] & 0x03) << 4) | (input[i - 1] >> 4)]; + output[j + 2] = base64codes[(input[i - 1] & 0x0f) << 2]; + output[j + 3] = 61; + } + return output; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6267e16 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './decode'; +export * from './encode';