mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
fix(composer): coerce remaining raw int() payload values, harden JS file/download handling
CodeRabbit flagged several payload-derived int() conversions in composer.py that could raise ValueError instead of clamping like every other coerced value in the module (_as_rgb_filter and its callers -- fillR/G/B, outR/G/B, bgR/G/B, trackR/G/B -- plus min_width, lineSpacing, barWidth/Height, start/endAngle, borderRadius, pip*, sparkline bar*, marquee gap/scrollSpeed). Route them all through _safe_int for consistency with the rest of the module and to avoid the generic 422 a raw ValueError produces. Also: - FileReader.onerror was unset in importDesign(), so a failed file read produced no status message. - URL.revokeObjectURL() ran synchronously right after link.click() in both exportDesign() and generateZip(); deferred via setTimeout(..., 0) so the download reliably starts before the object URL is revoked. - _module_level_code() (test helper) filtered ast.ImportFrom but not ast.Import, so a bare `import os` payload wouldn't be caught by the helper itself, even though downstream assertions still caught it. Added regression tests for the fillR/G/B injection + clamping path, which had no coverage (existing tests only covered the r/g/b _rgb_expr path). Skipped as not worth the churn (CodeRabbit nitpicks, both "Trivial/Low value"): - test_composer_empty_block.py's branch regex not matching digit-containing type names -- no such type exists today. - test_composer_path_containment.py's `C.composer_bp.name and Flask(...)` truthiness guard -- cosmetic, blueprint name is never falsy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XKc832xpVEx3C3W5BVqQ5Z
This commit is contained in:
@@ -67,7 +67,7 @@ def _module_level_code(src):
|
||||
tree = ast.parse(src)
|
||||
out = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.ImportFrom)):
|
||||
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.ImportFrom, ast.Import)):
|
||||
continue
|
||||
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
|
||||
continue # the docstring
|
||||
@@ -145,6 +145,28 @@ def test_colour_channels_are_clamped_to_a_byte():
|
||||
assert "(255, 0, 128)" in src, "channels were not clamped to 0-255"
|
||||
|
||||
|
||||
#: fillR/fillG/fillB (and outR/G/B, bgR/G/B, trackR/G/B) go through
|
||||
#: _as_fill_filter -> _as_rgb_filter, a separate path from _rgb_expr above.
|
||||
#: It used raw int() until it was found to raise ValueError on a non-numeric
|
||||
#: channel instead of clamping like every other coerced value in this module.
|
||||
@pytest.mark.parametrize("evil", EXPR_PAYLOADS)
|
||||
@pytest.mark.parametrize("channel", ["fillR", "fillG", "fillB"])
|
||||
def test_a_non_numeric_fill_channel_cannot_reach_the_source(evil, channel):
|
||||
el = {"type": "rectangle", "id": "r1", "x": 0, "y": 0, "width": 10, "height": 8,
|
||||
"fillR": 0, "fillG": 0, "fillB": 128}
|
||||
el[channel] = evil
|
||||
src = _generated(_payload(elements=[el]))
|
||||
assert "__import__" not in src and "os.system" not in src
|
||||
assert not _module_level_code(src)
|
||||
|
||||
|
||||
def test_fill_channels_are_clamped_to_a_byte():
|
||||
el = {"type": "rectangle", "id": "r1", "x": 0, "y": 0, "width": 10, "height": 8,
|
||||
"fillR": 99999, "fillG": -5, "fillB": 128}
|
||||
src = _generated(_payload(elements=[el]))
|
||||
assert "(255, 0, 128)" in src, "fill channels were not clamped to 0-255"
|
||||
|
||||
|
||||
def test_the_generated_module_still_has_no_top_level_statements():
|
||||
"""The clean case: a normal payload produces only imports and a class."""
|
||||
el = {"type": "text", "id": "t1", "x": 4, "y": 4, "text": "hi",
|
||||
|
||||
Reference in New Issue
Block a user