mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
a7dad43d15
PR-URL: https://github.com/nodejs/node/pull/53061 Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
458 lines
15 KiB
C++
458 lines
15 KiB
C++
#include "node_modules.h"
|
|
#include <cstdio>
|
|
#include "base_object-inl.h"
|
|
#include "node_errors.h"
|
|
#include "node_external_reference.h"
|
|
#include "node_url.h"
|
|
#include "permission/permission.h"
|
|
#include "permission/permission_base.h"
|
|
#include "util-inl.h"
|
|
#include "v8-fast-api-calls.h"
|
|
#include "v8-function-callback.h"
|
|
#include "v8-primitive.h"
|
|
#include "v8-value.h"
|
|
#include "v8.h"
|
|
|
|
#include "simdjson.h"
|
|
|
|
namespace node {
|
|
namespace modules {
|
|
|
|
using v8::Array;
|
|
using v8::Context;
|
|
using v8::FunctionCallbackInfo;
|
|
using v8::HandleScope;
|
|
using v8::Isolate;
|
|
using v8::Local;
|
|
using v8::NewStringType;
|
|
using v8::Object;
|
|
using v8::ObjectTemplate;
|
|
using v8::Primitive;
|
|
using v8::String;
|
|
using v8::Undefined;
|
|
using v8::Value;
|
|
|
|
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
|
|
// Do nothing
|
|
}
|
|
|
|
BindingData::BindingData(Realm* realm,
|
|
v8::Local<v8::Object> object,
|
|
InternalFieldInfo* info)
|
|
: SnapshotableObject(realm, object, type_int) {}
|
|
|
|
bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
|
|
v8::SnapshotCreator* creator) {
|
|
// Return true because we need to maintain the reference to the binding from
|
|
// JS land.
|
|
return true;
|
|
}
|
|
|
|
InternalFieldInfoBase* BindingData::Serialize(int index) {
|
|
DCHECK_IS_SNAPSHOT_SLOT(index);
|
|
InternalFieldInfo* info =
|
|
InternalFieldInfoBase::New<InternalFieldInfo>(type());
|
|
return info;
|
|
}
|
|
|
|
void BindingData::Deserialize(v8::Local<v8::Context> context,
|
|
v8::Local<v8::Object> holder,
|
|
int index,
|
|
InternalFieldInfoBase* info) {
|
|
DCHECK_IS_SNAPSHOT_SLOT(index);
|
|
HandleScope scope(context->GetIsolate());
|
|
Realm* realm = Realm::GetCurrent(context);
|
|
BindingData* binding = realm->AddBindingData<BindingData>(holder);
|
|
CHECK_NOT_NULL(binding);
|
|
}
|
|
|
|
Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
|
|
auto isolate = realm->isolate();
|
|
const auto ToString = [isolate](std::string_view input) -> Local<Primitive> {
|
|
return String::NewFromUtf8(
|
|
isolate, input.data(), NewStringType::kNormal, input.size())
|
|
.ToLocalChecked();
|
|
};
|
|
Local<Value> values[6] = {
|
|
name.has_value() ? ToString(*name) : Undefined(isolate),
|
|
main.has_value() ? ToString(*main) : Undefined(isolate),
|
|
ToString(type),
|
|
imports.has_value() ? ToString(*imports) : Undefined(isolate),
|
|
exports.has_value() ? ToString(*exports) : Undefined(isolate),
|
|
ToString(file_path),
|
|
};
|
|
return Array::New(isolate, values, 6);
|
|
}
|
|
|
|
const BindingData::PackageConfig* BindingData::GetPackageJSON(
|
|
Realm* realm, std::string_view path, ErrorContext* error_context) {
|
|
auto binding_data = realm->GetBindingData<BindingData>();
|
|
|
|
auto cache_entry = binding_data->package_configs_.find(path.data());
|
|
if (cache_entry != binding_data->package_configs_.end()) {
|
|
return &cache_entry->second;
|
|
}
|
|
|
|
PackageConfig package_config{};
|
|
package_config.file_path = path;
|
|
// No need to exclude BOM since simdjson will skip it.
|
|
if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
simdjson::ondemand::document document;
|
|
simdjson::ondemand::object main_object;
|
|
simdjson::error_code error =
|
|
binding_data->json_parser.iterate(package_config.raw_json).get(document);
|
|
|
|
const auto throw_invalid_package_config = [error_context, path, realm]() {
|
|
if (error_context == nullptr) {
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(), "Invalid package config %s.", path.data());
|
|
} else if (error_context->base.has_value()) {
|
|
auto file_url = ada::parse(error_context->base.value());
|
|
CHECK(file_url);
|
|
auto file_path = url::FileURLToPath(realm->env(), *file_url);
|
|
CHECK(file_path.has_value());
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(),
|
|
"Invalid package config %s while importing \"%s\" from %s.",
|
|
path.data(),
|
|
error_context->specifier.c_str(),
|
|
file_path->c_str());
|
|
} else {
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(), "Invalid package config %s.", path.data());
|
|
}
|
|
|
|
return nullptr;
|
|
};
|
|
|
|
if (error || document.get_object().get(main_object)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
|
|
simdjson::ondemand::raw_json_string key;
|
|
simdjson::ondemand::value value;
|
|
std::string_view field_value;
|
|
simdjson::ondemand::json_type field_type;
|
|
|
|
for (auto field : main_object) {
|
|
// Throw error if getting key or value fails.
|
|
if (field.key().get(key) || field.value().get(value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
|
|
// based on coverity using key with == derefs the raw value
|
|
// avoid derefing if its null
|
|
if (key.raw() == nullptr) continue;
|
|
|
|
if (key == "name") {
|
|
// Though there is a key "name" with a corresponding value,
|
|
// the value may not be a string or could be an invalid JSON string
|
|
if (value.get_string(package_config.name)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
} else if (key == "main") {
|
|
// Omit all non-string values
|
|
USE(value.get_string(package_config.main));
|
|
} else if (key == "exports") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::object:
|
|
case simdjson::ondemand::json_type::array: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config.exports = field_value;
|
|
break;
|
|
}
|
|
case simdjson::ondemand::json_type::string: {
|
|
if (value.get_string(package_config.exports)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} else if (key == "imports") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::array:
|
|
case simdjson::ondemand::json_type::object: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config.imports = field_value;
|
|
break;
|
|
}
|
|
case simdjson::ondemand::json_type::string: {
|
|
if (value.get_string(package_config.imports)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} else if (key == "type") {
|
|
if (value.get_string().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
// Only update type if it is "commonjs" or "module"
|
|
// The default value is "none" for backward compatibility.
|
|
if (field_value == "commonjs" || field_value == "module") {
|
|
package_config.type = field_value;
|
|
}
|
|
} else if (key == "scripts") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::object: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config.scripts = field_value;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// package_config could be quite large, so we should move it instead of
|
|
// copying it.
|
|
auto cached = binding_data->package_configs_.insert(
|
|
{std::string(path), std::move(package_config)});
|
|
|
|
return &cached.first->second;
|
|
}
|
|
|
|
void BindingData::ReadPackageJSON(const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier]
|
|
CHECK(args[0]->IsString()); // path
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
auto isolate = realm->isolate();
|
|
|
|
Utf8Value path(isolate, args[0]);
|
|
bool is_esm = args[1]->IsTrue();
|
|
auto error_context = ErrorContext();
|
|
if (is_esm) {
|
|
CHECK(args[2]->IsUndefined() || args[2]->IsString()); // base
|
|
CHECK(args[3]->IsString()); // specifier
|
|
|
|
if (args[2]->IsString()) {
|
|
Utf8Value base_value(isolate, args[2]);
|
|
error_context.base = base_value.ToString();
|
|
}
|
|
Utf8Value specifier(isolate, args[3]);
|
|
error_context.specifier = specifier.ToString();
|
|
}
|
|
|
|
THROW_IF_INSUFFICIENT_PERMISSIONS(
|
|
realm->env(),
|
|
permission::PermissionScope::kFileSystemRead,
|
|
path.ToStringView());
|
|
|
|
// TODO(StefanStojanovic): Remove ifdef after
|
|
// path.toNamespacedPath logic is ported to C++
|
|
#ifdef _WIN32
|
|
auto package_json = GetPackageJSON(
|
|
realm, "\\\\?\\" + path.ToString(), is_esm ? &error_context : nullptr);
|
|
#else
|
|
auto package_json =
|
|
GetPackageJSON(realm, path.ToString(), is_esm ? &error_context : nullptr);
|
|
#endif
|
|
if (package_json == nullptr) {
|
|
return;
|
|
}
|
|
|
|
args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
|
|
const BindingData::PackageConfig* BindingData::TraverseParent(
|
|
Realm* realm, const std::filesystem::path& check_path) {
|
|
std::filesystem::path current_path = check_path;
|
|
auto env = realm->env();
|
|
const bool is_permissions_enabled = env->permission()->enabled();
|
|
|
|
do {
|
|
current_path = current_path.parent_path();
|
|
|
|
// We don't need to try "/"
|
|
if (current_path.parent_path() == current_path) {
|
|
break;
|
|
}
|
|
|
|
// Stop the search when the process doesn't have permissions
|
|
// to walk upwards
|
|
if (UNLIKELY(is_permissions_enabled &&
|
|
!env->permission()->is_granted(
|
|
env,
|
|
permission::PermissionScope::kFileSystemRead,
|
|
current_path.generic_string()))) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Check if the path ends with `/node_modules`
|
|
if (current_path.generic_string().ends_with("/node_modules")) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto package_json_path = current_path / "package.json";
|
|
auto package_json =
|
|
GetPackageJSON(realm, package_json_path.string(), nullptr);
|
|
if (package_json != nullptr) {
|
|
return package_json;
|
|
}
|
|
} while (true);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void BindingData::GetNearestParentPackageJSON(
|
|
const v8::FunctionCallbackInfo<v8::Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
Utf8Value path_value(realm->isolate(), args[0]);
|
|
auto package_json =
|
|
TraverseParent(realm, std::filesystem::path(path_value.ToString()));
|
|
|
|
if (package_json != nullptr) {
|
|
args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
}
|
|
|
|
void BindingData::GetNearestParentPackageJSONType(
|
|
const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
Utf8Value path(realm->isolate(), args[0]);
|
|
auto package_json =
|
|
TraverseParent(realm, std::filesystem::path(path.ToString()));
|
|
|
|
if (package_json == nullptr) {
|
|
return;
|
|
}
|
|
|
|
Local<Value> value =
|
|
ToV8Value(realm->context(), package_json->type).ToLocalChecked();
|
|
args.GetReturnValue().Set(value);
|
|
}
|
|
|
|
void BindingData::GetPackageScopeConfig(
|
|
const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
Utf8Value resolved(realm->isolate(), args[0]);
|
|
auto package_json_url_base = ada::parse(resolved.ToStringView());
|
|
if (!package_json_url_base) {
|
|
url::ThrowInvalidURL(realm->env(), resolved.ToStringView(), std::nullopt);
|
|
return;
|
|
}
|
|
auto package_json_url =
|
|
ada::parse("./package.json", &package_json_url_base.value());
|
|
if (!package_json_url) {
|
|
url::ThrowInvalidURL(realm->env(), "./package.json", resolved.ToString());
|
|
return;
|
|
}
|
|
|
|
std::string_view node_modules_package_path = "/node_modules/package.json";
|
|
auto error_context = ErrorContext();
|
|
error_context.is_esm = true;
|
|
|
|
// TODO(@anonrig): Rewrite this function and avoid calling URL parser.
|
|
while (true) {
|
|
auto pathname = package_json_url->get_pathname();
|
|
if (pathname.ends_with(node_modules_package_path)) {
|
|
break;
|
|
}
|
|
|
|
auto file_url = url::FileURLToPath(realm->env(), *package_json_url);
|
|
CHECK(file_url);
|
|
error_context.specifier = resolved.ToString();
|
|
auto package_json = GetPackageJSON(realm, *file_url, &error_context);
|
|
if (package_json != nullptr) {
|
|
return args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
|
|
auto last_href = std::string(package_json_url->get_href());
|
|
auto last_pathname = std::string(package_json_url->get_pathname());
|
|
package_json_url = ada::parse("../package.json", &package_json_url.value());
|
|
if (!package_json_url) {
|
|
url::ThrowInvalidURL(realm->env(), "../package.json", last_href);
|
|
return;
|
|
}
|
|
|
|
// Terminates at root where ../package.json equals ../../package.json
|
|
// (can't just check "/package.json" for Windows support).
|
|
if (package_json_url->get_pathname() == last_pathname) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto package_json_url_as_path =
|
|
url::FileURLToPath(realm->env(), *package_json_url);
|
|
CHECK(package_json_url_as_path);
|
|
return args.GetReturnValue().Set(
|
|
String::NewFromUtf8(realm->isolate(),
|
|
package_json_url_as_path->c_str(),
|
|
NewStringType::kNormal,
|
|
package_json_url_as_path->size())
|
|
.ToLocalChecked());
|
|
}
|
|
|
|
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
|
|
Local<ObjectTemplate> target) {
|
|
Isolate* isolate = isolate_data->isolate();
|
|
SetMethod(isolate, target, "readPackageJSON", ReadPackageJSON);
|
|
SetMethod(isolate,
|
|
target,
|
|
"getNearestParentPackageJSONType",
|
|
GetNearestParentPackageJSONType);
|
|
SetMethod(isolate,
|
|
target,
|
|
"getNearestParentPackageJSON",
|
|
GetNearestParentPackageJSON);
|
|
SetMethod(isolate, target, "getPackageScopeConfig", GetPackageScopeConfig);
|
|
}
|
|
|
|
void BindingData::CreatePerContextProperties(Local<Object> target,
|
|
Local<Value> unused,
|
|
Local<Context> context,
|
|
void* priv) {
|
|
Realm* realm = Realm::GetCurrent(context);
|
|
realm->AddBindingData<BindingData>(target);
|
|
}
|
|
|
|
void BindingData::RegisterExternalReferences(
|
|
ExternalReferenceRegistry* registry) {
|
|
registry->Register(ReadPackageJSON);
|
|
registry->Register(GetNearestParentPackageJSONType);
|
|
registry->Register(GetNearestParentPackageJSON);
|
|
registry->Register(GetPackageScopeConfig);
|
|
}
|
|
|
|
} // namespace modules
|
|
} // namespace node
|
|
|
|
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
|
|
modules, node::modules::BindingData::CreatePerContextProperties)
|
|
NODE_BINDING_PER_ISOLATE_INIT(
|
|
modules, node::modules::BindingData::CreatePerIsolateProperties)
|
|
NODE_BINDING_EXTERNAL_REFERENCE(
|
|
modules, node::modules::BindingData::RegisterExternalReferences)
|