Skip to content

Commit

Permalink
url,buffer: implement URL.createObjectURL
Browse files Browse the repository at this point in the history
Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: #39693
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
  • Loading branch information
jasnell authored and danielleadams committed Aug 16, 2021
1 parent 53cf53c commit 37dda19
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 36 deletions.
14 changes: 14 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4948,6 +4948,20 @@ added: v3.0.0

An alias for [`buffer.constants.MAX_STRING_LENGTH`][].

### `buffer.resolveObjectURL(id)`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
`URL.createObjectURL()`.
* Returns: {Blob}

Resolves a `'blob:nodedata:...'` an associated {Blob} object registered using
a prior call to `URL.createObjectURL()`.

### `buffer.transcode(source, fromEnc, toEnc)`
<!-- YAML
added: v7.1.0
Expand Down
47 changes: 47 additions & 0 deletions doc/api/url.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,53 @@ console.log(JSON.stringify(myURLs));
// Prints ["https://www.example.com/","https://test.example.org/"]
```

#### `URL.createObjectURL(blob)`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `blob` {Blob}
* Returns: {string}

Creates a `'blob:nodedata:...'` URL string that represents the given {Blob}
object and can be used to retrieve the `Blob` later.

```js
const {
Blob,
resolveObjectURL,
} = require('buffer');

const blob = new Blob(['hello']);
const id = URL.createObjectURL(blob);

// later...

const otherBlob = resolveObjectURL(id);
console.log(otherBlob.size);
```

The data stored by the registered {Blob} will be retained in memory until
`URL.revokeObjectURL()` is called to remove it.

`Blob` objects are registered within the current thread. If using Worker
Threads, `Blob` objects registered within one Worker will not be available
to other workers or the main thread.

#### `URL.revokeObjectURL(id)`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
`URL.createObjectURL()`.

Removes the stored {Blob} identified by the given ID.

### Class: `URLSearchParams`
<!-- YAML
added:
Expand Down
2 changes: 2 additions & 0 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const {

const {
Blob,
resolveObjectURL,
} = require('internal/blob');

FastBuffer.prototype.constructor = Buffer;
Expand Down Expand Up @@ -1239,6 +1240,7 @@ function atob(input) {

module.exports = {
Blob,
resolveObjectURL,
Buffer,
SlowBuffer,
transcode,
Expand Down
94 changes: 77 additions & 17 deletions lib/internal/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const {
ObjectDefineProperty,
PromiseResolve,
PromiseReject,
PromisePrototypeFinally,
SafePromisePrototypeFinally,
ReflectConstruct,
RegExpPrototypeTest,
StringPrototypeToLowerCase,
StringPrototypeSplit,
Symbol,
SymbolIterator,
SymbolToStringTag,
Expand All @@ -20,7 +21,8 @@ const {
const {
createBlob: _createBlob,
FixedSizeBlobCopyJob,
} = internalBinding('buffer');
getDataObject,
} = internalBinding('blob');

const { TextDecoder } = require('internal/encoding');

Expand Down Expand Up @@ -57,26 +59,37 @@ const {
} = require('internal/validators');

const kHandle = Symbol('kHandle');
const kState = Symbol('kState');
const kType = Symbol('kType');
const kLength = Symbol('kLength');
const kArrayBufferPromise = Symbol('kArrayBufferPromise');

const kMaxChunkSize = 65536;

const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;

let Buffer;
let ReadableStream;
let URL;


// Yes, lazy loading is annoying but because of circular
// references between the url, internal/blob, and buffer
// modules, lazy loading here makes sure that things work.

function lazyURL(id) {
URL ??= require('internal/url').URL;
return new URL(id);
}

function lazyBuffer() {
if (Buffer === undefined)
Buffer = require('buffer').Buffer;
Buffer ??= require('buffer').Buffer;
return Buffer;
}

function lazyReadableStream(options) {
if (ReadableStream === undefined) {
ReadableStream =
require('internal/webstreams/readablestream').ReadableStream;
}
ReadableStream ??=
require('internal/webstreams/readablestream').ReadableStream;
return new ReadableStream(options);
}

Expand Down Expand Up @@ -232,9 +245,9 @@ class Blob {
return PromiseReject(new ERR_INVALID_THIS('Blob'));

// If there's already a promise in flight for the content,
// reuse it, but only once. After the cached promise resolves
// it will be cleared, allowing it to be garbage collected
// as soon as possible.
// reuse it, but only while it's in flight. After the cached
// promise resolves it will be cleared, allowing it to be
// garbage collected as soon as possible.
if (this[kArrayBufferPromise])
return this[kArrayBufferPromise];

Expand All @@ -260,15 +273,14 @@ class Blob {
resolve(ab);
};
this[kArrayBufferPromise] =
PromisePrototypeFinally(
SafePromisePrototypeFinally(
promise,
() => this[kArrayBufferPromise] = undefined);

return this[kArrayBufferPromise];
}

/**
*
* @returns {Promise<string>}
*/
async text() {
Expand All @@ -288,10 +300,20 @@ class Blob {

const self = this;
return new lazyReadableStream({
async start(controller) {
const ab = await self.arrayBuffer();
controller.enqueue(new Uint8Array(ab));
controller.close();
async start() {
this[kState] = await self.arrayBuffer();
},

pull(controller) {
if (this[kState].byteLength <= kMaxChunkSize) {
controller.enqueue(new Uint8Array(this[kState]));
controller.close();
this[kState] = undefined;
} else {
const slice = this[kState].slice(0, kMaxChunkSize);
this[kState] = this[kState].slice(kMaxChunkSize);
controller.enqueue(new Uint8Array(slice));
}
}
});
}
Expand All @@ -315,9 +337,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
value: 'Blob',
});

function resolveObjectURL(url) {
url = `${url}`;
try {
const parsed = new lazyURL(url);

const split = StringPrototypeSplit(parsed.pathname, ':');

if (split.length !== 2)
return;

const {
0: base,
1: id,
} = split;

if (base !== 'nodedata')
return;

const ret = getDataObject(id);

if (ret === undefined)
return;

const {
0: handle,
1: length,
2: type,
} = ret;

if (handle !== undefined)
return createBlob(handle, length, type);
} catch {
// If there's an error, it's ignored and nothing is returned
}
}

module.exports = {
Blob,
ClonedBlob,
createBlob,
isBlob,
kHandle,
resolveObjectURL,
};
79 changes: 68 additions & 11 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@ const {

const { getConstructorOf, removeColors } = require('internal/util');
const {
ERR_ARG_NOT_ITERABLE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_FILE_URL_HOST,
ERR_INVALID_FILE_URL_PATH,
ERR_INVALID_THIS,
ERR_INVALID_TUPLE,
ERR_INVALID_URL,
ERR_INVALID_URL_SCHEME,
ERR_MISSING_ARGS
} = require('internal/errors').codes;
codes: {
ERR_ARG_NOT_ITERABLE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_FILE_URL_HOST,
ERR_INVALID_FILE_URL_PATH,
ERR_INVALID_THIS,
ERR_INVALID_TUPLE,
ERR_INVALID_URL,
ERR_INVALID_URL_SCHEME,
ERR_MISSING_ARGS,
ERR_NO_CRYPTO,
},
} = require('internal/errors');
const {
CHAR_AMPERSAND,
CHAR_BACKWARD_SLASH,
Expand Down Expand Up @@ -100,6 +103,11 @@ const {
kSchemeStart
} = internalBinding('url');

const {
storeDataObject,
revokeDataObject,
} = internalBinding('blob');

const context = Symbol('context');
const cannotBeBase = Symbol('cannot-be-base');
const cannotHaveUsernamePasswordPort =
Expand All @@ -108,6 +116,24 @@ const special = Symbol('special');
const searchParams = Symbol('query');
const kFormat = Symbol('format');

let blob;
let cryptoRandom;

function lazyBlob() {
blob ??= require('internal/blob');
return blob;
}

function lazyCryptoRandom() {
try {
cryptoRandom ??= require('internal/crypto/random');
} catch {
// If Node.js built without crypto support, we'll fall
// through here and handle it later.
}
return cryptoRandom;
}

// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
const IteratorPrototype = ObjectGetPrototypeOf(
ObjectGetPrototypeOf([][SymbolIterator]())
Expand Down Expand Up @@ -930,6 +956,37 @@ class URL {
toJSON() {
return this[kFormat]({});
}

static createObjectURL(obj) {
const cryptoRandom = lazyCryptoRandom();
if (cryptoRandom === undefined)
throw new ERR_NO_CRYPTO();

// Yes, lazy loading is annoying but because of circular
// references between the url, internal/blob, and buffer
// modules, lazy loading here makes sure that things work.
const blob = lazyBlob();
if (!blob.isBlob(obj))
throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj);

const id = cryptoRandom.randomUUID();

storeDataObject(id, obj[blob.kHandle], obj.size, obj.type);

return `blob:nodedata:${id}`;
}

static revokeObjectURL(url) {
url = `${url}`;
try {
const parsed = new URL(url);
const split = StringPrototypeSplit(parsed.pathname, ':');
if (split.length === 2)
revokeDataObject(split[1]);
} catch {
// If there's an error, it's ignored.
}
}
}

ObjectDefineProperties(URL.prototype, {
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
// __attribute__((constructor)) like mechanism in GCC.
#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
V(blob) \
V(block_list) \
V(buffer) \
V(cares_wrap) \
Expand Down
Loading

0 comments on commit 37dda19

Please sign in to comment.