mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 13:33:19 +00:00
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:
@@ -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)> {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user