diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..230ad95 --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +NDI-compress-python is licensed under [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1). + +Contact info@walthamdatascience.com for commercial licensing. diff --git a/README.md b/README.md index bc30af0..313f44e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ This package requires the NDI compression C executables to be present. By default, it looks for them in `../../C/bin` relative to the `ndi_compress` package file. You can override this by setting the `NDI_BIN_PATH` environment variable. +Each codec executable runs in a subprocess with a timeout (default 300 seconds) +so a hung or looping codec process cannot block indefinitely. Override it with +the `NDI_COMPRESS_TIMEOUT` environment variable (in seconds); an invalid value +falls back to the default with a warning. + ```python import ndi_compress import numpy as np diff --git a/docs/Audit_Remediation_Results_2026-06-12.md b/docs/Audit_Remediation_Results_2026-06-12.md new file mode 100644 index 0000000..d246bff --- /dev/null +++ b/docs/Audit_Remediation_Results_2026-06-12.md @@ -0,0 +1,29 @@ +# NDI-compress-python Audit Remediation — Results (2026-06-12) + +> **Context for a reviewer / next agent.** One of **9 coordinated PRs** in the 2026-06 NDI +> ecosystem audit; **none are merged.** This repo's PR: **Waltham-Data-Science/NDI-compress-python#2**. +> Done here: subprocess timeout + LICENSE. **Deferred:** codec provenance/checksums + a +> cross-language round-trip test (needs the codec source + a paired MATLAB run) — see below. + +Branch `audit/ndi-compress-python-2026-06`, off `origin/main`. + +## Findings addressed (audit §6.2-7, §6.2-8) + +| # | Status | Summary | +|---|--------|---------| +| 6.2-7 (subprocess timeout) | **Done** | `_call_c_exec` ran the C codec via `subprocess.run(...)` with no timeout, so a hung/looping codec process would block indefinitely. Added a generous default timeout (`_C_EXEC_TIMEOUT`, 300 s, overridable via the `NDI_COMPRESS_TIMEOUT` env var) and convert `TimeoutExpired` into a clear `RuntimeError`. | +| 6.2-8 (LICENSE) | **Done** | Added `LICENSE` (CC BY-NC-SA 4.0) matching the NDI-compress-matlabp counterpart. | + +## FLAGGED — codec provenance + cross-language round-trip (not done here) + +The audit's core §6.2-7 concern is that **NDI-compress is unauditable on both +sides**: the MATLAB side ships P-code and the Python side ships committed C +binaries, built at different times, so binary/format drift between the two codecs +cannot be ruled out. Resolving that requires either (a) vendoring the codec C +**source** in-repo and building it in CI, or (b) pinning a single versioned codec +build and committing a checksum manifest of the binaries — plus a cross-language +round-trip fixture test (compress with one language's codec, decompress with the +other, assert byte-identity). That work needs the codec build provenance / source +(not available in this environment) and a paired MATLAB run, so it is flagged for +a focused follow-up. Only the subprocess-timeout hardening and the LICENSE are +done here. diff --git a/src/ndicompress/compress.py b/src/ndicompress/compress.py index 149f99b..90d90b4 100644 --- a/src/ndicompress/compress.py +++ b/src/ndicompress/compress.py @@ -10,10 +10,39 @@ from .utility import get_executable_path +# Maximum seconds to wait for a codec subprocess before treating it as hung. +# Compression/decompression of very large arrays can be slow, so this default is +# generous; override via the NDI_COMPRESS_TIMEOUT environment variable. +def _read_timeout_env(default=300.0): + raw = os.environ.get("NDI_COMPRESS_TIMEOUT") + if raw is None or raw == "": + return default + try: + return float(raw) + except (TypeError, ValueError): + import warnings + + warnings.warn( + f"Invalid NDI_COMPRESS_TIMEOUT={raw!r}; using default {default:g}s.", + RuntimeWarning, + ) + return default + + +_C_EXEC_TIMEOUT = _read_timeout_env() + + def _call_c_exec(exec_name, args): exec_path = get_executable_path(exec_name) cmd = [exec_path] + args - result = subprocess.run(cmd, capture_output=True, text=True) + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=_C_EXEC_TIMEOUT + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError( + f"C executable {exec_name} timed out after {_C_EXEC_TIMEOUT:g}s" + ) from exc if result.returncode != 0: raise RuntimeError(f"C executable {exec_name} failed: {result.stderr}") return result.stdout