Files
node/src/node_modules.cc
James M Snell 11e753536e src: improve error handling in multiple files
PR-URL: https://github.com/nodejs/node/pull/56962
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2025-02-17 13:26:44 -08:00

678 lines
23 KiB
C++

#include "node_modules.h"
#include <cstdio>
#include "base_object-inl.h"
#include "compile_cache.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_url.h"
#include "path.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::External;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::LocalVector;
using v8::Name;
using v8::NewStringType;
using v8::Null;
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;
}
// In some systems, std::string is annotated to generate an
// AddressSanitizer: container-overflow error when reading beyond the end of
// the string even when we are still within the capacity of the string.
// https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow
// https://github.com/nodejs/node/issues/55584
// The next lines are a workaround to avoid this false positive.
size_t json_length = package_config.raw_json.size();
package_config.raw_json.append(simdjson::SIMDJSON_PADDING, ' ');
simdjson::padded_string_view json_view(package_config.raw_json.data(),
json_length,
package_config.raw_json.size());
// End of workaround
simdjson::ondemand::document document;
simdjson::ondemand::object main_object;
simdjson::error_code error =
binding_data->json_parser.iterate(json_view).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();
BufferValue 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());
ToNamespacedPath(realm->env(), &path);
auto package_json = GetPackageJSON(
realm, path.ToStringView(), is_esm ? &error_context : nullptr);
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 (is_permissions_enabled &&
!env->permission()->is_granted(
env,
permission::PermissionScope::kFileSystemRead,
current_path.generic_string())) [[unlikely]] {
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);
BufferValue path_value(realm->isolate(), args[0]);
// Check if the path has a trailing slash. If so, add it after
// ToNamespacedPath() as it will be deleted by ToNamespacedPath()
bool slashCheck = path_value.ToStringView().ends_with(kPathSeparator);
ToNamespacedPath(realm->env(), &path_value);
std::string path_value_str = path_value.ToString();
if (slashCheck) {
path_value_str.push_back(kPathSeparator);
}
std::filesystem::path path;
#ifdef _WIN32
std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
path = std::filesystem::path(wide_path);
#else
path = std::filesystem::path(path_value_str);
#endif
auto package_json = TraverseParent(realm, path);
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);
BufferValue path_value(realm->isolate(), args[0]);
// Check if the path has a trailing slash. If so, add it after
// ToNamespacedPath() as it will be deleted by ToNamespacedPath()
bool slashCheck = path_value.ToStringView().ends_with(kPathSeparator);
ToNamespacedPath(realm->env(), &path_value);
std::string path_value_str = path_value.ToString();
if (slashCheck) {
path_value_str.push_back(kPathSeparator);
}
std::filesystem::path path;
#ifdef _WIN32
std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
path = std::filesystem::path(wide_path);
#else
path = std::filesystem::path(path_value_str);
#endif
auto package_json = TraverseParent(realm, path);
if (package_json == nullptr) {
return;
}
Local<Value> value;
if (ToV8Value(realm->context(), package_json->type).ToLocal(&value)) {
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);
if (!file_url) {
url::ThrowInvalidURL(realm->env(), resolved.ToStringView(), std::nullopt);
return;
}
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);
Local<Value> ret;
if (ToV8Value(realm->context(), *package_json_url_as_path, realm->isolate())
.ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
void FlushCompileCache(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!args[0]->IsBoolean() && !args[0]->IsUndefined()) {
THROW_ERR_INVALID_ARG_TYPE(env,
"keepDeserializedCache should be a boolean");
return;
}
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] module.flushCompileCache() requested.\n");
env->FlushCompileCache();
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] module.flushCompileCache() finished.\n");
}
void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "cacheDir should be a string");
return;
}
Utf8Value value(isolate, args[0]);
CompileCacheEnableResult result = env->EnableCompileCache(*value);
Local<Value> values[3];
values[0] = v8::Integer::New(isolate, static_cast<uint8_t>(result.status));
if (ToV8Value(context, result.message).ToLocal(&values[1]) &&
ToV8Value(context, result.cache_directory).ToLocal(&values[2])) {
args.GetReturnValue().Set(
Array::New(isolate, &values[0], arraysize(values)));
}
}
void GetCompileCacheDir(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!env->use_compile_cache()) {
args.GetReturnValue().Set(v8::String::Empty(isolate));
return;
}
Local<Value> ret;
if (ToV8Value(context, env->compile_cache_handler()->cache_dir())
.ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer.
CHECK(args[1]->IsString());
CHECK(args[2]->IsUint32());
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!env->use_compile_cache()) {
return;
}
Local<String> source = args[0].As<String>();
Local<String> filename = args[1].As<String>();
CachedCodeType type =
static_cast<CachedCodeType>(args[2].As<v8::Uint32>()->Value());
auto* cache_entry =
env->compile_cache_handler()->GetOrInsert(source, filename, type);
if (cache_entry == nullptr) {
return;
}
v8::LocalVector<v8::Name> names(isolate,
{FIXED_ONE_BYTE_STRING(isolate, "external")});
v8::LocalVector<v8::Value> values(isolate,
{v8::External::New(isolate, cache_entry)});
if (cache_entry->cache != nullptr) {
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] retrieving transpile cache for %s %s...",
cache_entry->type_name(),
cache_entry->source_filename);
std::string_view cache(
reinterpret_cast<const char*>(cache_entry->cache->data),
cache_entry->cache->length);
Local<Value> transpiled;
// TODO(joyeecheung): convert with simdutf and into external strings
if (!ToV8Value(context, cache).ToLocal(&transpiled)) {
Debug(env, DebugCategory::COMPILE_CACHE, "failed\n");
return;
} else {
Debug(env, DebugCategory::COMPILE_CACHE, "success\n");
}
names.push_back(FIXED_ONE_BYTE_STRING(isolate, "transpiled"));
values.push_back(transpiled);
} else {
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] no transpile cache for %s %s\n",
cache_entry->type_name(),
cache_entry->source_filename);
}
args.GetReturnValue().Set(Object::New(
isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
}
void SaveCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
DCHECK(env->use_compile_cache());
CHECK(args[0]->IsExternal());
CHECK(args[1]->IsString()); // TODO(joyeecheung): accept buffer.
auto* cache_entry =
static_cast<CompileCacheEntry*>(args[0].As<External>()->Value());
Utf8Value utf8(isolate, args[1].As<String>());
env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView());
}
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);
SetMethod(isolate, target, "enableCompileCache", EnableCompileCache);
SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir);
SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry);
SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry);
}
void BindingData::CreatePerContextProperties(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Realm* realm = Realm::GetCurrent(context);
realm->AddBindingData<BindingData>(target);
Isolate* isolate = context->GetIsolate();
LocalVector<Value> compile_cache_status_values(isolate);
#define V(status) \
compile_cache_status_values.push_back( \
FIXED_ONE_BYTE_STRING(isolate, #status));
COMPILE_CACHE_STATUS(V)
#undef V
USE(target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "compileCacheStatus"),
Array::New(isolate,
compile_cache_status_values.data(),
compile_cache_status_values.size())));
LocalVector<Name> cached_code_type_keys(isolate);
LocalVector<Value> cached_code_type_values(isolate);
#define V(type, value) \
cached_code_type_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, #type)); \
cached_code_type_values.push_back(Integer::New(isolate, value)); \
DCHECK_EQ(value, cached_code_type_values.size() - 1);
CACHED_CODE_TYPES(V)
#undef V
USE(target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "cachedCodeTypes"),
Object::New(isolate,
Null(isolate),
cached_code_type_keys.data(),
cached_code_type_values.data(),
cached_code_type_keys.size())));
}
void BindingData::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(ReadPackageJSON);
registry->Register(GetNearestParentPackageJSONType);
registry->Register(GetNearestParentPackageJSON);
registry->Register(GetPackageScopeConfig);
registry->Register(EnableCompileCache);
registry->Register(GetCompileCacheDir);
registry->Register(FlushCompileCache);
registry->Register(GetCompileCacheEntry);
registry->Register(SaveCompileCacheEntry);
}
} // 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)