Merge pull request #6759 from dotCooCoo/fix/js-tosource-property-flags

Scripts: toSource refuses read-only, non-configurable and non-extensible data it cannot write back
This commit is contained in:
BFlat
2026-11-12 11:42:09 +07:00
committed by GitHub
4 changed files with 70 additions and 3 deletions
+27 -2
View File
@@ -9,14 +9,18 @@
//! a script's own method: a getter, a function, a date, a regular expression, an error, a boxed
//! primitive, an array with properties besides its elements, an object whose prototype is not
//! `Object.prototype`, a property that is not enumerable or any other object that is not plain
//! data throws a `TypeError`, and so does an object that contains itself. A value nested deeper, with more entries or longer text than the
//! data throws a `TypeError`, and so does an object that contains itself. A literal makes an
//! extensible object whose properties are writable and configurable and an array whose `length`
//! is writable, so a property that is not writable or not configurable, an object or array that is
//! not extensible (after `Object.preventExtensions`, `Object.seal` or `Object.freeze`) and an
//! array with a read-only `length` throw a `TypeError` too. A value nested deeper, with more entries or longer text than the
//! realm's [`crate::DataLimits`] allow throws a `RangeError`. Every value written takes fuel.
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use super::{Builder, arg, method};
use crate::heap::{Class, NativeFn, ObjId, Prop, key};
use crate::heap::{CONFIGURABLE, Class, NativeFn, ObjId, Object, Prop, WRITABLE, key};
use crate::interp::{ErrorType, Interp, JsResult};
use crate::value::{Value, array_index, index_key, number_to_str};
use crate::{ScriptItem, bytecode::Code};
@@ -289,10 +293,14 @@ impl Source {
Class::Ordinary => return Err(interp.type_error(NOT_PLAIN_OBJECT)),
_ => None,
};
let fixed = shape.as_ref().and(fixed_state(o));
let Some(shape) = shape else {
let what = format!("toSource: a {} cannot be written as source", interp.object(id)?.class.name());
return Err(interp.type_error(&what));
};
if let Some(what) = fixed {
return Err(interp.type_error(what));
}
self.ancestors.push(id);
let r = match shape {
Err(length) => self.array(interp, id, length),
@@ -347,6 +355,23 @@ const NOT_PLAIN_ARRAY: &str =
"toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source";
const NOT_PLAIN_OBJECT: &str =
"toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source";
const NOT_EXTENSIBLE: &str = "toSource: a non-extensible object cannot be written as source";
const READ_ONLY_LENGTH: &str = "toSource: an array with a read-only length cannot be written as source";
const FIXED_PROPERTY: &str = "toSource: a property that is not writable or configurable cannot be written as source";
/// Why the literal for the array or plain object `o` would not make it again: `o` is not
/// extensible, its `length` is read-only, or one of its data properties is not writable or not
/// configurable. A literal makes each of these open.
fn fixed_state(o: &Object) -> Option<&'static str> {
if !o.extensible {
return Some(NOT_EXTENSIBLE);
}
if matches!(o.class, Class::Array { length_writable: false, .. }) {
return Some(READ_ONLY_LENGTH);
}
let open = WRITABLE | CONFIGURABLE;
o.props.iter().any(|(_, p)| matches!(p, Prop::Data { flags, .. } if flags & open != open)).then_some(FIXED_PROPERTY)
}
/// The getter behind `item`; [`ScriptItem::Global`] is a value and has none.
pub(crate) fn getter(item: ScriptItem) -> Option<(&'static str, NativeFn)> {
+39
View File
@@ -126,6 +126,45 @@ fn to_source_refuses_what_is_not_plain_data() {
assert!(thrown(&mut r, "toSource({get g() { throw 'ran'; }})").starts_with("TypeError"), "the getter does not run");
}
#[test]
fn to_source_refuses_flags_and_extensibility_a_literal_does_not_make() {
let mut r = realm();
let property = "TypeError: toSource: a property that is not writable or configurable cannot be written as source";
let extensible = "TypeError: toSource: a non-extensible object cannot be written as source";
let length = "TypeError: toSource: an array with a read-only length cannot be written as source";
for (source, want) in [
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, configurable: true}); toSource(o)", property),
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, writable: true}); toSource(o)", property),
("var a = [1, 2]; Object.defineProperty(a, '1', {writable: false}); toSource(a)", property),
("var a = [1, 2]; Object.defineProperty(a, '0', {configurable: false}); a.toSource()", property),
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true}); toSource({x: [{b: o}]})", property),
("toSource(Object.preventExtensions({}))", extensible),
("toSource(Object.preventExtensions([]))", extensible),
("toSource(Object.seal({a: 1}))", extensible),
("toSource(Object.freeze({a: 1}))", extensible),
("toSource(Object.seal([1]))", extensible),
("toSource(Object.freeze([1]))", extensible),
("toSource([1, {b: Object.freeze({})}])", extensible),
("var a = []; Object.defineProperty(a, 'length', {writable: false}); toSource(a)", length),
("var a = [1, , 3]; Object.defineProperty(a, 'length', {writable: false}); toSource({a: a})", length),
] {
assert_eq!(thrown(&mut r, source), want, "{source}");
}
// The refused states are ones the literal would lose: evaluated, it is open again.
let lost = "var o = Object.freeze({a: 1}); var w = eval('({\"a\":1})');
[Object.isExtensible(o), Object.isExtensible(w), Object.getOwnPropertyDescriptor(w, 'a').writable].join()";
assert_eq!(text(&mut r, lost), "false,true,true");
}
#[test]
fn to_source_writes_properties_defined_with_every_flag_on() {
let mut r = realm();
let source = "var o = {}; Object.defineProperty(o, 'a', {value: 1, writable: true, enumerable: true, configurable: true});
var a = [0]; Object.defineProperty(a, '0', {value: 2, writable: true, enumerable: true, configurable: true});
toSource({o: o, a: a, e: {}, f: [], s: [1, , 3]})";
assert_eq!(text(&mut r, source), r#"({"o":({"a":1}), "a":[2], "e":({}), "f":[], "s":[1, , 3]})"#);
}
#[test]
fn to_source_of_a_string_with_many_escapes_throws_a_range_error_before_the_heap_limit() {
let mut r = realm();
+3 -1
View File
@@ -1765,7 +1765,9 @@ as it is. A string as the first argument is used as the text.
objects as the text of an expression that makes the same value, such as `({"a":[1, "x"]})`. A function, a getter, a
date, a regular expression, an error, a host object such as `vc`, an array with properties besides its elements, an
object whose prototype is not `Object.prototype`, a property that is not enumerable, or an object that contains
itself throws a `TypeError`. A value deeper than 64 levels, with more than 1,048,576 entries or with text longer than 16,777,216
itself throws a `TypeError`. So does a property that is not writable or not configurable, an object or array after
`Object.preventExtensions`, `Object.seal` or `Object.freeze`, and an array whose `length` is read-only, since the text
would make each of them writable, configurable and extensible again (VectorCraft's own rule). A value deeper than 64 levels, with more than 1,048,576 entries or with text longer than 16,777,216
characters throws a `RangeError`.
The run is one transaction. When the script ends, each document it changed gets one undo step named Run Script. A
+1
View File
@@ -740,6 +740,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Scripts: File › Scripts › Other Script… and `script.run` run a JavaScript file against the open documents as one transaction with one undo step for each changed document. A failed or stopped run puts every document back. `crates/engine/src/tests/script_run.rs`.
- Scripts: Window › Script Console shows each run's `$.write` and `$.writeln` output, `vc.log` entries, uncaught error with the script's name, line and column, and a report of its time, host calls and slowest commands, keeping at most 2,048 lines and counting the lines it dropped. `script.run`'s report carries the same data, and `script.console`, `script.console.clear` and `ui.copyScriptConsole` read, clear and copy it. `crates/engine/src/tests/script_console.rs`.
- Scripts: the `$` object has the other members the Dollar object page lists (https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/), from `$.about()` to `$.version`, and scripts have the global `localize(object, ...values)` (https://extendscript.docsforadobe.dev/extendscript-tools-features/localizing-extendscript-strings/), `toSource(value)` and `Object.prototype.toSource()`. Each run starts with the interface's locale, and what a script changes in `$` lasts only for that run. `crates/engine/src/tests/script_dollar.rs`, `crates/js/src/extension_tests.rs`.
- Scripts: `toSource` throws a `TypeError` for a property that is not writable or not configurable, a sealed, frozen or non-extensible object or array, and an array with a read-only `length` (VectorCraft's own rule). `crates/js/src/extension_tests.rs`.
- Scripts: the Script Console panel's report line reads as `script.console` gives it, `time unavailable` included on a platform without a clock; counts of host calls, dropped lines and cut lines take the singular for one, in each language's own plural forms; and a run that is no longer available still gets its end line (`crates/engine/src/tests/script_console.rs`, `crates/ui-egui/src/panels/tests_script_console.rs`).
- Scripts: an error `$.evalFile` throws for a file it cannot run is reported at that file's line and column only while no `catch` has taken it. Caught and thrown again later, in the script or in another file, it is reported where it was thrown again, and a new error whose message holds its text is reported where that error was thrown. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`.
- Scripts: an uncaught error that one or more `finally` blocks pass on unchanged, in the script or in a function it called, is reported at the line and column where it was thrown, in `script.run`'s report and the Script Console. An error a `finally` block throws is reported at that throw, and one its `return`, `break` or `continue` discards is not reported. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`.