Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions docs/Audit_Remediation_Results_2026-06-12.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 30 additions & 1 deletion src/ndicompress/compress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading