| #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 "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::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(); |
| |
| 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 (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); |
| 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( |
| std::filesystem::path::preferred_separator); |
| |
| ToNamespacedPath(realm->env(), &path_value); |
| |
| std::string path_value_str = path_value.ToString(); |
| if (slashCheck) { |
| path_value_str.push_back(std::filesystem::path::preferred_separator); |
| } |
| |
| auto package_json = |
| TraverseParent(realm, std::filesystem::path(path_value_str)); |
| |
| 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( |
| std::filesystem::path::preferred_separator); |
| |
| ToNamespacedPath(realm->env(), &path_value); |
| |
| std::string path_value_str = path_value.ToString(); |
| if (slashCheck) { |
| path_value_str.push_back(std::filesystem::path::preferred_separator); |
| } |
| |
| auto package_json = |
| TraverseParent(realm, std::filesystem::path(path_value_str)); |
| |
| 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) |