Skip to content

A name Object.prototype carries is a name like any other: own-key lookups for classes, fields, parts and property bags - #78

Merged
ww-mw merged 6 commits into
mainfrom
fix-inherited-key-lookups
Oct 12, 2026
Merged

ww-mw merged 6 commits into
mainfrom
fix-inherited-key-lookups

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 12, 2026

Copy link
Copy Markdown
Member

Core looked up names a file spells (class names, struct field names, property names, zip member and OPC part names, attribute names, project location keys) in plain object literals and plain {} accumulators. An object literal answers every name Object.prototype carries with an inherited member: toString, valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable and toLocaleString are all valid MATLAB class and field names. A write of __proto__ into a plain {} calls the prototype setter instead of storing a key. MATLAB cannot write that name, but a file can carry it. #73 fixed this for the System Composer catalog (ScCatalog.ts). This PR applies the same approach everywhere else a census of src/ found it reachable from file content:

Every bag a reader hands out stays an ordinary object with Object.prototype.

Nothing in this PR writes to Object.prototype itself, and no site found before it did. The census looked specifically for a nested a[k1][k2] = v with a file-derived k1, and the only "look up, then mutate" (MatParser's fields[fn].push) threw rather than writing. The barrel (src/index.ts) and every exported type are unchanged. Two module exports are new, and the barrel carries neither: kindIn (kindMap.ts) and setOwn (parser/ownKeys.ts).

Six commits on origin/main 15831c2. Each includes its rebuilt dist/. At each one, a fresh rm -rf dist && npm run build in a clean worktree leaves git status --porcelain empty, and the full suite passes.

1. Node class and Kind lookups

NodeClassMap (CLASS_MAP, through getClass and parseValue), kindMap (KIND_BY_CLASS, DERIVED_KIND_BY_CLASS, KIND_BY_CLASSIFICATION, in kindForClass), DataNode.kind, schema/index.ts (CLASS_DEFS, CLASS_ALIASES), and MatlabVariableNode's MCOS_ICON_MAP.

Before, measured on test/fixtures/rt_text.sldd and rt_bin.sldd with the entry's class renamed from MyGadget:

  • A dictionary holding one entry of such a class failed to open at all, in either format: TypeError: NodeClass.parse is not a function. That held for each of toString, valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString, __proto__ and __defineGetter__. Controls: Simulink.Parameter opened as a ParameterNode, and Unknown.Klass / Foo.Bar opened as ObjectNodes whose Kind is the class name.
  • With only the open fixed, DataNode.kind was the inherited member: a function for toString, the Object function for constructor, and Object.prototype for __proto__. That reached toRow().Kind and toDTO().kind. A derived entry got the same answer through DERIVED_KIND_BY_CLASS.
  • kindForClass('toString') returned Object.prototype.toString, kindForClass('constructor') returned Object, and kindForClass('__proto__') returned Object.prototype. A classification token spelled that way did the same. mathworks/data-explorer-vscode labels a drag with kindForClass (src/webview/dropDecision.ts, kindInTarget).
  • getSchema mapped over the props of an inherited function and threw Cannot read properties of undefined (reading 'map'). Every setProperty asks for the schema first (trySetSchemaProperty), so such an entry could not be renamed or edited once it opened.
  • In a .mat, the MCOS bridge asked getClass whether it knows the class and was told yes, and then the typed dispatch threw. test/fixtures/mcos/object_props.mat with its Vehicle object renamed valueOf showed that object as an opaque variable without its five property rows, plus a part-unreadable warning naming the TypeError. With the subsystem left out, the opaque object's icon id was the inherited function.

Now such a class is what any unlisted class is: an ObjectNode with the raw class name as its Kind, the object icon, and the same rows as Unknown.Klass read from the same file. It can be renamed, edited, saved and reopened. mcosTypedNode.ts:83 (isKnown) is fixed through getClass.

2. MAT struct fields

MatParser's struct fields, MatlabVariableNode._buildVarObject and _serializeStructValue, and McosParser.buildStructValue and buildProperties.

Before: a 1x2 struct array with a field named constructor, toString, valueOf, hasOwnProperty (or any of the nine names), written by core's own encodeMatVariable, made parseMat and DataModel.addMatSource throw fields[fn].push is not a function, so the .mat did not open. A 1x1 struct's __proto__ field silently vanished.

Once the reader kept it, three more writes into {} lost it on the way out:

  • A 1x2 struct copied into a text dictionary after an edit kept _fields: ["a","__proto__"] but lost the values, and reopened showing [object Object] (read off Object.prototype).
  • A 1x1x2 struct, which a text dictionary holds as a MAT stream, lost the field.
  • In MCOS, a v.Specs struct patched to one field named __proto__ showed no field at origin/main. With f.FleetName renamed __proto__, f's property bag took the string's envelope as its prototype.

Now a field's presence is an own-key question, and every write goes through setOwn. parseMat is public, and the fields it hands out stays an ordinary object with Object.prototype, so fields.hasOwnProperty(…), String(fields) and toStrictEqual behave as before. _buildVarObject's fields is the same. Every such field round-trips through the tree, a text-dictionary paste and a binary-dictionary paste. An MCOS object's __proto__ property still gets no row, because ObjectNode reserves every _-named key (_addPropertyChildren), but it no longer re-parents the bag.

3. Zip members and text-OPC parts named __proto__

Inflate (the native central-directory walk and the fflate fallback), MdlParser's part splitter, and SlxParser.extractConfigSets.

Before: every member bag was a plain {}, so a member named __proto__ became the bag's prototype. parseSlx walks the bag with for…in, so a 20 KB .slx holding a 20,000,000-byte member of that name (zeros) took 2.7 s and then failed to open with RangeError: Too many properties to enumerate, under either engine. A text-OPC .mdl with a 20 MB part at /__proto__ failed the same way after 2.6 s. Separately, a configSetInfo entry naming part /toString (or constructor, hasOwnProperty, __proto__) was reported as a part that "could not be read (The "list" argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView.)" rather than one the package does not contain.

Now both engines and the .mdl splitter leave such a member out, and the part lookup is by own key. Leaving it out is what every caller already saw of that member. Keeping it isn't possible: fflate's zipSync, which serializeBinarySldd calls, throws on an own __proto__ key (measured: Cannot read properties of undefined (reading 'level')). The .slx and .mdl open as the same model as their controls (JSON.stringify / toEqual against the clean parse).

As before, no ParseWarning says a member was dropped. The zip readers have no warnings channel, and the .mdl splitter has one (it warns for a part header with no path) but stays silent here to match them. That is a follow-up, below.

4. Dictionary struct fields

BinarySlddParser.parseStructElement, StructNode.serializeElement, and StructNode.parse's field reads.

Before, on rt_text.sldd and rt_bin.sldd's gadget.Specs with color renamed:

  • A binary <P Name="__proto__"> field vanished on open.
  • A text __proto__ field opened as 'red', but after an edit and a save it reopened as [object Object].
  • A field that _fields names and the element doesn't hold (a hand-edited file) showed function Object() { [native code] } for constructor and function toString() { [native code] } for toString. Any other such name reads as absent.

Now the reader and the writer write each name with setOwn, and the read is by own key (fieldOf). All nine names open, edit, save and reopen as fields in both formats. The reader still assigns into a plain {}, so opening a binary dictionary costs what it did (see Performance).

5. Two more static tables

  • ProjectPage's LOCATION_LABEL[key] ?? key, where key is a project store's Reference Location. Read through parseProject from a monolithic store, a location keyed toString, constructor, valueOf or hasOwnProperty got an inherited function as its label, and __proto__ got Object.prototype. Now each is labelled with its raw key, as NewInSomeFutureRelease is.
  • MatWriter's CLASS_CODE[className], where className can be a text dictionary's _type. Such a class got past the "no MAT class" refusal and failed later as no payload width for MAT element type undefined. Both are MatWriteErrors, and every caller (textDictionaryForm, _matStream) handles them identically, so only the reason changed. It now refuses with no MAT class for "toString".

6. __proto__ keys in a dictionary's property bags

Before: the save paths copied each bag with Object.assign({}, bag, …). The copies were in DataNode (_serializeSimulinkObject, _mergeProps, _getSerializedProperties, writeIntoSaveobj), SimulinkObjectNode._propsOf, ParameterNode, SignalNode, BaseBusNode and its element, EnumTypeNode and its enumeral, and ObjectNode. In a text dictionary saved with no edits:

  • A Simulink.Parameter and a Simulink.Signal lost a __proto__ property.
  • A CustomObject whose __proto__ property held a _saveobj key read as a custom-saving class (SAVEOBJ_KEY in stored on the re-parented copy). The untouched save gained a _custom_save envelope the file never had.

Two bags filled key by key from the file also lost keys:

  • EncodedValue dropped an encoded element's __proto__ attribute on write-back (0 of 17 kept on sparse_binary.sldd).
  • DataNode.textDictionaryMetadata dropped metadata keys named toString or constructor (key in out) and __proto__.

Now every such copy is an object spread. That keeps key order and later-wins overrides, and defines each key as an own property. The two key-by-key bags write through setOwn, and the metadata's presence check is own-key. Copies whose keys are the node's own (_gatedProps, the _serializedOverrides literals, Object.assign(stored, propOverrides)) are unchanged, and so is ModelNode's copy of zip members, which can no longer hold that name.

Performance

Opening is as fast as at origin/main. These are medians of five processes, alternating between the dist trees, each process timing three ingest opens after one warm-up.

File origin/main 15831c2 this branch earlier form (8016698, on fcf4b46)
binsldd/dd140000.sldd (150,004 nodes) 2279 ms 2269 ms (−0.4%) 2426 ms (+6.5%)
customer/Controller_cummins.sldd (72,363) 964 ms 973 ms (+0.9%) 1006 ms (+4.4%)
customer/PTSA_SysIf_bosch.sldd (25,669) 521 ms 524 ms (+0.6%) 544 ms (+4.4%)

The earlier form built the binary reader's struct and object bags, and EncodedValue's, with Object.fromEntries over [name, value] pairs. An earlier run with commit 4 alone against both gave 3.8–4.7% for that form, and −0.4% to +0.6% for this one.

Two more spot checks, three alternating processes each:

  • perf/slx/large.slx opens in 642–675 ms at base and 642–649 ms here.
  • proj/…/allMidi.mat (2,016,325 nodes) takes 5166–5393 ms at base (median 5288) and 5183–5475 ms here (median 5232), over five alternating processes with a GC before each open. parseMat alone takes 1636–1640 ms at base and 1595–1651 ms here, and MatNode.fromParsed alone 2092–2127 ms at base and 2091–2119 ms here.

Tests

147 new cases. Every one goes through the real parse path (ingest/parseMat/parseSlx/parseMdl/parseProject on fixture-derived files), except kindForClass, getSchema/getLayout, getClass, encodeMatVariable, unzipEntries, and one icon case on parseMatVariable. No existing test is modified. Two import lists gain a name (schemaResolve, projectPage). Two pieces of this PR's own test code change in later commits: test/tools/matRename.ts (added in commit 1) takes raw bytes from commit 2 on, and commit 4 folds commit 2's .mat copy test into a helper it shares with a new binary-dictionary venue.

Commit Cases Files Fail on the previous commit's source Pinned per fix (each fix put back alone)
1 89 test/nodeClassMap.test.ts (new), kindMap, schema/schemaResolve, mcosTypedNode, objectIcon 85 (the 4 controls pass) class map 56, kindForClass 18, DataNode.kind 55, classDef 27, icon 2
2 23 matStructArray, mcosParser 14 (the 1x1 non-__proto__ cases and the control pass, as a plain {} held them) MatParser 14, _buildVarObject 1, _serializeStructValue 1, buildStructValue 1, buildProperties 1; a prototype-less fields fails 18
3 9 inflateSeam (each engine), mdlParser, parseWarnings 9 native walk 2, fflate filter 2, .mdl splitter 1, configSetInfo lookup 4
4 5 structNode, matStructArray 5 binary reader 3, serializeElement 2, own-key read 1
5 12 projectPage, matWriter 10 (the 2 controls pass) 5 for each table
6 9 absentPropertyWriteBack, encodedValueWriteBack 9 DataNode 6, BaseBusNode 4, ParameterNode 3, EnumTypeNode 2, ObjectNode 1, SignalNode 1, SimulinkObjectNode 1, EncodedValue 1

How the fixtures were built:

  • test/tools/matRename.ts (new) derives a .mat from a MATLAB-written one by a same-length replacement inside its compressed elements (text or raw bytes), and moves the header's subsystem offset to match.
  • The MAT struct files come from core's encodeMatVariable, with the 8-byte empty name element of the member it writes replaced by a small-element s.
  • The zip with a __proto__ member is written under a same-length placeholder and renamed in the local header and the central directory, because fflate's zipSync cannot be handed that key.
  • The property-bag cases in commit 6 are differential. The same value goes in under __proto__ and under _zzproto_ in every object of every entry's value (text) or every object element (binary). That covers the parity corpus's cases.sldd and the bus_types and custom_object fixtures, plus one bag beside a _custom_save envelope. Each file is saved untouched and again after every entry is renamed, and the two saves must be the same file but for the name.

Census: checked and left alone

  • fast-xml-parser 5.10.1 treats three kinds of element name differently:

    • __proto__, constructor or prototype are refused with [SECURITY] Invalid name, for the whole part. Measured: one <constructor/> anywhere in rt_bin.sldd's chunk makes the dictionary open with no entries and a source-unreadable warning naming the reason.
    • hasOwnProperty, toString, valueOf and the four __define…/__lookup… names are renamed with a __ prefix (<toString> becomes __toString).
    • Every other name, including isPrototypeOf, is kept as an own key.

    Attribute names are @_-prefixed first, so none of this applies to them. No reader asks for an element of any of those names, so a renamed or kept one is ignored exactly as an unknown element is. The refusal is unconditional (sanitizeName runs after transformTagName), no MATLAB writer produces such an element, nothing is polluted, and the loss is reported. Avoiding it would mean rewriting the XML before parsing it. The same applies to model and project XML parts.

  • Keys from a fixed vocabulary:

    • SECTION_NAMESPACE / ALLOWED_TYPES: design, arch, config, other.
    • FORMAT_LABEL: the reader's own layout names.
    • ATOM_BY_KEY and the schema REGISTRY: schema JSON keys.
    • CLASS_NAMES, ELEMENT_WIDTH, PAYLOAD_TYPE, WIDTH and McosParser's tables: numeric keys.
    • The 'Min' in sp / key in stored write-back gates in Bus, ConnectionBus, Signal, ValueType and SimulinkObjectNode: literal keys.
  • Looked up through Maps or Sets: NON_DATA_BLOCK_PARAMS and ENUM_BLOCK_PARAMS (through the NON_DATA_PARAMS Map), ProjectParser, UsageIndex, DataModel, typeLinkIndex, deletionPlan, maskScope and the scans. smol-toml 1.9's tables are prototype-less and keep a __proto__ key as an own key (measured).

  • Already guarded: DictionaryXmlFast (five interned tags behind hasOwnProperty, attributes @_-prefixed), and ScCatalog (Classify System Composer names by own key, and draw entry uuids from crypto.getRandomValues #73).

  • Written with file keys but only read by literal keys: SlxParser directProps, MdlParser flatProps (a __proto__ parameter is dropped there, and no read asks for it), and SlxParser rawContents (keys end .xml/.json).

  • Zip-member bags downstream of Inflate (ingest's .prj files, BinarySlddParser zipMetadata, BinarySlddSerializer, ModelNode): they can no longer hold a __proto__ member.

  • Host input, not file content:

    • DataNode.setProperty's self[resolved] when no prop matches. A host passing constructor would shadow node.constructor. The PI "Other" rows built from file keys are read-only.
    • getSectionMetadata('toString').
    • EventBus topics.

Pre-existing, not caused by this branch

  • complex/base.sldd, complex/plant.sldd and complex/shared.sldd (local corpus) fail to save at origin/main 15831c2 and here alike. The error is TypeError: Cannot read properties of undefined (reading '0') at ParameterNode.serializeValue, (result._elements)[0].
  • ObjectNode._reservedProps reads serial._properties, which ObjectNode.parse never sets, so it always sees {}. A user-class object's save rebuilds its bag from its rows, so a _-named key in that bag is not written back, __proto__ and _zz alike. Measured on custom_object_text.sldd: with _zz in each of its 12 property bags, an untouched save kept 5 and lost the 7 under ObjectNodes.

Follow-ups

  • A dropped __proto__ zip member or text-OPC part gets no ParseWarning. unzipEntries would need a warnings channel to report it for .slx, binary .sldd and .prj. The .mdl splitter could warn today, but would then be the only format that does.

Verification

  • npm run verify at the head passes: typecheck, build, smoke, test, check:pack, check:leak and check:browser. Tests: 206 files passed and 1 skipped, 6784 passed and 26 skipped. origin/main 15831c2 has 6637 passed and 26 skipped, so the difference is the 147 new cases.
  • Full suite at each commit: 6726, 6749, 6758, 6763, 6775 and 6784 passed, 26 skipped each time.
  • Differential, origin/main against this branch: 447 files, being every .sldd, .slx, .mdl and .mat in test/ and the local corpus. One file over 200 MB was skipped. For each file I compared every node's display name, class, Kind, value and icon (3,281,926 nodes), the source's warnings, and the bytes of an untouched save (219 dictionaries).
    • Nodes and warnings are identical for all 446 files compared.
    • All 219 saves are identical except binsldd/dd1400.sldd, whose zip bytes differ only by fflate's member timestamp. Its unzipped members are identical.
    • Re-run with the clock frozen (zip member times and lastmod stamps), over the 437 distinct files and 10 project folders, hashing every model and every save output (untouched save, copy, rename and save, reopen, every node rebuilt and saved, paste into a text and a binary dictionary): 0 differences in every step. The dd1400.sldd difference above is the clock alone.
    • The same 10 files fail to open in both (v7.3 MAT-files), and the same 3 fail to save in both (above).

ww-mw added 6 commits October 12, 2026 00:11
…c object it is

The node class map, the three Kind tables, the class-schema tables and the opaque-object
icon table are object literals, and each was asked about a class name the file supplies by
plain indexing. An object literal answers every name Object.prototype carries. toString,
valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable and toLocaleString
are valid MATLAB class names, and a file can also spell __proto__ and __defineGetter__.
Measured on rt_text.sldd and rt_bin.sldd with the entry's class renamed from MyGadget, and on
test/fixtures/mcos/object_props.mat with its Vehicle object renamed valueOf:

  - NodeClassMap.parseValue took CLASS_MAP[class] for the node class and called its parse,
    so a dictionary holding one such entry failed to open at all, text and binary alike:
    "TypeError: NodeClass.parse is not a function", for every one of the nine names.
  - DataNode.kind and kindForClass returned the inherited member as the Kind: a function
    for toString, the Object function for constructor, Object.prototype for __proto__. The
    Kind column, toRow().Kind and toDTO().kind all carried it, and so would the drop tooltip
    of mathworks/data-explorer-vscode, which labels a drag with kindForClass. A derived entry
    reached the same answer through DERIVED_KIND_BY_CLASS.
  - getSchema mapped over the `props` of CLASS_DEFS[class], an inherited function, and threw
    "Cannot read properties of undefined (reading 'map')". Every setProperty asks for the
    schema first (trySetSchemaProperty), so once such an entry could open, it could not be
    renamed or edited.
  - In a .mat, the MCOS bridge asked getClass whether it knows the class, was told yes, and
    the typed dispatch threw. The object degraded to an opaque variable without its five
    property rows, with a part-unreadable warning naming the TypeError. With the subsystem
    left out, so that the object stays opaque, its icon id was the inherited function.

Each lookup is now of an own key. getClass checks hasOwnProperty, and parseValue goes through
getClass. kindMap gains kindIn, which kindForClass and DataNode.kind both use; it is a module
export the barrel does not carry. schema/index.ts has a local helper for classDef, because
schema/ imports nothing. MatlabVariableNode.icon checks MCOS_ICON_MAP the same way. Such a
class is now what any class the tables do not list is: an ObjectNode whose Kind is the raw
class name, with the object icon and the same rows as an Unknown.Klass read from the same file.

89 new cases, in test/nodeClassMap.test.ts (new), test/kindMap.test.ts,
test/schema/schemaResolve.test.ts, test/mcosTypedNode.test.ts and test/objectIcon.test.ts.
test/tools/matRename.ts derives a .mat from a MATLAB-written one by renaming a token inside
its compressed elements, at the same length, and moves the header's subsystem offset to
match. Against origin/main's source (15831c2), 85 of the 89 fail and the 4 controls pass. Here
all 89 pass. With each fix put back alone, its own cases fail: the class map 56, kindForClass
18, DataNode.kind 55, classDef 27, the icon 2. Full suite: 6726 passed, 26 skipped.
… other

MatParser gathered a struct's fields into a plain `{}` keyed by the names the file spells,
and for a struct array it asked `if (!fields[fn]) fields[fn] = []` before pushing each
element's value. constructor, toString, valueOf, hasOwnProperty, isPrototypeOf,
propertyIsEnumerable and toLocaleString are legal MATLAB field names, and on a plain `{}`
each is already "there" as an inherited member, so the `[]` was skipped and the push went to
a function. A 1x2 struct array with any of them, written by core's own encodeMatVariable, made
parseMat and DataModel.addMatSource throw "fields[fn].push is not a function", and the whole
.mat failed to open. __proto__ is not a MATLAB name, but a file can spell it, and a plain `{}`
hands that write to the prototype setter: a struct array threw the same way, and a 1x1
struct's field silently disappeared. Nothing reached Object.prototype itself: `.push` on it
throws.

Presence is now an own-key question, and every write of a file-spelled key goes through
setOwn (parser/ownKeys.ts, new, not in the barrel). setOwn assigns as before, except that it
defines `__proto__` as the own data property every other key becomes. `fields` stays an
ordinary object with Object.prototype, as every bag parseMat hands out is, so a consumer's
`fields.hasOwnProperty(…)`, `String(fields)` and `toStrictEqual` work as they did. With that, a
__proto__ field is read, and three more places wrote it into a plain `{}` on its way out of
the reader:

  - MatlabVariableNode._buildVarObject rebuilt `fields` for the writer. A 1x1x2 struct, which
    a text dictionary holds as a MAT stream, lost the field once edited and copied there.
  - MatlabVariableNode._serializeStructValue built each element's bag. A 1x2 struct copied
    into a text dictionary after an edit lost the value while `_fields` still named it, so
    the reopened struct read the name off Object.prototype and showed `[object Object]`.
  - McosParser.buildStructValue did the same for a struct inside an MCOS object, and
    buildProperties for an object's property bag. Derived from object_props.mat, a v.Specs
    struct whose one field is __proto__ showed no field at origin/main, and
    `[object Object]` with only MatParser fixed. With f.FleetName renamed __proto__, f's
    property bag took the string's envelope as its prototype and inherited `_array_type`,
    `_elements` and the rest.

All three now write through setOwn. An object's __proto__ property still gets no row, because
ObjectNode reserves every `_`-named key.

23 new cases, in test/matStructArray.test.ts and test/mcosParser.test.ts. test/tools/matRename.ts
now also takes raw bytes, for a same-length patch of a struct's field-name stride. Against the
previous commit's source 14 fail. The 9 that pass are the 1x1 struct with each name but
__proto__, which a plain `{}` already held as an own key, and the ordinary-name control. Here
all 23 pass. With each fix put back alone: MatParser fails 14, _buildVarObject 1,
_serializeStructValue 1, buildStructValue 1 and buildProperties 1. A prototype-less `fields`
(Object.create(null)) fails 18, on the ordinary-object assertion. Full suite: 6749 passed,
26 skipped.
…ng one opens

A zip's member names, and a text-OPC .mdl's part paths, are the file's words, and every
reader gathered the members into a plain `{}` keyed by them: Inflate's own central-directory
walk, fflate's unzipSync (the fallback, and the only engine in a browser), and MdlParser's
part splitter. For `__proto__` the assignment is Object.prototype's setter, so the member's
bytes became the bag's prototype. parseSlx walks the bag with for…in, which then enumerated
every byte index of the member. A 20 KB .slx holding a 20,000,000-byte member of that name
(zeros, so it deflates to almost nothing) took 2.7 s and then failed to open with
"RangeError: Too many properties to enumerate", under either engine. A text-OPC .mdl with a
part at `/__proto__` of the same size failed the same way, after 2.6 s. A binary .sldd opened,
because its reader does not walk the bag, but the member was gone from it.

The member is now left out, by both engines and by the .mdl splitter. Keeping it is not an
option: no reader asks for a part of that name, and fflate's zipSync, which serializeBinarySldd
calls to write a dictionary back, throws on an own `__proto__` key. So the result is what every
caller already saw of the member (nothing), and the bag stays a plain bag. As before, no
warning says a member was left out: the zip readers have no warnings channel, and the .mdl
splitter, which has one, stays silent to match them.

SlxParser.extractConfigSets also asked the bag for each part that configSetInfo names, by
plain indexing. A set listed as part `/toString` (or constructor, hasOwnProperty, __proto__)
found an inherited member, so the set was reported as a part that "could not be read (The
"list" argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView.)".
The lookup is now of an own key, and the warning says what is true: the package does not
contain that part. The set is dropped either way, and the other set is still read.

9 new cases, in test/inflateSeam.test.ts (each engine), test/mdlParser.test.ts and
test/parseWarnings.test.ts. Against the previous commit's source all 9 fail; here all pass.
With each fix put back alone: the native walk fails 2, the fflate filter 2, the .mdl splitter
1, and the configSetInfo lookup 4. Full suite: 6758 passed, 26 skipped.
…ves as a field

A dictionary's struct field names are the file's words, in both formats, and three places used
them as keys of a plain object. Measured on rt_text.sldd and rt_bin.sldd, whose gadget.Specs
is a 1x1 struct of mass (1500) and color ('red'), with color renamed:

  - BinarySlddParser.parseStructElement gathered an element's <P Name="…"> children into `{}`.
    A field called __proto__ was the prototype setter's argument, and vanished on open.
  - StructNode.serializeElement rebuilt each element into `{}` on save. A text dictionary's
    __proto__ field opened as 'red', but after an edit of mass and a save its value was lost
    while `_fields` still named it. The reopened row read the name off Object.prototype and
    showed `[object Object]`.
  - StructNode.parse read each declared field off the element by plain indexing. A field that
    `_fields` names and the element does not hold (a hand-edited file) read an inherited
    member: the row showed `function Object() { [native code] }` for constructor and
    `function toString() { [native code] }` for toString, where any other such name reads as
    absent.

The reader and the writer now write each name with setOwn, which assigns as before and
defines `__proto__` as an own data property. The bags keep Object.prototype like every other
parsed bag, and a later <P> of the same name still overrides an earlier one in place. The read
is of an own key (fieldOf). A .mat struct with a __proto__ field now also survives a copy into
a binary dictionary, which dropped it until now.

Opening a binary dictionary costs what it did, because the reader still assigns into a plain
`{}`. Medians of five alternating processes, each the median of three opens after one warm-up,
against origin/main (15831c2): binsldd/dd140000.sldd 2286 ms there and 2281 ms here,
customer/Controller_cummins.sldd 966 and 972 ms, customer/PTSA_SysIf_bosch.sldd 526 and 524 ms.
An earlier form of this branch (8016698, on fcf4b46) built this bag and EncodedValue's from
[name, value] pairs with Object.fromEntries. Measured the same way, in the same runs, it took
2393, 1003 and 547 ms, 3.8 to 4.7 percent more than origin/main.

5 new cases, in test/structNode.test.ts and test/matStructArray.test.ts, each over nine
inherited names and an ordinary control. Two open, edit, save and reopen a struct field in
each format; one reads a declared field the element does not hold, in a text dictionary; and
the .mat copy test gains a binary-dictionary venue, through a helper now shared with its text
one. Against the previous commit's source all 5 fail; here all pass. With each fix put
back alone: the binary reader fails 3, serializeElement 2 (its text case, and the binary one,
whose save goes through it too), and the own-key read 1. Full suite: 6763 passed, 26 skipped.
… class refusal says why

Two more object-literal tables were asked about a file's word by plain indexing.

buildProjectPage labels each working folder with LOCATION_LABEL[key] ?? key. The key is a
Reference's Location in the project store. Read through parseProject from a monolithic store,
a location keyed toString, constructor, valueOf or hasOwnProperty got an inherited function
as its label, and `__proto__` got Object.prototype, where a key the table does not list
(NewInSomeFutureRelease) shows itself. The page is what a host renders for a .prj, so a label
there should be text.

encodeMatVariable takes the MAT class code from CLASS_CODE[className]. The class can come
from a file: a text dictionary's `_type`, or the Class of a binary dictionary's dense empty
(textDictionaryForm). For those names the inherited member got past the refusal of a class
with no MAT code, and the encode failed later as "no payload width for MAT element type
undefined" instead of "no MAT class for "toString"". Both are MatWriteErrors, and every
caller treats one like another, so nothing a user sees changed. Only the reason did.

Both lookups are now of an own key.

12 new cases, in test/projectPage.test.ts and test/matWriter.test.ts, five inherited names
and an ordinary control each. Against the previous commit's source the 10 inherited cases fail
and the 2 controls pass. Here all pass. With either lookup put back alone, its own 5 fail.
Full suite: 6775 passed, 26 skipped.
A dictionary's property bags, elements and value envelopes are keyed by the file's words, and
the readers now hold a `__proto__` key as an own key (JSON.parse, and BinarySlddParser since
the struct-field commit). The save paths then copied each bag with
`Object.assign({}, bag, …)`, which writes every key with [[Set]], so `__proto__` was handed to
the prototype setter. A primitive value was dropped. An object value became the copy's
prototype, and the copy then inherited whatever keys it held. MATLAB cannot write the key
(an identifier starts with a letter), but a file can carry it, and what was read should go
back out. Measured through a text dictionary saved with no edits:

  - A Simulink.Parameter and a Simulink.Signal each lost a `__proto__` property from the
    saved entry.
  - A CustomObject whose `__proto__` property held a `_saveobj` key read as a custom-saving
    class, because DataNode._mergeProps asks `SAVEOBJ_KEY in stored` of the copy. The saved
    entry gained a `_custom_save` envelope the file never had, carrying the object's
    Description.

Every such copy is now an object spread, which defines each key as an own data property, in
the same order and with the same later-wins overrides. That covers DataNode
(_serializeSimulinkObject, _mergeProps, _getSerializedProperties, writeIntoSaveobj),
SimulinkObjectNode._propsOf, ParameterNode, SignalNode, BaseBusNode and its element,
EnumTypeNode and its enumeral, and ObjectNode. EnumTypeNode's Enumerals wrapper was also
rebuilt key by key and is now one spread. Two more bags were filled key by key from the file
and now write through setOwn (parser/ownKeys.ts):

  - EncodedValue keeps an encoded element's attributes to write them back. An attribute
    called `__proto__` was dropped.
  - DataNode.textDictionaryMetadata respells a binary reader's metadata keys for a text
    dictionary and carries the rest across. It asked `key in out` of a plain `{}`, so a
    metadata key called toString or constructor counted as already written, and
    `__proto__` went to the setter. All three were dropped. Presence is now an own-key
    question.

The copies whose keys are the node's own (`_gatedProps`, the `_serializedOverrides` literals,
`Object.assign(stored, propOverrides)`) are unchanged. So is ModelNode's copy of a model's zip
members, which can no longer hold a member of that name.

9 new cases, in test/absentPropertyWriteBack.test.ts and test/encodedValueWriteBack.test.ts.
The property-bag cases are differential. The same value goes in under `__proto__` and under
`_zzproto_`, another `_`-named key of the same length: an object holding `_saveobj` in every
object of every entry's value (text), a double in every object element (binary). That is done
over the parity corpus's cases.sldd and the bus_types and custom_object fixtures, and once
more beside a `_custom_save` envelope. Each file is saved untouched and again after every
entry is renamed, and the two saves must be the same file but for the name. A user-class
object rebuilds its bag from its rows and has none for a `_`-named key, so there both keys go
the same way, as they do for every `_`-named key (ObjectNode keeps none of them on a save;
that is outside this change). Against the previous commit's source all 9 fail; here all 9
pass. With each file put back alone: DataNode fails 6, BaseBusNode 4, ParameterNode 3,
EnumTypeNode 2, ObjectNode 1, SignalNode 1, SimulinkObjectNode 1 and EncodedValue 1. Full
suite: 6784 passed, 26 skipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant