# generat3D - evaluate Daz DSON scenes and write .dbz files # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import gzip import json import os from pathlib import Path from urllib.parse import unquote from . import Error def parse_url(url): """Split a DSON URL "node:/path/file.dsf#id?prop" into (node, path, id, prop).""" node = "" if not url.startswith(("/", "#")) and ":" in url: node, url = url.split(":", 1) url, _, prop = url.partition("?") path, _, ident = url.partition("#") return unquote(node), unquote(path), unquote(ident), prop def read_dson(path): """Parse a plain or gzipped DSON file.""" try: data = Path(path).read_bytes() if data[:2] == b"\x1f\x8b": data = gzip.decompress(data) return json.loads(data) except (OSError, EOFError, ValueError) as e: raise Error(f"{path}: {e}") from None def _find_nocase(base, parts, want_dir=False): for part in parts: try: names = {n.lower(): n for n in os.listdir(base)} except OSError: return None name = names.get(part.lower()) if name is None: return None base = base / name return base if (base.is_dir() if want_dir else base.is_file()) else None class Library: def __init__(self, roots): self.roots = [Path(r) for r in roots] self._docs = {} def _locate(self, rel, want_dir): for root in self.roots: p = root / rel if p.is_dir() if want_dir else p.is_file(): yield root, p continue p = _find_nocase(root, rel.split("/"), want_dir) if p: yield root, p def find(self, path): """File for a library path like "/data/x.dsf", first root wins, or None.""" return next((p for _, p in self._locate(path.strip("/"), False)), None) def load(self, path): """Parsed DSON document for a library path. Error if missing or broken.""" key = path.lower() if key not in self._docs: f = self.find(path) if f is None: raise Error(f"not found in library roots: {path}") self._docs[key] = read_dson(f) return self._docs[key] def walk(self, path): """Library paths of every .dsf under a library directory, across all roots.""" seen = {} for root, base in self._locate(path.strip("/"), True): for f in sorted(base.rglob("*")): if f.is_file() and f.suffix.lower() == ".dsf": lp = "/" + f.relative_to(root).as_posix() seen.setdefault(lp.lower(), lp) return list(seen.values())