aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-28 10:44:16 +0100
committerDanilo M. <danix@danix.xyz>2026-10-28 10:44:16 +0100
commit0838881b87f3a59ccd841d247e4b975347529220 (patch)
treede0684bcf35e91c75ed5c615f88bc639fe4e2e44
parentd368de4b30d8a1dc9bb72ba45c55b07d3f034c82 (diff)
downloadgenerat3D-0838881b87f3a59ccd841d247e4b975347529220.tar.gz
generat3D-0838881b87f3a59ccd841d247e4b975347529220.zip
Turn remaining malformed-input crashes into Error
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-rw-r--r--generat3d/config.py5
-rw-r--r--generat3d/library.py3
-rw-r--r--generat3d/scene.py22
-rw-r--r--tests/test_config.py4
-rw-r--r--tests/test_library.py16
-rw-r--r--tests/test_scene.py32
6 files changed, 76 insertions, 6 deletions
diff --git a/generat3d/config.py b/generat3d/config.py
index 804723f..fbb9f4d 100644
--- a/generat3d/config.py
+++ b/generat3d/config.py
@@ -53,7 +53,10 @@ def library_roots(cli_roots, path=None):
for r in roots:
if not r.strip():
raise Error("empty library root")
- p = Path(r).expanduser()
+ try:
+ p = Path(r).expanduser()
+ except RuntimeError:
+ raise Error(f"cannot expand library root: {r}") from None
if not p.is_dir():
raise Error(f"library root not found: {p}")
out.append(p)
diff --git a/generat3d/library.py b/generat3d/library.py
index ea12794..39ad6fb 100644
--- a/generat3d/library.py
+++ b/generat3d/library.py
@@ -18,6 +18,7 @@
import gzip
import json
import os
+import zlib
from pathlib import Path
from urllib.parse import unquote
@@ -41,7 +42,7 @@ def read_dson(path):
if data[:2] == b"\x1f\x8b":
data = gzip.decompress(data)
return json.loads(data)
- except (OSError, EOFError, ValueError) as e:
+ except (OSError, EOFError, ValueError, zlib.error, RecursionError) as e:
raise Error(f"{path}: {e}") from None
diff --git a/generat3d/scene.py b/generat3d/scene.py
index 25e4b61..28dc00c 100644
--- a/generat3d/scene.py
+++ b/generat3d/scene.py
@@ -62,9 +62,17 @@ class Scene:
graph: Graph
+def _finite(v):
+ return (not isinstance(v, bool) and isinstance(v, (int, float))
+ and math.isfinite(v))
+
+
def _channel(node, name):
comps = {c["id"]: c.get("current_value", c.get("value", 0.0)) for c in node.get(name, [])}
- return [comps.get(a, 0.0) for a in AXES]
+ out = [comps.get(a, 0.0) for a in AXES]
+ if not all(_finite(v) for v in out):
+ raise Error(f"{node['id']}: {name} is not a finite number")
+ return out
def _lookup(library, doc, url, section):
@@ -147,9 +155,12 @@ class _Loader:
self.graph.add_prop(key(n["id"], n["id"], f"{ch}/{a}"), v)
for f in n.get("formulas", []):
self.pending.append((f, fpath))
+ verts = np.array(geom["vertices"]["values"], dtype=float)
+ if verts.ndim != 2 or verts.shape[1] != 3 or not np.isfinite(verts).all():
+ raise Error(f"{fpath}: bad vertices")
return Figure(asset["id"], inst.get("name", asset.get("name", asset["id"])),
inst.get("label", asset.get("label", asset["id"])),
- np.array(geom["vertices"]["values"], dtype=float),
+ verts,
list(geom.get("polygon_material_groups", {}).get("values", [])),
nodes)
@@ -179,6 +190,9 @@ class _Loader:
if parent and parent != self.figure.id:
continue
ch = mod.get("channel", {})
+ for name in ("value", "min", "max"):
+ if name in ch and not _finite(ch[name]):
+ raise Error(f"{path}#{mod['id']}: {name} is not a finite number")
k = key(self.figure.id, mod["id"], "value")
self.graph.add_prop(k, ch.get("value", 0.0), ch.get("min", -math.inf),
ch.get("max", math.inf), ch.get("clamped", False))
@@ -206,7 +220,7 @@ class _Loader:
doc = self.library.load(path)
try:
self._modifiers(path, doc)
- except (KeyError, TypeError, ValueError, IndexError, AttributeError) as e:
+ except (KeyError, TypeError, ValueError, IndexError, AttributeError, OverflowError) as e:
raise Error(f"{path}: malformed morph data: {e!r}") from None
def _morphs(self, fpath):
@@ -263,5 +277,5 @@ def load(path, library):
"""Load a .duf scene and everything its figure needs."""
try:
return _Loader(path, library).run()
- except (KeyError, TypeError, IndexError, ValueError, AttributeError) as e:
+ except (KeyError, TypeError, IndexError, ValueError, AttributeError, OverflowError) as e:
raise Error(f"{path}: malformed scene data: {e!r}") from None
diff --git a/tests/test_config.py b/tests/test_config.py
index 72ed0f6..d88b534 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -88,3 +88,7 @@ class ConfigTest(unittest.TestCase):
def test_empty_library_root_is_fatal(self):
with self.assertRaisesRegex(Error, "empty library root"):
library_roots([""], self.cfg)
+
+ def test_unexpandable_home_is_fatal(self):
+ with self.assertRaisesRegex(Error, "cannot expand library root"):
+ library_roots(["~nosuchuser_generat3d/lib"], self.cfg)
diff --git a/tests/test_library.py b/tests/test_library.py
index 84fa3cc..7e06ca4 100644
--- a/tests/test_library.py
+++ b/tests/test_library.py
@@ -89,3 +89,19 @@ class LibraryTest(unittest.TestCase):
p = Path(self.tmp.name) / "x.duf"
p.write_text(json.dumps({"x": 1}))
self.assertEqual(read_dson(p), {"x": 1})
+
+
+class ReadDsonCrashTest(unittest.TestCase):
+ def read(self, data):
+ with tempfile.TemporaryDirectory() as d:
+ p = Path(d) / "x.dsf"
+ p.write_bytes(data)
+ read_dson(p)
+
+ def test_corrupt_gzip_is_error(self):
+ with self.assertRaises(Error):
+ self.read(b"\x1f\x8b\x08\x00" + b"\xff" * 20)
+
+ def test_deep_nesting_is_error(self):
+ with self.assertRaises(Error):
+ self.read(b"[" * 200000)
diff --git a/tests/test_scene.py b/tests/test_scene.py
index 76a28b6..056f3e1 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -233,3 +233,35 @@ class SceneTest(unittest.TestCase):
"output": "hip:/data/Test/Fig2/Fig2.dsf#hip?center_point/y",
"operations": [{"op": "push", "val": 1}]}]})
self.assertEqual(len(scene.graph.formulas["hip:hip?center_point/y"]), 1)
+
+ def raw_fig2(self, text):
+ """Load the temp figure from raw JSON text (for literals json.dumps rejects)."""
+ root = self.dir / "raw"
+ p = root / "data/Test/Fig2/Fig2.dsf"
+ p.parent.mkdir(parents=True)
+ p.write_text(text)
+ nodes = [{"id": "Fig2", "url": "/data/Test/Fig2/Fig2.dsf#Fig2", "name": "Fig2"}]
+ return load_quiet(self.duf({"nodes": nodes}), [root])
+
+ def test_bone_value_not_a_number(self):
+ xyz = [{"id": "x", "value": "a"}]
+ with self.assertRaisesRegex(Error, "hip: center_point is not a finite number"):
+ self.fig2(hip_extra={"center_point": xyz})
+
+ def test_bone_value_not_finite(self):
+ text = json.dumps(self.fig_doc()).replace(
+ '"orientation": [{"id": "x", "value": 0}', '"orientation": [{"id": "x", "value": 1e400}', 1)
+ self.assertIn("1e400", text)
+ with self.assertRaisesRegex(Error, "is not a finite number"):
+ self.raw_fig2(text)
+
+ def test_modifier_value_nan(self):
+ root = self.root(self.MORPHS + "Nan.dsf", self.morph_doc(self.mod("Nan")))
+ p = root / (self.MORPHS + "Nan.dsf")
+ p.write_text(p.read_text().replace('"value": 0', '"value": NaN'))
+ with self.assertRaisesRegex(Error, "Nan: value is not a finite number"):
+ load_quiet(DATA / "scene.duf", [root])
+
+ def test_bad_vertices(self):
+ with self.assertRaisesRegex(Error, "bad vertices"):
+ self.fig2(verts=[[0, 0], [1, 0], [0, 1]])