node/src/node_modules.cc
Yagiz Nizipli a7dad43d15
src: simplify node modules traverse path
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>
2024-05-21 20:47:07 +00:00

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)