Conversation
JSON objects are unordered, but `.zi-parse-json` selected the containing object only when the wanted key was written first, and `.zi-get-package` then read fixed slots. Writing `zi-ices` before `plugin-info` left the output variable unassigned and aborted with "bad set of key/value pairs for associative array". Select the smallest object that declares the key, and look both slots up by name. The parser also stored raw string bodies, leaving `\\`, `\t`, `\n`, `\/` and `\uXXXX` in ice values that are later executed. Only `\"` was repaired, by a substitution in the caller that corrupts `"a\\\"b"` into `a"b` instead of `a\"b`. Decode the escapes where the string is captured and drop that substitution. Surrogate pairs, unknown escapes and malformed `\u` are left as written. Refs #513
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings remain, and malformed-escape coverage is incomplete.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes package manifest parsing for unordered keys and JSON escape decoding.
Changes:
- Resolves manifest fields by key name.
- Adds JSON string unescaping.
- Adds regression tests and CI coverage.
File summaries
| File | Summary and findings |
|---|---|
tests/package-manifest-parsing.zsh |
Adds parsing tests. Nit (1 vote): malformed \u escape coverage is missing. |
lib/zsh/install.zsh |
Updates parser and field lookup. Critical (3 votes): raw text matching can mistake escaped string content for a key. Moderate (3 votes): surrogate pairs are decoded independently. |
.github/workflows/zsh-n.yml |
Registers the new test in CI. |
Review details
Suppressed comments (1)
tests/package-manifest-parsing.zsh:104
- The acceptance criteria include malformed
\uescapes, but this test only exercises an unknown\qescape. A regression in the separate malformed-escape fallback could therefore pass CI; add a parsed\u12value and assert that it remains byte-for-byte unchanged.
# An unknown escape is not JSON; leave it exactly as written.
check ver "a${bs}qb" || return 1
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ___found="$___text" | ||
| # JSON objects are unordered, so the wanted object is the smallest one | ||
| # that declares the key, not merely one that opens with it. | ||
| if [[ $___text = [[:space:]]#\{*[\"\']${___key}[\"\'][[:space:]]#:* ]]; then |
Comment on lines
+25
to
+27
| if [[ $___esc == u && $___tail == (#b)([0-9a-fA-F](#c4))(*) ]] { | ||
| ___code=16#$match[1] | ||
| ___out+=${(#)___code} ___rest=$match[2] |
The parser is a fork of `@str-parse-json', not a mirror, and the two have diverged in three places. Nothing recorded that, so a future reader had no way to know that swapping in the library copy is a regression. Refs #513
This was referenced Sep 12, 2026
Merged
`Strings[level/n]` numbers objects across the whole parsed subtree in document order, but the profile's ice-list was addressed from its position inside `zi-ices` alone. An object nested in a member written before `zi-ices`, such as an npm-style `bugs` block in `plugin-info`, shifted the numbering and resolved a different profile's ices. Those values include `atclone` and `atpull`, so the failure mode was running the wrong commands. Every such object leaves an `--object--` marker in its own member's string, so the count of markers in the preceding members is the exact offset. No change to `.zi-parse-json` or to the shape of the `Strings` hash. Re-parsing keyed on the profile name also fixes the nesting case, but if the profile name is reused as a key in a smaller object the smallest-object rule selects the decoy. Both shapes are now covered. Extract the lookup into `.zi-read-package-manifest`. The test previously mirrored the caller's slot arithmetic, so it would have passed against the unfixed code; now both call the same function. Refs #515
This was referenced Sep 12, 2026
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.
Problem
Two independent defects in
.zi-parse-json/.zi-get-package, both found whilereviewing whether
zsh-string-libshould replace the vendored parser. Thelibrary copy fails identically, so adoption does not fix either.
Key order. The parser matched the wanted key only as an object's first
key, and the caller then read fixed slots
Strings[2/1]andStrings[2/2].JSON objects are unordered. Swapping
zi-icesbeforeplugin-infoinz-shell/zsh's own manifest, with no other change, left the output variableunassigned:
Escapes. The parser stored raw string bodies. Only
\"was repaired, by ablunt substitution in the caller, so the rest reached ice values that are later
executed:
"a\\b"a\\ba\b"p\tq"\t"l1\nl2"\n"xAy"AxAy"a\\\"b"a"ba\"bThe last row is the caller's substitution being wrong where it did fire.
Change
with it.
plugin-infoandzi-icesslots up by name inStrings[1/1], usingthe index idiom the caller already uses for profile names.
.zi-unescape-json-stringand decode escapes where a double-quoted body iscaptured; drop the now-redundant and incorrect
\"substitution.Surrogate pairs, unknown escapes such as
\q, and malformed\uare leftexactly as written.
Verification
tests/package-manifest-parsing.zsh, registered inzsh-n.yml(thezsh -nmatrix discovers files itself; the paths filters and job are explicit).It fails on
nextand passes here.zsh -nclean.scripts/public-contract-impact.zshagainstnext: no public-contract changes.z-shell/zshmanifest: every resolved ice isbyte-identical to
nextoncenext's caller-side\"repair is applied. Theonly change is that the decoding now happens in the parser.
built at run time, after an earlier heredoc silently ate
Aand made thatcase pass without testing anything.
Also fixed here: profile ices shifted by nested members (#515)
Strings[level/n]numbers objects across the whole subtree in document order,so an object nested in a member written before
zi-ices(an npm-stylebugsblock in
plugin-info, say) shifted the profile bodies and resolved a differentprofile's ices. Since ice values include
atcloneandatpull, that meansrunning the wrong commands. It was pre-existing, verified identical before and
after the first commit here.
Every such object leaves an
--object--marker in its own member's string, sothe marker count across the preceding members is the exact offset. No change to
.zi-parse-jsonor to the shape of theStringshash.Re-parsing keyed on the profile name also fixes the nesting case, but trades it
for another: if the profile name is reused as a key in a smaller object, the
smallest-object rule picks the decoy. Both shapes are covered by tests.
The lookup moved into
.zi-read-package-manifest, because the test had beenmirroring the caller's slot arithmetic and so would have passed against the
unfixed code. Test and caller now call the same function, and removing the
offset from that function fails the suite.
Verified again after this commit: 27 test files pass,
zsh -nclean, nopublic-contract changes, and every profile of the real
z-shell/zshmanifeststill resolves to its own ices (
5.9tozsh-5.9,5.3.1tozsh-5.3.1).Added after the first review request
A second commit records the fork on
.zi-parse-json: it began as@str-parse-jsoninz-shell/zsh-string-liband is now a maintained fork, withthree deliberate divergences named in the comment. Nothing recorded that before,
so a reader had no way to know that swapping in the library copy is a
regression. Comment only, no behavior change.
Refs #513
Refs #515