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:
James M Snell 2021-08-06 19:26:37 -07:00
parent 793c08b8d1
commit 31d1d0c4c1
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
14 changed files with 482 additions and 36 deletions

View File

@ -4952,6 +4952,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

View File

@ -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:

View File

@ -120,6 +120,7 @@ const {
const {
Blob,
resolveObjectURL,
} = require('internal/blob');
FastBuffer.prototype.constructor = Buffer;
@ -1239,6 +1240,7 @@ function atob(input) {
module.exports = {
Blob,
resolveObjectURL,
Buffer,
SlowBuffer,
transcode,

View File

@ -7,10 +7,11 @@ const {
ObjectDefineProperty,
PromiseResolve,
PromiseReject,
PromisePrototypeFinally,
SafePromisePrototypeFinally,
ReflectConstruct,
RegExpPrototypeTest,
StringPrototypeToLowerCase,
StringPrototypeSplit,
Symbol,
SymbolIterator,
SymbolToStringTag,
@ -20,7 +21,8 @@ const {
const {
createBlob: _createBlob,
FixedSizeBlobCopyJob,
} = internalBinding('buffer');
getDataObject,
} = internalBinding('blob');
const { TextDecoder } = require('internal/encoding');
@ -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);
}
@ -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];
@ -260,7 +273,7 @@ class Blob {
resolve(ab);
};
this[kArrayBufferPromise] =
PromisePrototypeFinally(
SafePromisePrototypeFinally(
promise,
() => this[kArrayBufferPromise] = undefined);
@ -268,7 +281,6 @@ class Blob {
}
/**
*
* @returns {Promise<string>}
*/
async text() {
@ -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));
}
}
});
}
@ -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,
};

View File

@ -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,
@ -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 =
@ -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]())
@ -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, {

View File

@ -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) \

View File

@ -26,12 +26,26 @@ using v8::Local;
using v8::MaybeLocal;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Uint32;
using v8::Undefined;
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, "storeDataObject", StoreDataObject);
env->SetMethod(target, "getDataObject", GetDataObject);
env->SetMethod(target, "revokeDataObject", RevokeDataObject);
FixedSizeBlobCopyJob::Initialize(env, target);
}
@ -220,6 +234,78 @@ std::unique_ptr<worker::TransferData> Blob::CloneForMessaging() const {
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(
Environment* env,
Local<Object> object,
@ -328,10 +414,88 @@ void FixedSizeBlobCopyJob::RegisterExternalReferences(
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) {
registry->Register(Blob::New);
registry->Register(Blob::ToArrayBuffer);
registry->Register(Blob::ToSlice);
registry->Register(Blob::StoreDataObject);
registry->Register(Blob::GetDataObject);
registry->Register(Blob::RevokeDataObject);
FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
}
} // namespace node
NODE_MODULE_CONTEXT_AWARE_INTERNAL(blob, node::Blob::Initialize);
NODE_MODULE_EXTERNAL_REFERENCE(blob, node::Blob::RegisterExternalReferences);

View File

@ -8,9 +8,12 @@
#include "env.h"
#include "memory_tracker.h"
#include "node_internals.h"
#include "node_snapshotable.h"
#include "node_worker.h"
#include "v8.h"
#include <string>
#include <unordered_map>
#include <vector>
namespace node {
@ -25,11 +28,19 @@ class Blob : public BaseObject {
public:
static void RegisterExternalReferences(
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 ToArrayBuffer(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(
Environment* env);
@ -131,6 +142,49 @@ class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork {
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
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

View File

@ -1266,8 +1266,6 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "utf8Write", StringWrite<UTF8>);
env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle);
Blob::Initialize(env, target);
}
} // anonymous namespace
@ -1311,9 +1309,6 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(DetachArrayBuffer);
registry->Register(CopyArrayBuffer);
Blob::RegisterExternalReferences(registry);
FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
}
} // namespace Buffer

View File

@ -49,6 +49,7 @@ class ExternalReferenceRegistry {
#define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
V(async_wrap) \
V(binding) \
V(blob) \
V(buffer) \
V(contextify) \
V(credentials) \

View File

@ -5,6 +5,7 @@
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node_blob.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_file.h"

View File

@ -15,7 +15,8 @@ struct SnapshotData;
#define SERIALIZABLE_OBJECT_TYPES(V) \
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 {
k_default = 0,

View 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',
});
});

View File

@ -130,6 +130,7 @@ const expectedModules = new Set([
'NativeModule internal/vm/module',
'NativeModule internal/worker/io',
'NativeModule internal/worker/js_transferable',
'Internal Binding blob',
'NativeModule internal/blob',
'NativeModule async_hooks',
'NativeModule path',