fix(cli): split --model/--feature option strings escape-aware (ADR-1190) - #1336
Draft
lusoris wants to merge 7 commits into
Draft
fix(cli): split --model/--feature option strings escape-aware (ADR-1190)#1336lusoris wants to merge 7 commits into
lusoris wants to merge 7 commits into
Conversation
lusoris
pushed a commit
that referenced
this pull request
Sep 5, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 03:01
611d5ee to
b17aed7
Compare
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 06:38
b17aed7 to
bde4513
Compare
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 09:30
bde4513 to
f87f190
Compare
14 tasks
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 09:45
f87f190 to
6b33af3
Compare
14 tasks
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 11:10
6b33af3 to
46c16a4
Compare
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 12:55
46c16a4 to
59442cf
Compare
lusoris
pushed a commit
that referenced
this pull request
Sep 6, 2026
My rebase dedup kept the wrong side. The rule it applied -- when a bug id appears twice after a keep-both rebase, keep the row that matches origin/master -- is right when the branch is merely restating a row master already carries. It is WRONG when the branch is the PR that closes the bug, which is exactly this case: #1336 fixes Netflix/vmaf#766, so its past-tense Recently-closed row is the correct survivor and master's present-tense Open row is the stale one. Restored the branch's closure row and left a tombstone comment where the Open row was, matching the convention used elsewhere in this file. Exactly one row per bug id; scripts/ci/check-state-md-rows.sh reports OK (361 rows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`core/tools/cli_parse.cpp` split the `--model` and `--feature` option strings with raw `strsep` at nine sites and carried no escape state, so every `:` and `=` in the string was a separator whatever the user meant. Reproduced against `cd52f2670`: -m 'path=/a/dir=eq/m.json' -> could not read model from path: "/a/dir" -m 'path=C:\models\vmaf_v0.6.1.json'-> bad option string "\models\vmaf_v0.6.1.json" --feature 'psnr=some_path=C:\x' -> bad option string "\x" The first is the worst: a silent truncation to a phantom path the user never typed. The second is the case Netflix/vmaf#766 reports, and the fork widens the blast radius because `pkg/libvmaf` and `pkg/corpus` paste user-supplied paths straight into `path=<path>`. Replace all nine `vmaf_cli_strsep` call sites with `cli_split()`, which breaks on the first *unescaped* separator and leaves backslash sequences in place, plus `cli_unescape()`, which drops the backslash in `\:`, `\=`, `\.` and `\\` once, at the leaf. Every other backslash is data, so `C:\models\m.json` survives verbatim, and a `:` that spells a Windows drive letter is data rather than a separator. A pair's value is now the whole remainder after the first unescaped `=`, so an inner `=` can no longer truncate a path. The `strsep` / `HAVE_STRSEP` portability shim and its meson probe are deleted with the last call site, which also ends a POSIX-vs-MSVC disagreement about trailing separators. Go callers escape through the new `pkg/cliopt.EscapeValue`, whose round-trip test pins it to the C grammar. Tests: eight cases in `core/test/test_cli_parse.c` (three reproduced failures, `\=`/`\\` escapes, and no-change regressions over `version=...:name=...:disable_clip`, the `--aom_ctc v1.0` psnr option string and a `adm.<option>` overload). Pre-fix, the truncation case fails the assertion and the two backslash cases exit through `usage()`. 114/114 `meson test --suite=fast` pass; `core/tools/cli_parse.cpp` stays at 0 clang-tidy warnings and `core/test/test_cli_parse.c` at its baseline 24 (ADR-1142). No scoring path is touched — the Netflix golden assertions are untouched and the Python harness only ever emits `name=vmaf:path=model/*.json`, which parses byte-identically. Closes T-UPSTREAM-766-CLI-OPTION-STRING-DELIMITERS-2026-09-03. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) Move the ledger row out of "Open bugs" into "Recently closed" per ADR-0165, citing PR #1336 and recording the pre-/post-fix reproducer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seven `cli_parse_corpus/` seeds all predate the escape-aware splitter, so the fuzzer had no starting point that exercises `cli_split()` / `cli_unescape()`. Add one NUL-separated argv seed covering, in a single input, the three constructs the new grammar introduces: an unescaped inner `=` in a value (`path=/dir=eq/...`, the silent-truncation case), a backslash-escaped delimiter (`m\:colon.json`), and a Windows drive-letter colon inside a `--feature` option value (`some_path=C:\x`). Force-added, like the seven seeds already tracked beside it: the `core/test/fuzz/*_corpus/` ignore rule exists to keep fuzzer-generated corpus growth out of the tree, not the checked-in seeds. Ported from PR #1310, which proposed the same idea but wrote the seed as one space-joined line with a trailing newline. `tokenise_argv()` in core/test/fuzz/fuzz_cli_parse.c splits on NUL, so that form collapses to a single argv token and never reaches the option-string parsers at all; this seed uses the NUL separation the harness and the other seeds use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…190) The harness comment still described the option-string sub-parsers as running `strsep` chains. ADR-1190 replaced all nine split sites with `cli_split()` / `cli_unescape()` in the same PR, so the rationale now names the current splitter and keeps `strsep` only as the historical note. Comment-only; no behaviour, no new symbols, no tidy delta (the fuzz targets are not in the CPU lane's compile_commands.json). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each dropped row restates one origin/master already carries; master is the authoritative record. Verified with scripts/ci/check-state-md-rows.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each dropped row restates one origin/master already carries; master is the authoritative record. Verified with scripts/ci/check-state-md-rows.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My rebase dedup kept the wrong side. The rule it applied -- when a bug id appears twice after a keep-both rebase, keep the row that matches origin/master -- is right when the branch is merely restating a row master already carries. It is WRONG when the branch is the PR that closes the bug, which is exactly this case: #1336 fixes Netflix/vmaf#766, so its past-tense Recently-closed row is the correct survivor and master's present-tense Open row is the stale one. Restored the branch's closure row and left a tombstone comment where the Open row was, matching the convention used elsewhere in this file. Exactly one row per bug id; scripts/ci/check-state-md-rows.sh reports OK (361 rows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/t-upstream-766-cli-option-string-delimit
branch
from
September 6, 2026 18:22
690d685 to
bd38e6a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
core/tools/cli_parse.cppsplit the--model/--featureoption strings with rawstrsepat nine sites and carried no escape state, so every:and=in the string was a separator whatever the user meant — Netflix/vmaf#766, ledger rowT-UPSTREAM-766-CLI-OPTION-STRING-DELIMITERS-2026-09-03. The worst failure was silent:-m 'path=/a/dir=eq/m.json'truncated to/a/dirand reported a phantom path the user never typed. This PR replaces all nine sites with an escape-awarecli_split()+cli_unescape()pair (ADR-1190), adds a Windows drive-letter affordance sopath=C:\...needs no escaping, makes a pair's value the whole remainder after the first unescaped=, and escapes the paths the Go surfaces synthesise via the newpkg/cliopt.EscapeValue.Type
fix— bug fixChecklist
make format && make lintis green locally (clang-format clean;core/tools/cli_parse.cppmeasures 0 clang-tidy warnings andcore/test/test_cli_parse.c24, exactly its ADR-1142 cpu baseline).meson test -C build-cpu --suite=fast→ 114/114 Ok, 0 Fail.pkg/cliopt/escape.go,pkg/cliopt/escape_test.go) carry theCopyright 2026 Lusorisheader.version=…:name=…:disable_clip,psnr=reduced_hbd_peak=true:…andadm.<option>=<value>parse byte-identically (regression tests cover all three). The only behaviour deltas are the ones that used to fail or truncate.docs/adr/_index_fragments/1190-cli-option-string-escape-grammar.mdwith the slug appended to_order.txt;docs/adr/README.mdwas regenerated byscripts/docs/concat-adr-index.sh --write(--checkclean).Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR:T-UPSTREAM-766-CLI-OPTION-STRING-DELIMITERS-2026-09-03moved out of Open bugs into Recently closed, citing this PR.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.name=vmaf:path=model/<file>.json(compat/python-vmaf/core/quality_runner.py:1191-1198) and<feature>=<k>=<v>:<k>=<v>(compat/python-vmaf/__init__.py:265-273) — none of those contain a backslash or a second=in a value, so they parse byte-identically under the new grammar.Cross-backend numerical results
Not applicable — no backend, kernel or scoring code is touched.
Reproducer
Build:
meson setup build-cpu core -Denable_cuda=false -Denable_sycl=false -Db_lto=false && ninja -C build-cpu -j 4 tools/vmaf test/test_cli_parsePre-fix (
git show origin/master:core/tools/cli_parse.cppcompiled into the same build dir):Post-fix, same commands:
Every path in the "post-fix" block reaches the model/feature loader with the exact bytes the user typed; the remaining errors are the expected "this file does not exist" / "this option is not a psnr option" ones.
Deep-dive deliverables (ADR-0108)
## Alternatives considered; there is no external literature to survey.docs/adr/1190-cli-option-string-escape-grammar.md## Alternatives considered(six options: escapes+drive-letter, escapes only, drive-letter only, hard-error on a second=, dropping\\from the escape set, quote-based grammar).AGENTS.mdinvariant note —core/tools/AGENTS.md, new ADR-1190 block: split-then-unescape is two passes, the value is the remainder after the first unescaped=, the overload key is split on.before unescaping, the drive-letter rule is load-bearing ergonomics, and the Go escaper is the same grammar in another language.changelog.d/fixed/T-UPSTREAM-766-cli-option-string-delimiters.md;CHANGELOG.mdre-rendered withbash scripts/release/concat-changelog-fragments.sh --write.docs/rebase-notes.md, sectionfix/t-upstream-766-cli-option-string-delimit.Documentation (ADR-0100)
docs/usage/cli.mdgains an Option-string grammar section (delimiters, the escape table, worked shell examples, the drive-letter rule, the UNC caveat and the shell-quoting caveat), and both the--modeland--featuresections link to it from the sentences that previously said only "colon-delimited key/value string".ffmpeg patch stack (CLAUDE.md §12 r14)
No patch change is needed and none is made. The rule covers public C-API entry points,
--enable-libvmaf-*configure flags,LIBVMAFContextfields andvf_libvmaf.cvariants; this PR touches none of them —cli_parse.cppis the CLI tool, not the library.ffmpeg-patches/0008-add-libvmaf_tune-filter.patch'sload_model()takes the whole remainder after the first=and never splits on:, so it is already immune to both failure modes, and ffmpeg's filtergraph parser owns escaping at that layer — teaching it the CLI grammar would double-unescape.Known follow-ups
compat/python-vmaf/) still concatenatespath=strings without escaping. It only ever passes repo-relative model paths, so nothing is reachable today; escaping there would touch the Netflix-mirrored harness and is deliberately out of scope.Supersedes #1310 (same bug, same ledger row)
#1310 (
fix/cli-option-string-escapes, ADR-1180) implemented the same fix andwas closed in favour of this PR. Both binaries were built from their own branch
tips and probed side by side (
build-cpu/tools/vmaf, CPU-only,-Db_lto=false):ee7e13e3b)f87f190ba)-m 'path=/a/dir=eq/m.json'could not read model from path: "/a/dir"— still silently truncated… "/a/dir=eq/m.json"--feature 'psnr=some_path=C:\x'Problem parsing feature "psnr", bad option string "\x"— still rejectedunknown option 'some_path'(path reached the extractor whole)-m 'path=C:\models\vmaf_v0.6.1.json'-m 'path=/a/dir\:colon/m.json'#1310 kept the second
strsep-style split on=(so a value still ends at thenext unescaped
=) and put its drive-letter affordance insideparse_model_config()behind!strcmp(key, "path"), so--featurenever gotit. It also left
vmaf_cli_strsepand theHAVE_STRSEPprobe in the tree asdead code, exported
vmaf_cli_splitfromcli_parse.h, and duplicated its Goescaper verbatim in
pkg/libvmafandpkg/corpus.Ported from #1310:
core/test/fuzz/cli_parse_corpus/seed_escapes.argv(commit
f87f190ba), rewritten NUL-separated —tokenise_argv()incore/test/fuzz/fuzz_cli_parse.csplits on NUL, and #1310's space-joined,newline-terminated seed collapsed to a single argv token that never reached the
option-string parsers.
Deliberately not ported: #1310's edit to
ffmpeg-patches/0008-add-libvmaf_tune-filter.patch. It is corrupt — thenew-file hunk header still declares
@@ -0,0 +1,427 @@while the body carries429
+lines, sogit applywrites a 427-linevf_libvmaf_tune.cthat ismissing the closing
FILTER_QUERY_FUNC2(query_formats);and};offf_vf_libvmaf_tuneand cannot compile. Its intent is also wrong for thatlayer: unescaping
\\inside the filter would turn a UNC\\server\shareinto
\server\share, and ffmpeg's filtergraph parser already owns escapingthere (see the "ffmpeg patch stack" section above).