fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(parser): defensive Bitmap copy and Struct prototype-chain guard - #44

Merged
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness
May 20, 2026
Merged

fix(parser): defensive Bitmap copy and Struct prototype-chain guard#44
RobinBol merged 3 commits into
masterfrom
fix/parser-robustness

Conversation

@RobinBol

Copy link
Copy Markdown
Contributor

Summary

Two small robustness fixes to the parser primitives. Both have zero behavior impact on correct callers; both close real footguns. Each lands with a regression-locking test.

1. Bitmap.fromBuffer now owns its bytes

Bitmap.fromBuffer previously stored buf.slice(i, i + len) as the bitmap's backing store. In Node, `Buffer.slice` returns a view sharing memory with the source. If anything mutated the source after parsing (e.g. a radio driver reusing its receive buffer across frames), the bits read from an already-parsed Bitmap would silently change - a TOCTOU-style hazard that depends on caller buffer-management behavior.

Wrapping the slice in `Buffer.from(...)` makes the Bitmap own its bytes. One-character diff.

2. `Struct` rejects prototype-chain keys

`Struct`'s constructor used `if (!defs[key]) throw` to validate incoming field names. That check walks the prototype chain, so `defs.constructor` resolves to `Object` (truthy), and `constructor` is silently accepted as a valid field name - quietly shadowing the instance's `.constructor` reference. `toString`, `valueOf`, etc. were affected the same way.

Switching to `Object.prototype.hasOwnProperty.call(defs, key)` restricts acceptance to declared own-property fields. `proto` was already rejected (its prototype-chain value is treated as the unexpected key); this aligns the rest of the prototype-chain keys with the same behavior.

Test plan

  • New test in `test/DataTypes.test.js`: parse a map8 from a source buffer, mutate the source, read the bitmap again - bits must be unchanged.
  • New tests in `test/Struct.test.js`: `constructor`, `toString` rejected; declared and undeclared field names handled as before.
  • Full suite: 14/14 passing (9 pre-existing + 5 new). No regressions.
  • Lint and typecheck clean.
  • Verified against downstream consumer `node-zigbee-clusters` (81/81 tests pass against the patched build).

Out of scope

The silent zero-fallback behavior in `uintFromBuf` / `enumFromBuf` / `dataFromBuf` / `bufferFromBuf` on truncated input is a separate, larger discussion (it's a breaking change to make those throw). The downstream consumer `node-zigbee-clusters` has separately added a strict length precheck around command-arg parsing (athombv/node-zigbee-clusters#193), which addresses the most consequential consumer impact without requiring changes here.

Two small robustness fixes:
1. `Bitmap.fromBuffer` previously stored the result of `buf.slice(...)`,
which in Node returns a Buffer that shares memory with the source. If
the source buffer was mutated after parsing (e.g. a radio driver reusing
its receive buffer across frames), the bits read from an already-parsed
Bitmap would change. Wrap the slice in `Buffer.from(...)` so the Bitmap
owns its bytes.
2. `Struct`'s constructor used `!defs[key]` to validate field names. That
check walks the prototype chain, so `defs.constructor` resolves to
`Object` (truthy) and `constructor` is accepted as a valid field name,
silently overwriting the instance's `.constructor` reference. Use
`Object.prototype.hasOwnProperty.call(defs, key)` instead so only
declared own-property fields are accepted. Other prototype-chain keys
(`toString`, `valueOf`, ...) are also now correctly rejected.
Tests added for both. Neither change affects behavior for any
correct caller.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens two core parser primitives to avoid subtle footguns: (1) preventing Bitmap instances from aliasing caller buffers, and (2) tightening Struct construction to reject unexpected keys that come from the prototype chain.

Changes:

  • Make Bitmap.fromBuffer copy its backing bytes so later mutations of the source buffer can’t change already-parsed bitmaps.
  • Validate Struct constructor input keys using hasOwnProperty (instead of truthiness lookup) to reject prototype-chain keys like constructor/toString.
  • Add regression tests covering buffer aliasing and prototype-chain key rejection.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
lib/DataTypes.jsCopies bitmap bytes in Bitmap.fromBuffer to prevent source-buffer aliasing.
lib/Struct.jsSwitches unexpected-property validation to hasOwnProperty to block prototype-chain keys.
test/DataTypes.test.jsAdds regression test ensuring parsed bitmaps don’t change when the source buffer is mutated.
test/Struct.test.jsAdds tests ensuring prototype-chain keys are rejected while normal declared/undeclared behavior remains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlib/Struct.js Outdated
Address Copilot review feedback on #44: the throw path previously used
`this.constructor.name` for the error prefix. If a struct legitimately
declares a `constructor` field and the caller sets it before an
unexpected key is encountered, `this.constructor` is already the
user-supplied value and `.name` would crash or be undefined.
Switch to the closed-over `name` parameter, which is lexically bound
and cannot be shadowed by instance assignments. Adds a regression test
for the edge case.
…sages
Followup to the previous commit. Closed-over `name` was correct on the
shadow-resistance axis but lost the subclass-aware error message that
`this.constructor.name` originally provided: when a Struct-generated
class is subclassed, the error should identify the actual instantiated
subclass, not the underlying Struct name.
`new.target` is a syntactic binding to the constructor actually invoked
via `new`. It reflects the subclass identity AND cannot be shadowed by
a `constructor` field the caller may have set earlier in the same loop.
Strictly better than both `this.constructor.name` and closed-over `name`.
Adds a subclassing regression test so this property is locked in.
@RobinBol
RobinBol merged commit 7e6177e into masterMay 20, 2026
4 checks passed
@RobinBol
RobinBol deleted the fix/parser-robustness branch May 20, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@RobinBol