diff options
| -rw-r--r-- | generat3d/config.py | 5 | ||||
| -rw-r--r-- | generat3d/library.py | 3 | ||||
| -rw-r--r-- | generat3d/scene.py | 22 | ||||
| -rw-r--r-- | tests/test_config.py | 4 | ||||
| -rw-r--r-- | tests/test_library.py | 16 | ||||
| -rw-r--r-- | tests/test_scene.py | 32 |
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]]) |
