url,buffer: implement URL.createObjectURL
Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/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>
This commit is contained in:
parent
793c08b8d1
commit
31d1d0c4c1
@ -4952,6 +4952,20 @@ added: v3.0.0
|
|||||||
|
|
||||||
An alias for [`buffer.constants.MAX_STRING_LENGTH`][].
|
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)`
|
### `buffer.transcode(source, fromEnc, toEnc)`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v7.1.0
|
added: v7.1.0
|
||||||
|
@ -608,6 +608,53 @@ console.log(JSON.stringify(myURLs));
|
|||||||
// Prints ["https://www.example.com/","https://test.example.org/"]
|
// 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`
|
### Class: `URLSearchParams`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added:
|
added:
|
||||||
|
@ -120,6 +120,7 @@ const {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
Blob,
|
Blob,
|
||||||
|
resolveObjectURL,
|
||||||
} = require('internal/blob');
|
} = require('internal/blob');
|
||||||
|
|
||||||
FastBuffer.prototype.constructor = Buffer;
|
FastBuffer.prototype.constructor = Buffer;
|
||||||
@ -1239,6 +1240,7 @@ function atob(input) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Blob,
|
Blob,
|
||||||
|
resolveObjectURL,
|
||||||
Buffer,
|
Buffer,
|
||||||
SlowBuffer,
|
SlowBuffer,
|
||||||
transcode,
|
transcode,
|
||||||
|
@ -7,10 +7,11 @@ const {
|
|||||||
ObjectDefineProperty,
|
ObjectDefineProperty,
|
||||||
PromiseResolve,
|
PromiseResolve,
|
||||||
PromiseReject,
|
PromiseReject,
|
||||||
PromisePrototypeFinally,
|
SafePromisePrototypeFinally,
|
||||||
ReflectConstruct,
|
ReflectConstruct,
|
||||||
RegExpPrototypeTest,
|
RegExpPrototypeTest,
|
||||||
StringPrototypeToLowerCase,
|
StringPrototypeToLowerCase,
|
||||||
|
StringPrototypeSplit,
|
||||||
Symbol,
|
Symbol,
|
||||||
SymbolIterator,
|
SymbolIterator,
|
||||||
SymbolToStringTag,
|
SymbolToStringTag,
|
||||||
@ -20,7 +21,8 @@ const {
|
|||||||
const {
|
const {
|
||||||
createBlob: _createBlob,
|
createBlob: _createBlob,
|
||||||
FixedSizeBlobCopyJob,
|
FixedSizeBlobCopyJob,
|
||||||
} = internalBinding('buffer');
|
getDataObject,
|
||||||
|
} = internalBinding('blob');
|
||||||
|
|
||||||
const { TextDecoder } = require('internal/encoding');
|
const { TextDecoder } = require('internal/encoding');
|
||||||
|
|
||||||
@ -57,26 +59,37 @@ const {
|
|||||||
} = require('internal/validators');
|
} = require('internal/validators');
|
||||||
|
|
||||||
const kHandle = Symbol('kHandle');
|
const kHandle = Symbol('kHandle');
|
||||||
|
const kState = Symbol('kState');
|
||||||
const kType = Symbol('kType');
|
const kType = Symbol('kType');
|
||||||
const kLength = Symbol('kLength');
|
const kLength = Symbol('kLength');
|
||||||
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
|
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
|
||||||
|
|
||||||
|
const kMaxChunkSize = 65536;
|
||||||
|
|
||||||
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
|
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
|
||||||
|
|
||||||
let Buffer;
|
let Buffer;
|
||||||
let ReadableStream;
|
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() {
|
function lazyBuffer() {
|
||||||
if (Buffer === undefined)
|
Buffer ??= require('buffer').Buffer;
|
||||||
Buffer = require('buffer').Buffer;
|
|
||||||
return Buffer;
|
return Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function lazyReadableStream(options) {
|
function lazyReadableStream(options) {
|
||||||
if (ReadableStream === undefined) {
|
ReadableStream ??=
|
||||||
ReadableStream =
|
require('internal/webstreams/readablestream').ReadableStream;
|
||||||
require('internal/webstreams/readablestream').ReadableStream;
|
|
||||||
}
|
|
||||||
return new ReadableStream(options);
|
return new ReadableStream(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +245,9 @@ class Blob {
|
|||||||
return PromiseReject(new ERR_INVALID_THIS('Blob'));
|
return PromiseReject(new ERR_INVALID_THIS('Blob'));
|
||||||
|
|
||||||
// If there's already a promise in flight for the content,
|
// If there's already a promise in flight for the content,
|
||||||
// reuse it, but only once. After the cached promise resolves
|
// reuse it, but only while it's in flight. After the cached
|
||||||
// it will be cleared, allowing it to be garbage collected
|
// promise resolves it will be cleared, allowing it to be
|
||||||
// as soon as possible.
|
// garbage collected as soon as possible.
|
||||||
if (this[kArrayBufferPromise])
|
if (this[kArrayBufferPromise])
|
||||||
return this[kArrayBufferPromise];
|
return this[kArrayBufferPromise];
|
||||||
|
|
||||||
@ -260,7 +273,7 @@ class Blob {
|
|||||||
resolve(ab);
|
resolve(ab);
|
||||||
};
|
};
|
||||||
this[kArrayBufferPromise] =
|
this[kArrayBufferPromise] =
|
||||||
PromisePrototypeFinally(
|
SafePromisePrototypeFinally(
|
||||||
promise,
|
promise,
|
||||||
() => this[kArrayBufferPromise] = undefined);
|
() => this[kArrayBufferPromise] = undefined);
|
||||||
|
|
||||||
@ -268,7 +281,6 @@ class Blob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async text() {
|
async text() {
|
||||||
@ -288,10 +300,20 @@ class Blob {
|
|||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
return new lazyReadableStream({
|
return new lazyReadableStream({
|
||||||
async start(controller) {
|
async start() {
|
||||||
const ab = await self.arrayBuffer();
|
this[kState] = await self.arrayBuffer();
|
||||||
controller.enqueue(new Uint8Array(ab));
|
},
|
||||||
controller.close();
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -315,9 +337,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
|
|||||||
value: 'Blob',
|
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 = {
|
module.exports = {
|
||||||
Blob,
|
Blob,
|
||||||
ClonedBlob,
|
ClonedBlob,
|
||||||
createBlob,
|
createBlob,
|
||||||
isBlob,
|
isBlob,
|
||||||
|
kHandle,
|
||||||
|
resolveObjectURL,
|
||||||
};
|
};
|
||||||
|
@ -42,17 +42,20 @@ const {
|
|||||||
|
|
||||||
const { getConstructorOf, removeColors } = require('internal/util');
|
const { getConstructorOf, removeColors } = require('internal/util');
|
||||||
const {
|
const {
|
||||||
ERR_ARG_NOT_ITERABLE,
|
codes: {
|
||||||
ERR_INVALID_ARG_TYPE,
|
ERR_ARG_NOT_ITERABLE,
|
||||||
ERR_INVALID_ARG_VALUE,
|
ERR_INVALID_ARG_TYPE,
|
||||||
ERR_INVALID_FILE_URL_HOST,
|
ERR_INVALID_ARG_VALUE,
|
||||||
ERR_INVALID_FILE_URL_PATH,
|
ERR_INVALID_FILE_URL_HOST,
|
||||||
ERR_INVALID_THIS,
|
ERR_INVALID_FILE_URL_PATH,
|
||||||
ERR_INVALID_TUPLE,
|
ERR_INVALID_THIS,
|
||||||
ERR_INVALID_URL,
|
ERR_INVALID_TUPLE,
|
||||||
ERR_INVALID_URL_SCHEME,
|
ERR_INVALID_URL,
|
||||||
ERR_MISSING_ARGS
|
ERR_INVALID_URL_SCHEME,
|
||||||
} = require('internal/errors').codes;
|
ERR_MISSING_ARGS,
|
||||||
|
ERR_NO_CRYPTO,
|
||||||
|
},
|
||||||
|
} = require('internal/errors');
|
||||||
const {
|
const {
|
||||||
CHAR_AMPERSAND,
|
CHAR_AMPERSAND,
|
||||||
CHAR_BACKWARD_SLASH,
|
CHAR_BACKWARD_SLASH,
|
||||||
@ -100,6 +103,11 @@ const {
|
|||||||
kSchemeStart
|
kSchemeStart
|
||||||
} = internalBinding('url');
|
} = internalBinding('url');
|
||||||
|
|
||||||
|
const {
|
||||||
|
storeDataObject,
|
||||||
|
revokeDataObject,
|
||||||
|
} = internalBinding('blob');
|
||||||
|
|
||||||
const context = Symbol('context');
|
const context = Symbol('context');
|
||||||
const cannotBeBase = Symbol('cannot-be-base');
|
const cannotBeBase = Symbol('cannot-be-base');
|
||||||
const cannotHaveUsernamePasswordPort =
|
const cannotHaveUsernamePasswordPort =
|
||||||
@ -108,6 +116,24 @@ const special = Symbol('special');
|
|||||||
const searchParams = Symbol('query');
|
const searchParams = Symbol('query');
|
||||||
const kFormat = Symbol('format');
|
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
|
// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
|
||||||
const IteratorPrototype = ObjectGetPrototypeOf(
|
const IteratorPrototype = ObjectGetPrototypeOf(
|
||||||
ObjectGetPrototypeOf([][SymbolIterator]())
|
ObjectGetPrototypeOf([][SymbolIterator]())
|
||||||
@ -930,6 +956,37 @@ class URL {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return this[kFormat]({});
|
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, {
|
ObjectDefineProperties(URL.prototype, {
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
// __attribute__((constructor)) like mechanism in GCC.
|
// __attribute__((constructor)) like mechanism in GCC.
|
||||||
#define NODE_BUILTIN_STANDARD_MODULES(V) \
|
#define NODE_BUILTIN_STANDARD_MODULES(V) \
|
||||||
V(async_wrap) \
|
V(async_wrap) \
|
||||||
|
V(blob) \
|
||||||
V(block_list) \
|
V(block_list) \
|
||||||
V(buffer) \
|
V(buffer) \
|
||||||
V(cares_wrap) \
|
V(cares_wrap) \
|
||||||
|
166
src/node_blob.cc
166
src/node_blob.cc
@ -26,12 +26,26 @@ using v8::Local;
|
|||||||
using v8::MaybeLocal;
|
using v8::MaybeLocal;
|
||||||
using v8::Number;
|
using v8::Number;
|
||||||
using v8::Object;
|
using v8::Object;
|
||||||
|
using v8::String;
|
||||||
using v8::Uint32;
|
using v8::Uint32;
|
||||||
using v8::Undefined;
|
using v8::Undefined;
|
||||||
using v8::Value;
|
using v8::Value;
|
||||||
|
|
||||||
void Blob::Initialize(Environment* env, v8::Local<v8::Object> target) {
|
void Blob::Initialize(
|
||||||
|
Local<Object> target,
|
||||||
|
Local<Value> unused,
|
||||||
|
Local<Context> context,
|
||||||
|
void* priv) {
|
||||||
|
Environment* env = Environment::GetCurrent(context);
|
||||||
|
|
||||||
|
BlobBindingData* const binding_data =
|
||||||
|
env->AddBindingData<BlobBindingData>(context, target);
|
||||||
|
if (binding_data == nullptr) return;
|
||||||
|
|
||||||
env->SetMethod(target, "createBlob", New);
|
env->SetMethod(target, "createBlob", New);
|
||||||
|
env->SetMethod(target, "storeDataObject", StoreDataObject);
|
||||||
|
env->SetMethod(target, "getDataObject", GetDataObject);
|
||||||
|
env->SetMethod(target, "revokeDataObject", RevokeDataObject);
|
||||||
FixedSizeBlobCopyJob::Initialize(env, target);
|
FixedSizeBlobCopyJob::Initialize(env, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +234,78 @@ std::unique_ptr<worker::TransferData> Blob::CloneForMessaging() const {
|
|||||||
return std::make_unique<BlobTransferData>(store_, length_);
|
return std::make_unique<BlobTransferData>(store_, length_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Blob::StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
BlobBindingData* binding_data =
|
||||||
|
Environment::GetBindingData<BlobBindingData>(args);
|
||||||
|
|
||||||
|
CHECK(args[0]->IsString()); // ID key
|
||||||
|
CHECK(Blob::HasInstance(env, args[1])); // Blob
|
||||||
|
CHECK(args[2]->IsUint32()); // Length
|
||||||
|
CHECK(args[3]->IsString()); // Type
|
||||||
|
|
||||||
|
Utf8Value key(env->isolate(), args[0]);
|
||||||
|
Blob* blob;
|
||||||
|
ASSIGN_OR_RETURN_UNWRAP(&blob, args[1]);
|
||||||
|
|
||||||
|
size_t length = args[2].As<Uint32>()->Value();
|
||||||
|
Utf8Value type(env->isolate(), args[3]);
|
||||||
|
|
||||||
|
binding_data->store_data_object(
|
||||||
|
std::string(*key, key.length()),
|
||||||
|
BlobBindingData::StoredDataObject(
|
||||||
|
BaseObjectPtr<Blob>(blob),
|
||||||
|
length,
|
||||||
|
std::string(*type, type.length())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Blob::RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
||||||
|
BlobBindingData* binding_data =
|
||||||
|
Environment::GetBindingData<BlobBindingData>(args);
|
||||||
|
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
CHECK(args[0]->IsString()); // ID key
|
||||||
|
|
||||||
|
Utf8Value key(env->isolate(), args[0]);
|
||||||
|
|
||||||
|
binding_data->revoke_data_object(std::string(*key, key.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Blob::GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
||||||
|
BlobBindingData* binding_data =
|
||||||
|
Environment::GetBindingData<BlobBindingData>(args);
|
||||||
|
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
CHECK(args[0]->IsString());
|
||||||
|
|
||||||
|
Utf8Value key(env->isolate(), args[0]);
|
||||||
|
|
||||||
|
BlobBindingData::StoredDataObject stored =
|
||||||
|
binding_data->get_data_object(std::string(*key, key.length()));
|
||||||
|
if (stored.blob) {
|
||||||
|
Local<Value> type;
|
||||||
|
if (!String::NewFromUtf8(
|
||||||
|
env->isolate(),
|
||||||
|
stored.type.c_str(),
|
||||||
|
v8::NewStringType::kNormal,
|
||||||
|
static_cast<int>(stored.type.length())).ToLocal(&type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Local<Value> values[] = {
|
||||||
|
stored.blob->object(),
|
||||||
|
Uint32::NewFromUnsigned(env->isolate(), stored.length),
|
||||||
|
type
|
||||||
|
};
|
||||||
|
|
||||||
|
args.GetReturnValue().Set(
|
||||||
|
Array::New(
|
||||||
|
env->isolate(),
|
||||||
|
values,
|
||||||
|
arraysize(values)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FixedSizeBlobCopyJob::FixedSizeBlobCopyJob(
|
FixedSizeBlobCopyJob::FixedSizeBlobCopyJob(
|
||||||
Environment* env,
|
Environment* env,
|
||||||
Local<Object> object,
|
Local<Object> object,
|
||||||
@ -328,10 +414,88 @@ void FixedSizeBlobCopyJob::RegisterExternalReferences(
|
|||||||
registry->Register(Run);
|
registry->Register(Run);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::StoredDataObject::MemoryInfo(
|
||||||
|
MemoryTracker* tracker) const {
|
||||||
|
tracker->TrackField("blob", blob);
|
||||||
|
tracker->TrackFieldWithSize("type", type.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobBindingData::StoredDataObject::StoredDataObject(
|
||||||
|
const BaseObjectPtr<Blob>& blob_,
|
||||||
|
size_t length_,
|
||||||
|
const std::string& type_)
|
||||||
|
: blob(blob_),
|
||||||
|
length(length_),
|
||||||
|
type(type_) {}
|
||||||
|
|
||||||
|
BlobBindingData::BlobBindingData(Environment* env, Local<Object> wrap)
|
||||||
|
: SnapshotableObject(env, wrap, type_int) {
|
||||||
|
MakeWeak();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::MemoryInfo(MemoryTracker* tracker) const {
|
||||||
|
tracker->TrackField("data_objects", data_objects_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::store_data_object(
|
||||||
|
const std::string& uuid,
|
||||||
|
const BlobBindingData::StoredDataObject& object) {
|
||||||
|
data_objects_[uuid] = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::revoke_data_object(const std::string& uuid) {
|
||||||
|
CHECK_NE(data_objects_.find(uuid), data_objects_.end());
|
||||||
|
data_objects_.erase(uuid);
|
||||||
|
CHECK_EQ(data_objects_.find(uuid), data_objects_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobBindingData::StoredDataObject BlobBindingData::get_data_object(
|
||||||
|
const std::string& uuid) {
|
||||||
|
auto entry = data_objects_.find(uuid);
|
||||||
|
if (entry == data_objects_.end())
|
||||||
|
return BlobBindingData::StoredDataObject {};
|
||||||
|
return entry->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::Deserialize(
|
||||||
|
Local<Context> context,
|
||||||
|
Local<Object> holder,
|
||||||
|
int index,
|
||||||
|
InternalFieldInfo* info) {
|
||||||
|
DCHECK_EQ(index, BaseObject::kSlot);
|
||||||
|
HandleScope scope(context->GetIsolate());
|
||||||
|
Environment* env = Environment::GetCurrent(context);
|
||||||
|
BlobBindingData* binding =
|
||||||
|
env->AddBindingData<BlobBindingData>(context, holder);
|
||||||
|
CHECK_NOT_NULL(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobBindingData::PrepareForSerialization(
|
||||||
|
Local<Context> context,
|
||||||
|
v8::SnapshotCreator* creator) {
|
||||||
|
// Stored blob objects are not actually persisted.
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalFieldInfo* BlobBindingData::Serialize(int index) {
|
||||||
|
DCHECK_EQ(index, BaseObject::kSlot);
|
||||||
|
InternalFieldInfo* info = InternalFieldInfo::New(type());
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr FastStringKey BlobBindingData::type_name;
|
||||||
|
|
||||||
void Blob::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
void Blob::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
registry->Register(Blob::New);
|
registry->Register(Blob::New);
|
||||||
registry->Register(Blob::ToArrayBuffer);
|
registry->Register(Blob::ToArrayBuffer);
|
||||||
registry->Register(Blob::ToSlice);
|
registry->Register(Blob::ToSlice);
|
||||||
|
registry->Register(Blob::StoreDataObject);
|
||||||
|
registry->Register(Blob::GetDataObject);
|
||||||
|
registry->Register(Blob::RevokeDataObject);
|
||||||
|
|
||||||
|
FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
|
NODE_MODULE_CONTEXT_AWARE_INTERNAL(blob, node::Blob::Initialize);
|
||||||
|
NODE_MODULE_EXTERNAL_REFERENCE(blob, node::Blob::RegisterExternalReferences);
|
||||||
|
@ -8,9 +8,12 @@
|
|||||||
#include "env.h"
|
#include "env.h"
|
||||||
#include "memory_tracker.h"
|
#include "memory_tracker.h"
|
||||||
#include "node_internals.h"
|
#include "node_internals.h"
|
||||||
|
#include "node_snapshotable.h"
|
||||||
#include "node_worker.h"
|
#include "node_worker.h"
|
||||||
#include "v8.h"
|
#include "v8.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
@ -25,11 +28,19 @@ class Blob : public BaseObject {
|
|||||||
public:
|
public:
|
||||||
static void RegisterExternalReferences(
|
static void RegisterExternalReferences(
|
||||||
ExternalReferenceRegistry* registry);
|
ExternalReferenceRegistry* registry);
|
||||||
static void Initialize(Environment* env, v8::Local<v8::Object> target);
|
|
||||||
|
static void Initialize(
|
||||||
|
v8::Local<v8::Object> target,
|
||||||
|
v8::Local<v8::Value> unused,
|
||||||
|
v8::Local<v8::Context> context,
|
||||||
|
void* priv);
|
||||||
|
|
||||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void ToArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void ToArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void ToSlice(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void ToSlice(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
static void StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
static void GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
static void RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
|
||||||
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
|
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
|
||||||
Environment* env);
|
Environment* env);
|
||||||
@ -131,6 +142,49 @@ class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork {
|
|||||||
size_t length_ = 0;
|
size_t length_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class BlobBindingData : public SnapshotableObject {
|
||||||
|
public:
|
||||||
|
explicit BlobBindingData(Environment* env, v8::Local<v8::Object> wrap);
|
||||||
|
|
||||||
|
SERIALIZABLE_OBJECT_METHODS()
|
||||||
|
|
||||||
|
static constexpr FastStringKey type_name{"node::BlobBindingData"};
|
||||||
|
static constexpr EmbedderObjectType type_int =
|
||||||
|
EmbedderObjectType::k_blob_binding_data;
|
||||||
|
|
||||||
|
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||||
|
SET_SELF_SIZE(BlobBindingData)
|
||||||
|
SET_MEMORY_INFO_NAME(BlobBindingData)
|
||||||
|
|
||||||
|
struct StoredDataObject : public MemoryRetainer {
|
||||||
|
BaseObjectPtr<Blob> blob;
|
||||||
|
size_t length;
|
||||||
|
std::string type;
|
||||||
|
|
||||||
|
StoredDataObject() = default;
|
||||||
|
|
||||||
|
StoredDataObject(
|
||||||
|
const BaseObjectPtr<Blob>& blob_,
|
||||||
|
size_t length_,
|
||||||
|
const std::string& type_);
|
||||||
|
|
||||||
|
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||||
|
SET_SELF_SIZE(StoredDataObject)
|
||||||
|
SET_MEMORY_INFO_NAME(StoredDataObject)
|
||||||
|
};
|
||||||
|
|
||||||
|
void store_data_object(
|
||||||
|
const std::string& uuid,
|
||||||
|
const StoredDataObject& object);
|
||||||
|
|
||||||
|
void revoke_data_object(const std::string& uuid);
|
||||||
|
|
||||||
|
StoredDataObject get_data_object(const std::string& uuid);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, StoredDataObject> data_objects_;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
@ -1266,8 +1266,6 @@ void Initialize(Local<Object> target,
|
|||||||
env->SetMethod(target, "utf8Write", StringWrite<UTF8>);
|
env->SetMethod(target, "utf8Write", StringWrite<UTF8>);
|
||||||
|
|
||||||
env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle);
|
env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle);
|
||||||
|
|
||||||
Blob::Initialize(env, target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
@ -1311,9 +1309,6 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
|||||||
|
|
||||||
registry->Register(DetachArrayBuffer);
|
registry->Register(DetachArrayBuffer);
|
||||||
registry->Register(CopyArrayBuffer);
|
registry->Register(CopyArrayBuffer);
|
||||||
|
|
||||||
Blob::RegisterExternalReferences(registry);
|
|
||||||
FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Buffer
|
} // namespace Buffer
|
||||||
|
@ -49,6 +49,7 @@ class ExternalReferenceRegistry {
|
|||||||
#define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
|
#define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
|
||||||
V(async_wrap) \
|
V(async_wrap) \
|
||||||
V(binding) \
|
V(binding) \
|
||||||
|
V(blob) \
|
||||||
V(buffer) \
|
V(buffer) \
|
||||||
V(contextify) \
|
V(contextify) \
|
||||||
V(credentials) \
|
V(credentials) \
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include "base_object-inl.h"
|
#include "base_object-inl.h"
|
||||||
#include "debug_utils-inl.h"
|
#include "debug_utils-inl.h"
|
||||||
#include "env-inl.h"
|
#include "env-inl.h"
|
||||||
|
#include "node_blob.h"
|
||||||
#include "node_errors.h"
|
#include "node_errors.h"
|
||||||
#include "node_external_reference.h"
|
#include "node_external_reference.h"
|
||||||
#include "node_file.h"
|
#include "node_file.h"
|
||||||
|
@ -15,7 +15,8 @@ struct SnapshotData;
|
|||||||
|
|
||||||
#define SERIALIZABLE_OBJECT_TYPES(V) \
|
#define SERIALIZABLE_OBJECT_TYPES(V) \
|
||||||
V(fs_binding_data, fs::BindingData) \
|
V(fs_binding_data, fs::BindingData) \
|
||||||
V(v8_binding_data, v8_utils::BindingData)
|
V(v8_binding_data, v8_utils::BindingData) \
|
||||||
|
V(blob_binding_data, BlobBindingData)
|
||||||
|
|
||||||
enum class EmbedderObjectType : uint8_t {
|
enum class EmbedderObjectType : uint8_t {
|
||||||
k_default = 0,
|
k_default = 0,
|
||||||
|
48
test/parallel/test-blob-createobjecturl.js
Normal file
48
test/parallel/test-blob-createobjecturl.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Flags: --no-warnings
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
|
||||||
|
// Because registering a Blob URL requires generating a random
|
||||||
|
// UUID, it can only be done if crypto support is enabled.
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
|
||||||
|
const {
|
||||||
|
URL,
|
||||||
|
} = require('url');
|
||||||
|
|
||||||
|
const {
|
||||||
|
Blob,
|
||||||
|
resolveObjectURL,
|
||||||
|
} = require('buffer');
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const blob = new Blob(['hello']);
|
||||||
|
const id = URL.createObjectURL(blob);
|
||||||
|
assert.strictEqual(typeof id, 'string');
|
||||||
|
const otherBlob = resolveObjectURL(id);
|
||||||
|
assert.strictEqual(otherBlob.size, 5);
|
||||||
|
assert.strictEqual(
|
||||||
|
Buffer.from(await otherBlob.arrayBuffer()).toString(),
|
||||||
|
'hello');
|
||||||
|
URL.revokeObjectURL(id);
|
||||||
|
assert.strictEqual(resolveObjectURL(id), undefined);
|
||||||
|
|
||||||
|
// Leaving a Blob registered should not cause an assert
|
||||||
|
// when Node.js exists
|
||||||
|
URL.createObjectURL(new Blob());
|
||||||
|
|
||||||
|
})().then(common.mustCall());
|
||||||
|
|
||||||
|
['not a url', undefined, 1, 'blob:nodedata:1:wrong', {}].forEach((i) => {
|
||||||
|
assert.strictEqual(resolveObjectURL(i), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
[undefined, 1, '', false, {}].forEach((i) => {
|
||||||
|
assert.throws(() => URL.createObjectURL(i), {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
});
|
||||||
|
});
|
@ -130,6 +130,7 @@ const expectedModules = new Set([
|
|||||||
'NativeModule internal/vm/module',
|
'NativeModule internal/vm/module',
|
||||||
'NativeModule internal/worker/io',
|
'NativeModule internal/worker/io',
|
||||||
'NativeModule internal/worker/js_transferable',
|
'NativeModule internal/worker/js_transferable',
|
||||||
|
'Internal Binding blob',
|
||||||
'NativeModule internal/blob',
|
'NativeModule internal/blob',
|
||||||
'NativeModule async_hooks',
|
'NativeModule async_hooks',
|
||||||
'NativeModule path',
|
'NativeModule path',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user