2026-10-27 16:58:26 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Metadata diagnostics and dependency inspection without a server or modules."""
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
2026-11-06 18:13:49 +01:00
|
|
|
import platform
|
2026-10-27 16:58:26 +01:00
|
|
|
|
|
|
|
|
binary = str(Path(sys.argv[1]).resolve())
|
|
|
|
|
|
|
|
|
|
def check(option, path, code, *messages):
|
|
|
|
|
result = subprocess.run([binary, option, str(path)], capture_output=True, text=True)
|
|
|
|
|
assert result.returncode == code, (result.returncode, result.stdout, result.stderr)
|
|
|
|
|
for message in messages:
|
|
|
|
|
assert message in result.stdout + result.stderr, (message, result.stdout, result.stderr)
|
|
|
|
|
|
|
|
|
|
base = Path("tests/fixtures/project-v10.xtc").read_text()
|
|
|
|
|
check("--inspect", "tests/fixtures/project-v10.xtc", 0, "Project format 10", "missing/unreadable: 0")
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
|
|
|
|
|
root = Path(directory)
|
|
|
|
|
project = root / "project.xtc"
|
|
|
|
|
media = root / "media.wav"
|
|
|
|
|
# Relative paths resolve against the project, independently of the subprocess cwd.
|
|
|
|
|
audio = base.replace("ASSET -", "ASSET " + b"media.wav".hex()).replace("MIDI 1", "MIDI 0")
|
|
|
|
|
audio = audio.replace("NOTES 1 1\nNOTE 0 0 0 480 69 100 0", "NOTES 0 0")
|
|
|
|
|
project.write_text(audio)
|
|
|
|
|
check("--validate", project, 0, "metadata valid")
|
|
|
|
|
check("--inspect", project, 2, "Media clip 0", str(media), "missing/unreadable: 1")
|
|
|
|
|
media.write_text("Deliberately not WAV data")
|
|
|
|
|
check("--inspect", project, 0, "available", "WAV contents", "not checked")
|
|
|
|
|
module = root / "module.so"
|
|
|
|
|
plugin = Path("tests/fixtures/project-plugin-v6.xtc").read_text()
|
|
|
|
|
plugin = plugin.replace(b"/missing/test.so".hex(), str(module).encode().hex())
|
|
|
|
|
project.write_text(plugin)
|
|
|
|
|
check("--validate", project, 0)
|
|
|
|
|
check("--inspect", project, 2, "LADSPA node 0 ID 900000 descriptor 0", "xtc_test_gain", str(module))
|
|
|
|
|
# Inspection succeeds for readable files without trying dlopen or decoding.
|
|
|
|
|
module.write_text("Deliberately not an executable module")
|
|
|
|
|
check("--inspect", project, 0, "no plugin code executed")
|
2026-10-27 17:34:46 +01:00
|
|
|
# Current metadata-only inspection must not execute any modern plugin ABI.
|
|
|
|
|
maps = base[base.index("CONTROL_MAPS"):]
|
|
|
|
|
for number, name, identity in ((1, "CLAP", "org.xtc.fixture"), (2, "VST2", ""),
|
|
|
|
|
(3, "VST3", "0123456789abcdef0123456789abcdef")):
|
|
|
|
|
lines = []
|
|
|
|
|
for line in plugin.splitlines():
|
|
|
|
|
if line.startswith(("CONTROL ", "PARAM ")):
|
|
|
|
|
continue
|
|
|
|
|
if line.startswith("XTC_PROJECT "):
|
|
|
|
|
line = "XTC_PROJECT 11"
|
|
|
|
|
elif line.startswith("PLUGIN "):
|
|
|
|
|
fields = line.split()
|
|
|
|
|
fields[4] = "0"
|
|
|
|
|
line = " ".join(fields)
|
|
|
|
|
elif line.startswith("PLUGIN_AUTOMATION "):
|
|
|
|
|
line = "PLUGIN_AUTOMATION 0"
|
|
|
|
|
elif line == "END":
|
|
|
|
|
line = maps.rstrip()
|
|
|
|
|
lines.append(line)
|
|
|
|
|
if line.startswith("MODULE "):
|
|
|
|
|
lines.append(f"FORMAT {number} {identity.encode().hex() if identity else '-'}")
|
|
|
|
|
modern = "\n".join(lines) + "\n"
|
|
|
|
|
project.write_text(modern)
|
|
|
|
|
check("--inspect", project, 0, name + " node 0", "Project format 11")
|
|
|
|
|
project.write_text(modern.replace(f"FORMAT {number} ", "FORMAT 99 "))
|
|
|
|
|
check("--validate", project, 1, "Unsupported")
|
|
|
|
|
if number == 3:
|
|
|
|
|
project.write_text(modern.replace(identity.encode().hex(), b"bad-class-id".hex()))
|
|
|
|
|
check("--validate", project, 1)
|
2026-10-27 17:51:48 +01:00
|
|
|
# V12 track bindings retain references without executing module contents.
|
|
|
|
|
for number, identity in ((1, "org.xtc.instrument"), (2, ""),
|
|
|
|
|
(3, "0123456789abcdef0123456789abcdef")):
|
|
|
|
|
binding = (f"INSTRUMENT 0 {number} 0 42 {b'synth'.hex()}\n"
|
|
|
|
|
f"MODULE {str(module).encode().hex()}\n"
|
|
|
|
|
f"IDENTITY {identity.encode().hex() if identity else '-'}\n")
|
|
|
|
|
document = base.replace("XTC_PROJECT 10", "XTC_PROJECT 12").replace(
|
|
|
|
|
"END\n", "TRACK_INSTRUMENTS 1\n" + binding + "LIVE_INSTRUMENT 0\nEND\n")
|
|
|
|
|
project.write_text(document)
|
|
|
|
|
check("--inspect", project, 0, "instrument track 0", "no plugin code executed")
|
|
|
|
|
for malformed in (
|
|
|
|
|
document.replace("TRACK_INSTRUMENTS 1", "TRACK_INSTRUMENTS 2").replace(binding,binding+binding),
|
|
|
|
|
document.replace("INSTRUMENT 0 ", "INSTRUMENT 99 "),
|
|
|
|
|
document.replace("LIVE_INSTRUMENT 0", "LIVE_INSTRUMENT 99"),
|
|
|
|
|
document.replace(f"INSTRUMENT 0 {number}", "INSTRUMENT 0 0"),
|
|
|
|
|
):
|
|
|
|
|
project.write_text(malformed)
|
|
|
|
|
check("--validate", project, 1)
|
2026-11-06 01:50:25 +01:00
|
|
|
# V17 track-chain dependency inspection must not dlopen the readable fake module.
|
|
|
|
|
for number, name, identity in ((1, "CLAP", "org.xtc.fixture"), (2, "VST2", ""),
|
|
|
|
|
(3, "VST3", "0123456789abcdef0123456789abcdef")):
|
|
|
|
|
chain = ("CHAINS 1\nCHAIN 0 1 3f800000\n"
|
|
|
|
|
f"INSERT 1 3f800000 {number} 0 42 1 120000 {b'effect'.hex()}\n"
|
|
|
|
|
f"MODULE {str(module).encode().hex()}\nIDENTITY {identity.encode().hex() if identity else '-'}\n"
|
|
|
|
|
"CONTROLS 0\nPARAMETERS 0\n")
|
|
|
|
|
document = base.replace("XTC_PROJECT 10", "XTC_PROJECT 17").replace(
|
|
|
|
|
"END\n", "TRACK_INSTRUMENTS 0\nLIVE_INSTRUMENT 18446744073709551615\nMARKERS 0\n" + chain + "END\n")
|
|
|
|
|
project.write_text(document)
|
|
|
|
|
check("--inspect", project, 0, name + " insert track 0 slot 1", "no plugin code executed", "missing/unreadable: 0")
|
|
|
|
|
for malformed in (document.replace("CHAIN 0 1", "CHAIN 0 9"),
|
|
|
|
|
document.replace("INSERT 1 3f800000", "INSERT 1 7f800000"),
|
|
|
|
|
document.replace("CHAIN 0 1", "CHAIN 99 1")):
|
|
|
|
|
project.write_text(malformed)
|
|
|
|
|
check("--validate", project, 1)
|
|
|
|
|
project.write_text(document.replace(str(module).encode().hex(), b"/missing/effect.so".hex()))
|
|
|
|
|
check("--inspect", project, 2, name + " insert track 0", "missing/unreadable: 1")
|
|
|
|
|
project.write_text(document.replace("XTC_PROJECT 17", "XTC_PROJECT 16").replace(chain, ""))
|
|
|
|
|
check("--validate", project, 0, "metadata valid")
|
2026-11-06 18:13:49 +01:00
|
|
|
# A conventional VST3 bundle is inspected without executing its fake binary.
|
|
|
|
|
architecture = {"x86_64": "x86_64", "aarch64": "aarch64",
|
|
|
|
|
"i386": "i386", "i686": "i386", "armv7l": "arm"}.get(platform.machine())
|
|
|
|
|
if architecture:
|
|
|
|
|
bundle = root / "Example.vst3"
|
|
|
|
|
inner = bundle / "Contents" / (architecture + "-linux") / "Example.so"
|
|
|
|
|
inner.parent.mkdir(parents=True)
|
|
|
|
|
bundled = document.replace(str(module).encode().hex(), str(bundle).encode().hex())
|
|
|
|
|
project.write_text(bundled)
|
|
|
|
|
check("--inspect", project, 2, "VST3 insert track 0", "missing/unreadable: 1")
|
|
|
|
|
inner.write_text("Not a module: metadata inspection must not execute it")
|
|
|
|
|
check("--inspect", project, 0, "VST3 insert track 0", "no plugin code executed", "missing/unreadable: 0")
|
2026-10-27 16:58:26 +01:00
|
|
|
for text, messages in [
|
2026-11-06 01:34:08 +01:00
|
|
|
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..17")),
|
2026-10-27 16:58:26 +01:00
|
|
|
("XTC_PROJECT 10\n", ("line 2", "APP")),
|
|
|
|
|
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
|
|
|
|
|
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),
|
|
|
|
|
(base.replace("APP 0 1 0", "APP 0 1 0 extra"), ("APP",)),
|
|
|
|
|
]:
|
|
|
|
|
project.write_text(text)
|
|
|
|
|
check("--inspect", project, 1, *messages)
|
|
|
|
|
with project.open("wb") as stream:
|
|
|
|
|
stream.truncate(4194305)
|
|
|
|
|
check("--inspect", project, 1, "SIZE", "4 MiB")
|
|
|
|
|
check("--inspect", root / "absent.xtc", 1, "OPEN")
|
|
|
|
|
print("Project inspection availability, diagnostics and strict compatibility passed.")
|