fix(navigator): expose readonly plugin collection lengths

This commit is contained in:
ldm0
2026-09-09 04:06:29 +08:00
committed by Donough Liu
parent ff384a8126
commit 22facd581d
3 changed files with 131 additions and 49 deletions
@@ -1,6 +1,6 @@
use super::super::*;
use crate::{
util::{define_v8_array_data_properties, get_private_value, throw_type_error},
util::{get_private_value, throw_type_error},
webidl,
};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
@@ -35,6 +35,7 @@ const PDF_MIME_TYPES: &[(&str, &str, &str)] = &[
const PLUGIN_ARRAY_BRAND_SLOT: &str = "__moliPluginArrayBrand";
const MIME_TYPE_ARRAY_BRAND_SLOT: &str = "__moliMimeTypeArrayBrand";
const PLUGIN_BRAND_SLOT: &str = "__moliPluginBrand";
const COLLECTION_LENGTH_SLOT: &str = "__moliNavigatorCollectionLength";
#[derive(WebApiObject)]
#[webapi(interface = "MimeType")]
@@ -53,34 +54,34 @@ struct MimeTypeObjectDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
struct MimeTypeArrayObjectDeclaration<'scope> {
#[webapi(prototype)]
prototype: Option<v8::Local<'scope, v8::Object>>,
#[webapi(interface = "MimeTypeArray")]
struct MimeTypeArrayObjectDeclaration {
#[webapi(slot = MIME_TYPE_ARRAY_BRAND_SLOT, init = true)]
brand: (),
#[webapi(slot = COLLECTION_LENGTH_SLOT)]
length: u32,
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
struct PluginArrayObjectDeclaration<'scope> {
#[webapi(prototype)]
prototype: Option<v8::Local<'scope, v8::Object>>,
#[webapi(interface = "PluginArray")]
struct PluginArrayObjectDeclaration {
#[webapi(slot = PLUGIN_ARRAY_BRAND_SLOT, init = true)]
brand: (),
#[webapi(slot = COLLECTION_LENGTH_SLOT)]
length: u32,
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Plugin")]
struct PluginObjectDeclaration<'scope> {
#[webapi(prototype)]
prototype: Option<v8::Local<'scope, v8::Object>>,
#[webapi(slot = PLUGIN_BRAND_SLOT, init = true)]
brand: (),
#[webapi(slot = COLLECTION_LENGTH_SLOT)]
length: u32,
#[webapi(data_property)]
name: v8::Local<'scope, v8::String>,
@@ -94,6 +95,9 @@ struct PluginObjectDeclaration<'scope> {
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "MimeTypeArray", enumerable)]
struct MimeTypeArrayPrototypeDeclaration {
#[webapi(accessor_property, getter = mime_type_array_length_getter)]
length: (),
#[webapi(method, callback = mime_type_array_item_callback, length = 1)]
item: (),
@@ -110,6 +114,9 @@ struct MimeTypeArrayPrototypeDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "PluginArray", enumerable)]
struct PluginArrayPrototypeDeclaration {
#[webapi(accessor_property, getter = plugin_array_length_getter)]
length: (),
#[webapi(method, callback = plugin_array_item_callback, length = 1)]
item: (),
@@ -129,6 +136,9 @@ struct PluginArrayPrototypeDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "Plugin", enumerable)]
struct PluginPrototypeDeclaration {
#[webapi(accessor_property, getter = plugin_length_getter)]
length: (),
#[webapi(method, callback = plugin_item_callback, length = 1)]
item: (),
@@ -160,6 +170,45 @@ pub(super) fn install_navigator_collection_template_bindings<'s>(
}
}
fn plugin_array_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
collection_length_getter_for(scope, args, rv, PLUGIN_ARRAY_BRAND_SLOT);
}
fn mime_type_array_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
collection_length_getter_for(scope, args, rv, MIME_TYPE_ARRAY_BRAND_SLOT);
}
fn plugin_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
collection_length_getter_for(scope, args, rv, PLUGIN_BRAND_SLOT);
}
fn collection_length_getter_for<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
brand_slot: &'static str,
) {
if !receiver_has_brand(scope, args.this(), brand_slot) {
throw_type_error(scope, "Illegal invocation");
return;
}
if let Some(length) = get_private_value(scope, args.this(), COLLECTION_LENGTH_SLOT) {
rv.set(length);
}
}
fn plugin_array_item_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -301,23 +350,21 @@ fn build_plugin<'s>(
scope: &mut v8::PinScope<'s, '_>,
name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let plugin = v8::Array::new(scope, PDF_MIME_TYPES.len() as i32);
PluginObjectDeclaration::new(
global_constructor_prototype(scope, "Plugin"),
let plugin = PluginObjectDeclaration::new(
PDF_MIME_TYPES.len() as u32,
v8_string(scope, name)?,
v8_string(scope, "internal-pdf-viewer")?,
v8_string(scope, "Portable Document Format")?,
)
.initialize(scope, plugin.into())
.bind(scope)
.ok()?;
let mut mime_types = Vec::with_capacity(PDF_MIME_TYPES.len());
for (type_name, suffixes, description) in PDF_MIME_TYPES {
let mime_type =
build_mime_type(scope, type_name, suffixes, description, Some(plugin.into()))?;
let mime_type = build_mime_type(scope, type_name, suffixes, description, Some(plugin))?;
mime_types.push((type_name, mime_type));
}
define_v8_array_data_properties(scope, plugin, mime_types.iter().map(|(_, item)| *item))?;
define_collection_indices(scope, plugin, mime_types.iter().map(|(_, item)| *item))?;
for (type_name, mime_type) in mime_types {
let _ = plugin.define_own_property(
scope,
@@ -327,16 +374,15 @@ fn build_plugin<'s>(
);
}
Some(plugin.into())
Some(plugin)
}
fn build_mime_type_array<'s>(
scope: &mut v8::PinScope<'s, '_>,
enabled_plugin: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let array = v8::Array::new(scope, PDF_MIME_TYPES.len() as i32);
MimeTypeArrayObjectDeclaration::new(global_constructor_prototype(scope, "MimeTypeArray"))
.initialize(scope, array.into())
let array = MimeTypeArrayObjectDeclaration::new(PDF_MIME_TYPES.len() as u32)
.bind(scope)
.ok()?;
let mut mime_types = Vec::with_capacity(PDF_MIME_TYPES.len());
for (type_name, suffixes, description) in PDF_MIME_TYPES {
@@ -349,7 +395,7 @@ fn build_mime_type_array<'s>(
)?;
mime_types.push((type_name, mime_type));
}
define_v8_array_data_properties(scope, array, mime_types.iter().map(|(_, item)| *item))?;
define_collection_indices(scope, array, mime_types.iter().map(|(_, item)| *item))?;
for (type_name, mime_type) in mime_types {
let _ = array.define_own_property(
scope,
@@ -358,20 +404,19 @@ fn build_mime_type_array<'s>(
v8::PropertyAttribute::DONT_ENUM,
);
}
Some(array.into())
Some(array)
}
fn build_plugin_array<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<v8::Local<'s, v8::Object>> {
let array = v8::Array::new(scope, PDF_PLUGIN_NAMES.len() as i32);
PluginArrayObjectDeclaration::new(global_constructor_prototype(scope, "PluginArray"))
.initialize(scope, array.into())
let array = PluginArrayObjectDeclaration::new(PDF_PLUGIN_NAMES.len() as u32)
.bind(scope)
.ok()?;
let mut plugins = Vec::with_capacity(PDF_PLUGIN_NAMES.len());
for name in PDF_PLUGIN_NAMES {
let plugin = build_plugin(scope, name)?;
plugins.push((name, plugin));
}
define_v8_array_data_properties(scope, array, plugins.iter().map(|(_, item)| *item))?;
define_collection_indices(scope, array, plugins.iter().map(|(_, item)| *item))?;
for (name, plugin) in plugins {
let _ = array.define_own_property(
scope,
@@ -380,7 +425,21 @@ fn build_plugin_array<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<v8::Local<
v8::PropertyAttribute::DONT_ENUM,
);
}
Some(array.into())
Some(array)
}
fn define_collection_indices<'s>(
scope: &mut v8::PinScope<'s, '_>,
collection: v8::Local<'s, v8::Object>,
items: impl IntoIterator<Item = v8::Local<'s, v8::Object>>,
) -> Option<()> {
for (index, item) in items.into_iter().enumerate() {
let key = v8_string(scope, &index.to_string())?;
if collection.create_data_property(scope, key.into(), item.into()) != Some(true) {
return None;
}
}
Some(())
}
pub(super) struct NavigatorPluginCollections<'scope> {
@@ -22042,6 +22042,45 @@ fn zhihu_probe_navigator_plugin_and_mime_surfaces_match_chromium_pdf_builtins()
r#"{"languages":["en-US","en"],"hardwareConcurrencyPositive":true,"pluginsCtorType":"function","pluginsCtorName":"PluginArray","pluginsTag":"[object PluginArray]","pluginsLength":5,"pluginsInstanceof":true,"plugin0CtorName":"Plugin","plugin0Instanceof":true,"plugin0Name":"PDF Viewer","pluginNamedItem":"PDF Viewer","mimeTypesCtorType":"function","mimeTypesCtorName":"MimeTypeArray","mimeTypesTag":"[object MimeTypeArray]","mimeTypesLength":2,"mimeTypesInstanceof":true,"mime0CtorName":"MimeType","mime0Instanceof":true,"mime0Type":"application/pdf","mime0EnabledPluginIsFirst":true,"mime1EnabledPluginIsFirst":true,"mimeNamedItem":"application/pdf","pdfViewerEnabled":true}"#
);
}
#[test]
fn navigator_plugin_collection_lengths_are_branded_readonly_prototype_attributes() {
let mut vm = new_storage_test_vm("https://navigator-collections-length.test/");
let result = vm.eval(r#"
(() => {
const collections = [navigator.plugins, navigator.mimeTypes, navigator.plugins[0]];
function throwsTypeError(callback) {
try { callback(); return false; } catch (error) { return error instanceof TypeError; }
}
for (const [index, collection] of collections.entries()) {
const prototype = Object.getPrototypeOf(collection);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'length');
if (!descriptor || descriptor.get.name !== 'get length' || descriptor.get.length !== 0 ||
descriptor.set !== undefined || !descriptor.enumerable || !descriptor.configurable ||
Object.hasOwn(collection, 'length') || Array.isArray(collection)) return 'descriptor';
const length = index === 0 ? 5 : 2;
const first = collection[0];
collection.length = 0;
collection.__moliNavigatorCollectionLength = 0;
if (collection.length !== length || collection[0] !== first ||
Array.from(collection).length !== length || collection.item(0) !== first)
return 'assignment changed collection';
if (!throwsTypeError(() => { 'use strict'; collection.length = 0; })) return 'strict';
for (const fake of [{}, [], Object.create(collection), prototype, collections[(index + 1) % 3]]) {
if (!throwsTypeError(() => descriptor.get.call(fake))) return 'brand';
}
}
const iframe = document.createElement('iframe');
document.appendChild(document.createElement('html')).appendChild(iframe);
const foreign = iframe.contentWindow.navigator;
const foreignCollections = [foreign.plugins, foreign.mimeTypes, foreign.plugins[0]];
return collections.every((collection, i) =>
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(collection), 'length')
.get.call(foreignCollections[i]) === collection.length);
})()
"#).expect("collection length probe should evaluate");
assert_eq!(result, "true");
}
#[test]
fn navigator_plugin_collections_parse_webidl_arguments() {
let mut vm = new_storage_test_vm("https://navigator-collections-webidl.test/");
-16
View File
@@ -168,22 +168,6 @@ pub(crate) fn apply_webidl_constructor_prototype_fallback<'s>(
let _ = receiver.set_prototype(scope, prototype.into());
}
pub(crate) fn define_v8_array_data_properties<'s, I, T>(
scope: &mut v8::PinScope<'s, '_>,
array: v8::Local<'s, v8::Array>,
values: I,
) -> Option<()>
where
I: IntoIterator<Item = T>,
T: WebApiValue<'s>,
{
for (index, value) in values.into_iter().enumerate() {
let value = value.to_v8_value(scope)?;
define_v8_array_data_property(scope, array, index as u32, value)?;
}
Some(())
}
pub(super) fn create_script_origin<'s>(
scope: &mut v8::PinScope<'s, '_>,
resource_name: &str,