diff --git a/.changeset/lucky-items-lay.md b/.changeset/lucky-items-lay.md new file mode 100644 index 00000000..693c35ca --- /dev/null +++ b/.changeset/lucky-items-lay.md @@ -0,0 +1,16 @@ +--- +'data-uri-to-buffer': major +--- + +Refactor to return an `ArrayBuffer` instead of a Node.js `Buffer`. + +This change is being made to make the package platform-agnostic, and work in web browsers or other non-Node.js environments without polyfills. + +For Node.js users of this package, you can get a Node.js `Buffer` instance from an `ArrayBuffer` like so: + +```typescript +const uri = 'data:,Hello%2C%20World!'; +const parsed = dataUriToBuffer(uri); +const buffer = Buffer.from(parsed.buffer); +// `buffer` is a Node.js Buffer +``` diff --git a/packages/data-uri-to-buffer/README.md b/packages/data-uri-to-buffer/README.md index 0d118721..f5c2ba79 100644 --- a/packages/data-uri-to-buffer/README.md +++ b/packages/data-uri-to-buffer/README.md @@ -1,26 +1,29 @@ data-uri-to-buffer ================== -### Generate a Buffer instance from a [Data URI][rfc] string +### Create an ArrayBuffer instance from a [Data URI][rfc] string -This module accepts a ["data" URI][rfc] String of data, and returns a -node.js `Buffer` instance with the decoded data. +This module accepts a ["data" URI][rfc] String of data, and returns +an `ArrayBuffer` instance with the decoded data. + +This module is intended to work on a large variety of JavaScript +runtimes, including Node.js and web browsers. Example ------- -``` js +```typescript import { dataUriToBuffer } from 'data-uri-to-buffer'; // plain-text data is supported let uri = 'data:,Hello%2C%20World!'; -let decoded = dataUriToBuffer(uri); -console.log(decoded.toString()); +let parsed = dataUriToBuffer(uri); +console.log(new TextDecoder().decode(parsed.buffer)); // 'Hello, World!' // base64-encoded data is supported uri = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D'; -decoded = dataUriToBuffer(uri); -console.log(decoded.toString()); +parsed = dataUriToBuffer(uri); +console.log(new TextDecoder().decode(parsed.buffer)); // 'Hello, World!' ``` @@ -28,21 +31,30 @@ console.log(decoded.toString()); API --- -### dataUriToBuffer(String uri) → Buffer +```typescript +export interface ParsedDataURI { + type: string; + typeFull: string; + charset: string; + buffer: ArrayBuffer; +} +``` + +### dataUriToBuffer(uri: string | URL) → ParsedDataURI -The `type` property on the Buffer instance gets set to the main type portion of +The `type` property gets set to the main type portion of the "mediatype" portion of the "data" URI, or defaults to `"text/plain"` if not specified. -The `typeFull` property on the Buffer instance gets set to the entire +The `typeFull` property gets set to the entire "mediatype" portion of the "data" URI (including all parameters), or defaults to `"text/plain;charset=US-ASCII"` if not specified. -The `charset` property on the Buffer instance gets set to the Charset portion of +The `charset` property gets set to the Charset portion of the "mediatype" portion of the "data" URI, or defaults to `"US-ASCII"` if the entire type is not specified, or defaults to `""` otherwise. -*Note*: If the only the main type is specified but not the charset, e.g. +*Note*: If only the main type is specified but not the charset, e.g. `"data:text/plain,abc"`, the charset is set to the empty string. The spec only defaults to US-ASCII as charset if the entire type is not specified. diff --git a/packages/data-uri-to-buffer/package.json b/packages/data-uri-to-buffer/package.json index 9fa9d442..6a63b2fc 100644 --- a/packages/data-uri-to-buffer/package.json +++ b/packages/data-uri-to-buffer/package.json @@ -1,7 +1,7 @@ { "name": "data-uri-to-buffer", "version": "5.0.1", - "description": "Generate a Buffer instance from a Data URI string", + "description": "Create an ArrayBuffer instance from a Data URI string", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ diff --git a/packages/data-uri-to-buffer/src/index.ts b/packages/data-uri-to-buffer/src/index.ts index a43096c9..da586793 100644 --- a/packages/data-uri-to-buffer/src/index.ts +++ b/packages/data-uri-to-buffer/src/index.ts @@ -1,17 +1,66 @@ -export interface MimeBuffer extends Buffer { +export interface ParsedDataURI { type: string; typeFull: string; charset: string; + buffer: ArrayBuffer; +} + +function base64ToArrayBuffer(base64: string) { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const bytes = []; + + for (let i = 0; i < base64.length; i += 4) { + const idx0 = chars.indexOf(base64.charAt(i)); + const idx1 = chars.indexOf(base64.charAt(i + 1)); + const idx2 = + base64.charAt(i + 2) === '=' + ? 0 + : chars.indexOf(base64.charAt(i + 2)); + const idx3 = + base64.charAt(i + 3) === '=' + ? 0 + : chars.indexOf(base64.charAt(i + 3)); + + const bin0 = (idx0 << 2) | (idx1 >> 4); + const bin1 = ((idx1 & 15) << 4) | (idx2 >> 2); + const bin2 = ((idx2 & 3) << 6) | idx3; + + bytes.push(bin0); + if (base64.charAt(i + 2) !== '=') bytes.push(bin1); + if (base64.charAt(i + 3) !== '=') bytes.push(bin2); + } + + const buffer = new ArrayBuffer(bytes.length); + const view = new Uint8Array(buffer); + view.set(bytes); + return buffer; +} + +function stringToBuffer(str: string): ArrayBuffer { + // Create a buffer with length equal to the string length + const buffer = new ArrayBuffer(str.length); + + // Create a view to manipulate the buffer content + const view = new Uint8Array(buffer); + + // Iterate over the string and populate the buffer with ASCII codes + for (let i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + + return buffer; } /** * Returns a `Buffer` instance from the given data URI `uri`. * * @param {String} uri Data URI to turn into a Buffer instance - * @returns {Buffer} Buffer instance from Data URI - * @api public */ -export function dataUriToBuffer(uri: string): MimeBuffer { +export function dataUriToBuffer(uri: string | URL): ParsedDataURI { + uri = String(uri); + if (!/^data:/i.test(uri)) { throw new TypeError( '`uri` does not appear to be a Data URI (must begin with "data:")' @@ -51,18 +100,13 @@ export function dataUriToBuffer(uri: string): MimeBuffer { } // get the encoded data portion and decode URI-encoded chars - const encoding = base64 ? 'base64' : 'ascii'; const data = unescape(uri.substring(firstComma + 1)); - const buffer = Buffer.from(data, encoding) as MimeBuffer; - - // set `.type` and `.typeFull` properties to MIME type - buffer.type = type; - buffer.typeFull = typeFull; - - // set the `.charset` property - buffer.charset = charset; + const buffer = base64 ? base64ToArrayBuffer(data) : stringToBuffer(data); - return buffer; + return { + type, + typeFull, + charset, + buffer, + }; } - -export default dataUriToBuffer; diff --git a/packages/data-uri-to-buffer/test/data-uri-to-buffer.test.ts b/packages/data-uri-to-buffer/test/data-uri-to-buffer.test.ts index 7fcc2ae1..02663459 100644 --- a/packages/data-uri-to-buffer/test/data-uri-to-buffer.test.ts +++ b/packages/data-uri-to-buffer/test/data-uri-to-buffer.test.ts @@ -5,19 +5,19 @@ describe('data-uri-to-buffer', function () { it('should decode bare-bones Data URIs', function () { const uri = 'data:,Hello%2C%20World!'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('text/plain;charset=US-ASCII', buf.typeFull); - assert.equal('US-ASCII', buf.charset); - assert.equal('Hello, World!', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('text/plain;charset=US-ASCII', parsed.typeFull); + assert.equal('US-ASCII', parsed.charset); + assert.equal('Hello, World!', Buffer.from(parsed.buffer).toString()); }); it('should decode bare-bones "base64" Data URIs', function () { const uri = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('Hello, World!', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('Hello, World!', Buffer.from(parsed.buffer).toString()); }); it('should decode plain-text Data URIs', function () { @@ -31,10 +31,10 @@ describe('data-uri-to-buffer', function () { // Escape the HTML for URL formatting const uri = 'data:text/html;charset=utf-8,' + encodeURIComponent(html); - const buf = dataUriToBuffer(uri); - assert.equal('text/html', buf.type); - assert.equal('utf-8', buf.charset); - assert.equal(html, buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/html', parsed.type); + assert.equal('utf-8', parsed.charset); + assert.equal(html, Buffer.from(parsed.buffer).toString()); }); // the next 4 tests are from: @@ -43,9 +43,10 @@ describe('data-uri-to-buffer', function () { it('should decode "ISO-8859-8 in Base64" URIs', function () { const uri = 'data:text/plain;charset=iso-8859-8-i;base64,+ezl7Q=='; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('iso-8859-8-i', buf.charset); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('iso-8859-8-i', parsed.charset); + const buf = new Uint8Array(parsed.buffer); assert.equal(4, buf.length); assert.equal(0xf9, buf[0]); assert.equal(0xec, buf[1]); @@ -56,9 +57,10 @@ describe('data-uri-to-buffer', function () { it('should decode "ISO-8859-8 in URL-encoding" URIs', function () { const uri = 'data:text/plain;charset=iso-8859-8-i,%f9%ec%e5%ed'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('iso-8859-8-i', buf.charset); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('iso-8859-8-i', parsed.charset); + const buf = new Uint8Array(parsed.buffer); assert.equal(4, buf.length); assert.equal(0xf9, buf[0]); assert.equal(0xec, buf[1]); @@ -69,9 +71,10 @@ describe('data-uri-to-buffer', function () { it('should decode "UTF-8 in Base64" URIs', function () { const uri = 'data:text/plain;charset=UTF-8;base64,16nXnNeV150='; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('UTF-8', buf.charset); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('UTF-8', parsed.charset); + const buf = Buffer.from(parsed.buffer); assert.equal(8, buf.length); assert.equal('שלום', buf.toString('utf8')); }); @@ -79,9 +82,10 @@ describe('data-uri-to-buffer', function () { it('should decode "UTF-8 in URL-encoding" URIs', function () { const uri = 'data:text/plain;charset=UTF-8,%d7%a9%d7%9c%d7%95%d7%9d'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('UTF-8', buf.charset); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('UTF-8', parsed.charset); + const buf = Buffer.from(parsed.buffer); assert.equal(8, buf.length); assert.equal('שלום', buf.toString('utf8')); }); @@ -94,8 +98,9 @@ describe('data-uri-to-buffer', function () { 'AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n' + '9TXL0Y4OHwAAAABJRU5ErkJggg=='; - const buf = dataUriToBuffer(uri); - assert.equal('image/png', buf.type); + const parsed = dataUriToBuffer(uri); + assert.equal('image/png', parsed.type); + const buf = Buffer.from(parsed.buffer); assert.equal( 'iVBORw0KGgoAAAANSUhEUgAAAAUA' + 'AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO' + @@ -107,79 +112,79 @@ describe('data-uri-to-buffer', function () { it('should decode a plain-text URI with a space character in it', function () { const uri = 'data:,foo bar'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('foo bar', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('foo bar', Buffer.from(parsed.buffer).toString()); }); it('should decode data with a "," comma char', function () { const uri = 'data:,a,b'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('a,b', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('a,b', Buffer.from(parsed.buffer).toString()); }); it('should decode data with traditionally reserved characters like ";"', function () { const uri = 'data:,;test'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal(';test', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal(';test', Buffer.from(parsed.buffer).toString()); }); it('should not default to US-ASCII if main type is provided', function () { const uri = 'data:text/plain,abc'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('text/plain', buf.typeFull); - assert.equal('', buf.charset); - assert.equal('abc', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('text/plain', parsed.typeFull); + assert.equal('', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); it('should default to text/plain if main type is not provided', function () { const uri = 'data:;charset=UTF-8,abc'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('text/plain;charset=UTF-8', buf.typeFull); - assert.equal('UTF-8', buf.charset); - assert.equal('abc', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('text/plain;charset=UTF-8', parsed.typeFull); + assert.equal('UTF-8', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); it('should not allow charset without a leading ;', function () { const uri = 'data:charset=UTF-8,abc'; - const buf = dataUriToBuffer(uri); - assert.equal('charset=UTF-8', buf.type); - assert.equal('charset=UTF-8', buf.typeFull); - assert.equal('', buf.charset); - assert.equal('abc', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('charset=UTF-8', parsed.type); + assert.equal('charset=UTF-8', parsed.typeFull); + assert.equal('', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); it('should allow custom media type parameters', function () { const uri = 'data:application/javascript;version=1.8;charset=UTF-8,abc'; - const buf = dataUriToBuffer(uri); - assert.equal('application/javascript', buf.type); + const parsed = dataUriToBuffer(uri); + assert.equal('application/javascript', parsed.type); assert.equal( 'application/javascript;version=1.8;charset=UTF-8', - buf.typeFull + parsed.typeFull ); - assert.equal('UTF-8', buf.charset); - assert.equal('abc', buf.toString()); + assert.equal('UTF-8', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); it('should allow base64 annotation anywhere', function () { const uri = 'data:text/plain;base64;charset=UTF-8,YWJj'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('text/plain;charset=UTF-8', buf.typeFull); - assert.equal('UTF-8', buf.charset); - assert.equal('abc', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('text/plain;charset=UTF-8', parsed.typeFull); + assert.equal('UTF-8', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); it('should parse meta with unnecessary semicolons', function () { const uri = 'data:text/plain;;charset=UTF-8;;;base64;;;;,YWJj'; - const buf = dataUriToBuffer(uri); - assert.equal('text/plain', buf.type); - assert.equal('text/plain;charset=UTF-8', buf.typeFull); - assert.equal('UTF-8', buf.charset); - assert.equal('abc', buf.toString()); + const parsed = dataUriToBuffer(uri); + assert.equal('text/plain', parsed.type); + assert.equal('text/plain;charset=UTF-8', parsed.typeFull); + assert.equal('UTF-8', parsed.charset); + assert.equal('abc', Buffer.from(parsed.buffer).toString()); }); }); diff --git a/packages/get-uri/src/data.ts b/packages/get-uri/src/data.ts index f4dbf1ae..1ffcbaeb 100644 --- a/packages/get-uri/src/data.ts +++ b/packages/get-uri/src/data.ts @@ -1,7 +1,7 @@ import createDebug from 'debug'; import { Readable } from 'stream'; import { createHash } from 'crypto'; -import dataUriToBuffer from 'data-uri-to-buffer'; +import { dataUriToBuffer } from 'data-uri-to-buffer'; import { GetUriProtocol } from './'; import NotModifiedError from './notmodified'; @@ -42,7 +42,7 @@ export const data: GetUriProtocol = async ( throw new NotModifiedError(); } else { debug('creating Readable stream from "data:" URI buffer'); - const buf = dataUriToBuffer(uri); - return new DataReadable(hash, buf); + const { buffer } = dataUriToBuffer(uri); + return new DataReadable(hash, Buffer.from(buffer)); } };