Skip to content

install.sh and openRepoShape: a $OPENREPOSHAPE_BIN_DIR, the repository or a ref holding a bidi control, a character of no width, a soft hyphen or U+2028 is refused by its code point before any request and never echoed, so no line shows the value as another - #841

Merged
brettheap merged 12 commits into
mainfrom
fix/837-bin-dir-format-chars
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #837 (comment)

Stacked on

Fix round 1, on rv841's review (READY, 2 SHOULDs, 4 NITs)

Finding What changed
SHOULD 1: the set leaves out U+FFF9 to U+FFFB, U+E0001 and U+1D173 to U+1D17A All three groups are in the list now, and in MISDRAWN. The encoder builds the four-byte form as well (lead byte F0 to F4, then three continuation bytes, with bash 3.2's arithmetic and %b's \0NNN). The comment says which forms it builds. I checked every other format character by the same rule, and the comment gives each one left out its reason (table below)
SHOULD 2: the repository and ref echo Closed here, not in a follow-up. Every [![:print:]] check in both files now asks misdrawn_characters first and refuses by the code point, without echoing the value. In install.sh that is checked_repo, checked_version (the version named, and the newest release's tag), the option check and latest_from_redirect's location. In the command it is shape_checkout (the repository) and ref_fault ($OPENREPOSHAPE_REF and the record). Tested under C and C.UTF-8, through both entry points
NIT 3: U+00AD is not invisible Every refusal now says "one that is invisible, is drawn as another character, ends a line or reorders the text around it"
NIT 4: inexact or missing reasons The comment now gives each reason: U+180E (Mongolian needs it, as Persian needs U+200C); U+1160, kept out because it forms a syllable block with the consonant before it; U+115F, U+3164 and U+FFA0, drawn as blank cells like the spaces; and the variation selectors, tags and U+034F
NIT 5: no test pins the order of the two checks A row holding a tab and U+202E expects the control refusal. rv841's mutant E, with the misdrawn check above the control check, now fails in each file
NIT 6: four lines of link setup in two files Left as they are: they are under Sonar's 10-line threshold and came from #804

What changed

The goal is #837's first line: refuse, before any request and before any line names it, a $OPENREPOSHAPE_BIN_DIR holding a character a terminal draws as something other than itself. #804's check (*[[:cntrl:]]* and the C1 bytes, under LC_ALL=C) passed every such character under C and C.UTF-8 alike. The command then placed itself under <dir>/a U+202E forged/bin, exit 0, and printed the value in four lines (installed at, placed in, the PATH note, the export PATH line), and install.sh made its request.

#837's own line also named checked_repo and the other [![:print:]] checks of both files (w760's report): a UTF-8 locale's U+202E passes [![:print:]], and the refusal after it echoed the value. Fix round 1 closes those too, so Closes #837 closes the whole record.

Refused, not printed with LC_ALL=C printf '%q'. Under LC_ALL=C, %q prints every byte past ASCII as octal, so a directory called café, or one named in Persian or Hindi, would print as $'caf\303\251'. It is bash's own spelling, and bash 3.2's is unmeasured. The export PATH line would carry $'…', which no POSIX sh reads. And every line naming the directory would change, for every non-ASCII name. The value is the person's own variable, so a refusal costs them one rename, and it is what #804 does for a control character.

What a terminal draws (fribidi, libfribidi.so.0 through ctypes, each name in an LTR paragraph as a terminal line is, the zero-width characters dropped):

~/<U+202E>.local/bin        is drawn as  ~/nib/lacol.
~/<U+2067>.local/bin        is drawn as  ~/local/bin.
/opt/tools-1<U+200F>/2/bin  is drawn as  /opt/tools-12//bin
/opt/tools-1<U+061C>/2/bin  is drawn as  /opt/tools-12//bin
~/.local<U+200B>/bin        is drawn as  ~/.local/bin

The set: 41 code points, MISDRAWN_CHARACTERS=(00AD 061C 200B 200E-200F 2028-202E 2060-206F FEFF FFF9-FFFB 1D173-1D17A E0001) in the launcher, copied as it is into install.sh. wcwidth below is glibc's, under C.UTF-8.

Characters In or out Why
U+202A to U+202E, U+2066 to U+2069 (embeddings, overrides, isolates) in they reorder the rest of the line (above). repo_shape.spelled (#391) escapes them, and unwritable_reason (#255) refuses them
U+061C, U+200E, U+200F (the marks) in invisible (wcwidth 0), and as strong directional characters they reorder the neutral characters beside them: /opt/tools-12//bin above. #391 escapes all twelve bidi controls for this reason; #255 refuses only the nine
U+2028, U+2029 in they end a line for every reader that keeps Unicode's line breaks, Python's str.splitlines among them, and wcwidth gives -1 for both. #391 and #255 both treat them as line breaks; #804 left them "refused in none", open to a ruling. NOT_CONTROLS loses its two rows for them
U+200B, U+FEFF, U+2060 to U+206F in no width (wcwidth 0, or -1 for the unassigned U+2065): ~/.local<U+200B>/bin is drawn as the name of another directory. perl's \p{Default_Ignorable_Code_Point} holds for every code point U+2060 to U+206F, so the range has no gap
U+FFF9 to U+FFFB (interlinear annotation), U+1D173 to U+1D17A (musical beam, tie, slur and phrase controls), U+E0001 (LANGUAGE TAG, deprecated) in (fix round 1) format characters with wcwidth 0 that nothing needs; the command placed and printed them before
U+00AD (soft hyphen) in drawn as the font's hyphen in a terminal cell (wcwidth 1) and as nothing elsewhere, never as what it is
U+200C, U+200D, U+180E out Persian, the Indic scripts and emoji sequences need U+200C and U+200D (the issue's Direction), and Mongolian needs U+180E; pinned in NOT_CONTROLS (the first two)
U+13430 to U+1343F, U+1BCA0 to U+1BCA3 out the Egyptian hieroglyph and shorthand format controls: wcwidth 0, but those scripts build their signs with them
U+E0020 to U+E007F (tags), the variation selectors, U+034F, U+1160 out each is drawn as part of the character before it: tags make a subdivision flag (pinned in NOT_CONTROLS), a variation selector chooses how that character is drawn, U+034F keeps a grapheme whole, and U+1160 forms a syllable block with the consonant before it
U+0600 to U+0605, U+06DD, U+070F, U+0890, U+0891, U+08E2, U+110BD, U+110CD out format characters drawn over the digits after them: wcwidth 1, drawn as themselves
every space (U+00A0, U+200A, U+202F, U+205F, U+3000), U+115F, U+3164, U+FFA0 out drawn as a blank cell, as a space is (wcwidth 1 or 2)

How it matches. misdrawn_characters builds each character's UTF-8 from its code point with printf's octal escapes (printf -v bytes '\\0%o…', then '%b', which POSIX defines): two bytes below U+0800, three below U+10000, four above. It looks for those bytes in the value under LC_ALL=C, in the subshell #804's check uses, and names every one it finds as U+XXXX, in the list's order. So one list says what is refused and what the refusal names, and no byte is read back as a number. Nothing in it is newer than bash 3.1 by the manual (printf -v came in 3.1); bash 3.2 itself was not run. Swept against Python at 4123b9e5, the function lifted from the file, /tmp/a + the character + b:

  • 81,317 names: every BMP code point but the surrogates (63,487); every 61st code point from U+10000 to U+10FFFF; and the whole of U+1D100 to U+1D1FF, U+E0000 to U+E007F, U+E0100 to U+E01EF, U+13430 to U+1343F and U+1BCA0 to U+1BCA3, with a few emoji (17,830 past U+FFFF).
  • 0 mismatches under C, 0 under C.UTF-8, and each of the 41 is still named behind a lone 0xE9.

Where.

The refusals, measured by a probe (<H> stands for the probe's directory; gh stub first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR, a curl stub that logs and exits 22, LC_ALL=C.UTF-8, U+202E shown as <U+202E>):

Before (the stacked base), OPENREPOSHAPE_BIN_DIR=<H>/a<U+202E>forged/bin openRepoShape --install, exit 0, placed:

openRepoShape: installed at <H>/a<U+202E>forged/bin/openRepoShape
openRepoShape: 1 of 1 placed in <H>/a<U+202E>forged/bin
  <H>/a<U+202E>forged/bin is not on $PATH. Add it:
    export PATH="<H>/a<U+202E>forged/bin:$PATH"

and install.sh v9.9.9 made one request (exit 2 only because the stub curl failed it). Now, exit 2, nothing placed, nothing on stdout, no request:

REFUSED: --install would place this command in a directory whose name holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it, so no line about $PATH can print the name as it reads. Nothing was placed. Set $OPENREPOSHAPE_BIN_DIR to a directory without one, then run this again.
REFUSED: the directory to install into holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it, so no line about $PATH can print the name as it reads. Nothing was installed. Set $OPENREPOSHAPE_BIN_DIR to a directory without one, then run this line again.

The repository and the ref, at cc5ae88e (each exit 2, the value echoed) and at 4123b9e5 (exit 2, not echoed):

install.sh v1<U+202E>forged
  cc5ae88e: REFUSED: the version is 'v1<U+202E>forged', which is not a release tag, branch or commit: a ref starts with a letter or digit, ...
  4123b9e5: REFUSED: the version holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. No release tag, branch or commit holds one.
install.sh -x<U+202E>forged
  cc5ae88e: REFUSED: unknown option '-x<U+202E>forged'. The one argument is a version, such as v1.0.1; with none, the newest release.
  4123b9e5: REFUSED: an option holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. The one argument is a version, such as v1.0.1; with none, the newest release.
OPENREPOSHAPE_REPO=acme/x<U+202E>forged install.sh
  cc5ae88e: REFUSED: $OPENREPOSHAPE_REPO is 'acme/x<U+202E>forged', which is not an owner/name on github.com, such as opensoft/openRepoShape: this installer builds github.com URLs from it.
  4123b9e5: REFUSED: $OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. It must be an owner/name on github.com, such as opensoft/openRepoShape.
OPENREPOSHAPE_REF=v1<U+202E>forged OPENREPOSHAPE_RECORD_REF=1 openRepoShape --install
  cc5ae88e: REFUSED: $OPENREPOSHAPE_REF is 'v1<U+202E>forged', which is not a branch, tag or commit this tool will pass to `git checkout`: a ref starts with a letter or digit, ...
  4123b9e5: REFUSED: $OPENREPOSHAPE_REF holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. No branch, tag or commit this tool will pass to `git checkout` holds one.

The command's --doctor refuses such a repository with "OPENREPOSHAPE_REPO holds a character … around it. This tool will not put it on a git clone command line. Set it to a URL or a path.", before any git runs. A redirect to a location holding one gives "it redirects to a location that names no release". Two characters at once are named (U+200B, U+202E). Under C every one of these words is the same. A Persian name with U+200C, an emoji sequence with U+200D and a subdivision flag (tags) pass through both files.

Also: install.sh's header (exit status 2) names #837. #804's comment in both files no longer says U+2028 and U+2029 "are refused in none". export_path_line's comment names all three refusals.

Tests

File Test What it pins
tests/test_openreposhape_command.py test_a_bin_dir_is_refused_in_every_locale_whatever_else_it_holds (was #804's C1 test; now over REFUSED_IN_EVERY_LOCALE) each of the 41, then two at once (both named, in the list's order), one after a byte that is not UTF-8, one beside a :, one in a drive spelling; a tab before U+202E, refused as a control character (NIT 5); and #804's five C1 rows. Each under C and C.UTF-8: exit 2, the words with the code points, nothing placed, no forged tail and no character of MISDRAWN in either stream
tests/test_install_sh.py test_the_installer_refuses_in_every_locale_before_any_request (was #804's C1 test) the same 51 values through install.sh: exit 2, the fake curl and gh record no call, nothing placed, $TMPDIR empty
tests/test_openreposhape_command.py test_a_ref_or_repository_drawn_as_another_is_refused_unechoed $OPENREPOSHAPE_REF for --install to record, the record beside an installed command (--version), $OPENREPOSHAPE_REPO for --doctor to clone (a git that leaves a mark if it runs): under C and C.UTF-8, exit 2, the words, nothing placed, no clone, neither stream holds the value
tests/test_install_sh.py test_the_installer_refuses_a_value_drawn_as_another_unechoed a version, an option, the repository, the newest release's tag, the redirect's location: under C and C.UTF-8, exit 2, nothing placed, nothing fetched, no request at all where the value is not the network's, neither stream holds the value
tests/test_openreposhape_command.py test_misdrawn_characters_names_its_own_and_nothing_else[openRepoShape, install.sh] the function lifted out of each file, under LC_ALL=C, over every code point that shares a first byte with one of the 41 (U+0080 to U+00BF, U+0600 to U+063F, U+2000 to U+20FF, U+FE00 to U+FFFF, U+1D100 to U+1D1FF, U+E0000 to U+E007F) and a sample of the planes past U+FFFF (the Kaithi, Egyptian and shorthand format characters among it): it names exactly the 41, and nothing else. The names go on stdin as bytes, so this one also runs under Git Bash
tests/test_openreposhape_command.py test_install_sh_carries_the_launchers_misdrawn_check_as_it_is install.sh's list and function are the launcher's, byte for byte
both test_a_bin_dir_is_refused_before_a_link_in_it_names_it, test_a_bin_dir_is_refused_before_the_symlink_check_names_it (#804's, now over LINKED_BIN_DIRS) U+202E beside the carriage return: refused before the symlink refusal names the link
both test_a_bin_dir_with_an_unprinted_letter_is_placed, test_the_installer_places_a_directory_with_an_unprinted_letter (#804's, over NOT_CONTROLS) NOT_CONTROLS drops U+2028 and U+2029, and gains a Persian word with U+200C, an emoji sequence with U+200D, U+2026 (first byte 0xE2), U+FF21 (first byte 0xEF) and England's subdivision flag (tags, first bytes 0xF3 0xA0, as U+E0001's): placed, under both locales, through both files
tests/test_openreposhape_command.py assert_refused_saying (shared by every refusal test above) exit 2, the words, nothing on stdout, and no control character, no character of MISDRAWN and no forged tail in either stream

The new and changed cases (-k "every_locale or misdrawn or unprinted_letter or link_in_it or symlink_check_names or drawn_as_another or record_that_is_not") give 260 passed at 4123b9e5.

  • Over cc5ae88e's two shell files: 199 failed, 61 passed.
    • 48 fail on behaviour: the 12 new code points, which the command placed (24) and install.sh asked the network about (24).
    • 15 of the 16 repository and ref cases fail, because the value is echoed or refused in other words. The redirect location under C passes, because cc5ae88e's [![:print:]] already hid it.
    • The lifted test fails in both files (2).
    • 134 fail only on the new words: cc5ae88e refused them already, without "is drawn as another character".
  • Over the stacked base's shell files, cc5ae88e's own tests gave 137 failed, 46 passed.

Mutants. Each one is a single edit in a git archive copy of the head, run against the selected cases after an unmutated control. List and function mutants are applied to both files alike, so the identity test is not what kills them. The runner, the mutants, the run logs and the kill tables are in this PR's attachments, under mutants/; the probes are under probes/.

Fix round 1, 32 mutants, all killed (control: 260 passed):

Mutant Killed by
FFF9-FFFB→FFFA-FFFB, →FFF9-FFFA; 1D173-1D17A→1D174-1D17A, →1D173-1D179; no E0001; E0001→E0002 (M01, M02, M05, M06, M09, M12) that code point's locale cases in both files, and the lifted test
FFF9-FFFB→FFF8-FFFB, →FFF9-FFFC; 1D173-1D17A→1D172-1D17A, →1D173-1D17B; E0001→E0000-E0001 (M03, M04, M07, M08, M10) the lifted test (both files)
E0001→E0001-E007F (M11) the subdivision flag, placed, in both files; the lifted test
no four-byte branch, lead 0xE0, lead >> 17, second byte & 31, last byte & 31 (B01-B05) the locale cases of the code points past U+FFFF that each breaks, and the lifted test
install.sh: the repository check dropped, or moved after the control case, or after the shape cases (S01-S03) the_repository (after the control case: under C, where [![:print:]] answers in other words)
install.sh: the version check dropped, moved after the control case, or echoing the value (S04, S05, S08) a_version, the_newest_tag
install.sh: the option check dropped (S06); the location check dropped (S07) an_option; a_location under C.UTF-8
the command: the repository check dropped, or moved after the control case (S09, S10) a_repository_to_clone
the command: the ref check dropped, or its return 0 dropped so the shape refusal follows (S11, S12) a_ref_to_record, a_recorded_ref
rv841's mutant E, in each file: the misdrawn check above the control check (E01, E02) a-tab-then-u+202e, both locales
the words of cc5ae88e (W01) every misdrawn locale case and the link case of the command

cc5ae88e's 50 mutants: 48 killed, 2 survived. The survivors are LC_ALL=C dropped from the new call, in either file. That is equivalent in C and C.UTF-8, and in the other locales rv841 tried. It would differ only in a multibyte locale that is not UTF-8, and none could be built here. Their table is mutants/kills.txt.

Runs (Linux, bash 5.2.21, Python 3.12.3; env -u GH_TOKEN -u GITHUB_TOKEN GH_CONFIG_DIR=<empty> PATH=<a gh stub that exits 97>:… /usr/bin/python3 -m pytest -q -p no:cacheprovider, one basetemp, df -i checked first):

Set Base 8a559cb5 cc5ae88e 4123b9e5
test_openreposhape_command.py, test_install_sh.py, test_install_sh_filter_repo.py, test_openreposhape_package.py 549 passed, 103.60s
the same four at the restacked head f2483cf1 (#804's squash 2e476a77 and main 677f9bfa merged; the four files are 4123b9e5's, byte for byte) 549 passed, 214.63s
those four with test_repo_hygiene.py, test_cli_reference.py, test_agents_md_codes.py 479 passed 624 passed 696 passed, 131.30s
test_shape_doctor.py (it runs the launcher's --doctor), test_release_workflow.py, test_doctor_validator_rows.py, test_doctor_machine_row.py 549 passed, 3 skipped, 1 xfailed 549 passed, 3 skipped, 1 xfailed 549 passed, 3 skipped, 1 xfailed, 741.40s
the three files tests-git-bash runs, every Path.write_text writing CRLF (a plugin; a probe test passes with it and fails without it) 419 passed 491 passed, with the plugin's probe test passing in the same run (492 in all), 277.80s

render-cli-reference.py --check: ok.

Not changed

Closes #837

🤖 Generated with Claude Code

Summary by Sourcery

Refuse terminal-misdrawn Unicode characters in installer inputs and paths before they can trigger requests, filesystem changes, or misleading output.

Bug Fixes:

  • Reject repositories, refs, installer options, redirect locations, and installation directories containing terminal-misdrawn Unicode characters before requests, checkout, placement, or output.
  • Prevent refused values from being echoed in installer and command-line diagnostics.

Enhancements:

  • Share consistent code-point detection and refusal behavior between install.sh and openRepoShape, including support for supplementary-plane characters.
  • Expand coverage across locales, malformed UTF-8, mixed control characters, symlink checks, and values received from redirects or records.

Tests:

  • Add comprehensive tests for all refused code points, refusal ordering, unechoed diagnostics, byte-level detection, and parity between the two shell scripts.

Brett Heap and others added 8 commits October 16, 2026 14:07
…IR` is respelled with `cygpath -u` or refused before install.sh's fetch, and the PATH note single-quotes a directory bash would still read, and `--install` refuses one holding `:` or a newline, so install.sh names `/d/tools/bin` as the command does and the `export PATH` line does what it says (#667)

#649's two leftovers, both from #656's review.

install.sh read `$OPENREPOSHAPE_BIN_DIR` as typed, so with `D:\tools\bin`
its `preflight:` and `done:` lines (and `install:` for an old release) said
`D:\tools\bin/openRepoShape` while the command's own lines said
`/d/tools/bin/openRepoShape`, and where no `cygpath` answers the command's
`--install` refused (exit 2) only after install.sh had fetched it. install.sh
now carries a copy of the command's rule where it reads the variable, before
the symlink check and the fetch: a value that starts `[A-Za-z]:/` or
`[A-Za-z]:\` is spelled with `cygpath -u --`, and is refused with exit 2 where
no `cygpath` answers, fails, or answers a drive spelling again. The refusal
does not echo the value.

The command's PATH note printed `export PATH="$dir:$PATH"` with the directory
between double quotes as it was, so `a"b$HOME c` printed a line `eval` could
not parse, and a `$`, a backquote or a `\` in a name was read, not kept.
`export_path_line` keeps today's line byte for byte for a directory that
double quotes hold (a space and an apostrophe included) and, for one holding
a `"`, `$`, backquote, `\` or `!` (bash reads that at a prompt as history),
prints it between single quotes with `'\''` for an apostrophe. `--install`
also refuses a directory holding a `:` or a newline, which no `$PATH` line can
name, before anything is placed; the newline's refusal does not echo it.

Tests feed one value through install.sh and through the command, in each
spelling and each state of `cygpath`, and pin every `install:`, `preflight:`
and `done:` line; the PATH note is checked as printed, by `eval` and by
`bash -i`.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…holding a control character before they name it, and install.sh refuses a `:` before its fetch, so a carriage return or an escape is no longer echoed as typed (#760)

#667's two checklist candidates, with rv667's NIT 2.

`--install` refused a newline in `$OPENREPOSHAPE_BIN_DIR` and nothing else
that is a control character: a carriage return, an escape, a tab or DEL
placed the command and was printed as typed (`/tmp/x^MFORGED is not on
$PATH`; an escape sequence reaches the terminal the same way). The newline
arm is now `*[[:cntrl:]]*`, first in `install_commands`, ahead of the
Windows spelling and the colon, and its refusal still says nothing of the
value. It is `[[:cntrl:]]` and not `[![:print:]]`, so a directory with a
letter the locale does not print (`café` under LC_ALL=C) is placed. Which
C1 controls match is the locale's, measured with bash 5.2.21: C.UTF-8
matches U+0080 to U+009F (and U+2028, U+2029), C and POSIX match none, and
the ASCII controls and DEL match in all three.

install.sh learnt of a `:` or a newline only after its fetch: it fetched the
launcher, ran `--install`, and said only that `--install` refused (exit 2); a
release older than #667 placed the command under the directory. It now
carries both arms of the command's `case`, copied as #667 copied the
drive-letter one: the control-character arm first, before any line names the
directory, and the colon arm after the Windows respelling, both before the
symlink check and any request. Where the control-character refusal says
nothing of the value, the colon refusal names the directory, as the
command's does.

Tests feed each value through install.sh and through the command (a colon,
two directories, a drive-relative name, a newline, a carriage return, an
escape, a tab, DEL, a drive spelling with a newline or an escape, a colon
with an escape): exit 2, no request, nothing placed, neither stream holds a
forged tail. A link in a directory holding a carriage return, a drive
spelling, a colon with an escape, a letter under LC_ALL=C and LC_ALL=C.UTF-8,
a C1 control under a UTF-8 locale, and the `$HOME` default pin the order and
the width of the arms.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…`LC_ALL=C` and names a C1 control's two bytes, so U+0080 to U+009F is refused in every locale and whatever else the value holds, where a UTF-8 locale let one through beside a byte that is not UTF-8 (#760)

Lane 1's review of #804 (finding 1): `*[[:cntrl:]]*` matches a C1 control only
in a value that is valid UTF-8. One byte that is not (a lone 0xE9) makes bash
match byte by byte, and then U+009B's two bytes (0xC2 0x9B) are not controls,
as under `C`. Measured on 7569ffa with bash 5.2.21: under `LC_ALL=C.UTF-8`
and `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/caf`, 0xE9, U+009B, `31mFORGED/bin`,
`openRepoShape --install` exited 0, placed the command and printed the value
raw in four lines; install.sh, given the same value after a `:`, exited 2 with
the colon refusal naming it, the C1 control in the line.

Both files now run the check as `(LC_ALL=C; case ... *[[:cntrl:]]* |
*$'\302'[$'\200'-$'\237']* ...)`: the ASCII controls and DEL byte by byte, and
the two bytes of each C1 control named, so the answer is the same in every
locale. Every value measured on 7569ffa and refused only under C.UTF-8, or
not at all, is refused with exit 2, nothing placed, nothing echoed, and
install.sh makes no request. `café` and U+00A0 are still placed. U+2028 and
U+2029, which only a UTF-8 locale's `[[:cntrl:]]` matched, are now placed in
every locale, and a test says so. Both comments say what the check does, and
no longer say the C1 controls are the locale's.

Tests: the C1 case, which skipped where no UTF-8 locale read U+009B as a
control, is now five values (U+009B, the first and the last of the range, a
0xE9 byte before U+009B, and a colon beside that) in each of `C` and
`C.UTF-8`, through the command and through install.sh. The placed-letter test
gains U+00A0 (the byte after the range's last), U+2028 and U+2029. 18 of the
36 fail on 7569ffa's code and the other 18 are guards; 18 mutants of the two
code hunks are caught.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	install.sh
#	openRepoShape
#	tests/test_install_sh.py
#	tests/test_openreposhape_command.py
…holding a bidi control, a character of no width, a soft hyphen or U+2028 before any request and name it by its code point, so no line about `$PATH` shows the directory as another (#837)

#804's check refuses the C0 and C1 controls and DEL, and passed every
character a terminal draws as something other than itself, under `C` and
`C.UTF-8` alike: the command placed itself under `<dir>/a` U+202E
`forged/bin` and printed the value in four lines, and install.sh made its
request. Laid out as a terminal lays out a line (fribidi), `~/` U+202E
`.local/bin` reads `~/nib/lacol.`, `/opt/tools-1` U+200F `/2/bin` reads
`/opt/tools-12//bin`, and `~/.local` U+200B `/bin` reads `~/.local/bin`.

Both files now refuse, right after the control check, before the drive
spelling, the colon, the symlink check and any request, a directory
holding one of 29 code points, `MISDRAWN_CHARACTERS` in the launcher:
U+00AD; the twelve bidirectional controls (U+061C, U+200E, U+200F,
U+202A to U+202E, U+2066 to U+2069), as `repo_shape.spelled` escapes
them (#391); U+2028 and U+2029, which end a line for every reader that
keeps Unicode's line breaks, and which #804 left to a ruling; and U+200B,
U+FEFF and U+2060 to U+206F, of no width, the whole block
default-ignorable. U+200C and U+200D stay allowed: Persian, the Indic
scripts and emoji sequences need them. So does every space.

`misdrawn_characters` builds each one's UTF-8 from its code point with
printf's octal escapes and looks for the bytes under `LC_ALL=C`, in the
subshell #804's check uses, so one list says what is refused and what
the refusal names, and no byte is read back as a number. The refusal
says "holds a character a terminal draws as something other than itself
(U+202E): one that is invisible, ends a line or reorders the text around
it, so no line about $PATH can print the name as it reads", names every
one found, and nothing else of the value. install.sh carries the list and
the function as the launcher has them, and a test holds the two to one
text.

Tests: #804's C1 locale test in each file now runs `MISDRAWN_BIN_DIRS`
too (each code point, two at once, after a byte that is not UTF-8, beside
a colon, in a drive spelling), under `C` and `C.UTF-8`, through the
command and install.sh, with no request; the link test takes U+202E; the
lifted function names exactly its 29 over every code point that shares a
first byte with them (896, on stdin, so it runs under Git Bash);
`NOT_CONTROLS` drops U+2028 and U+2029 and gains a Persian word with
U+200C, an emoji sequence with U+200D, U+2026 and U+FF21.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR extends both the launcher and installer with an identical, Bash-compatible UTF-8 byte detector for 41 Unicode characters that terminals may render invisibly, misleadingly, or as line breaks, refusing affected directories and repository/ref-related inputs before requests or output can expose them. Tests comprehensively verify code-point coverage, supplementary-plane encoding, refusal ordering, locale independence, no-echo and no-side-effect guarantees, allowed Unicode exceptions, and installer/launcher parity.

Sequence diagram for preflight refusal of misleading Unicode input

sequenceDiagram
    participant User
    participant EntryPoint
    participant Detector as misdrawn_characters
    participant Network
    participant Filesystem

    User->>EntryPoint: invoke installer or launcher
    EntryPoint->>Detector: inspect repository, ref, option, or bin directory
    Detector-->>EntryPoint: matching code points or empty result
    alt misdrawn character found
        EntryPoint-->>User: refuse with U+XXXX code point
        Note over EntryPoint,User: no value echo, request, checkout, placement, or PATH output
    else input is safe
        EntryPoint->>Network: request release or repository data
        EntryPoint->>Filesystem: checkout or install command
    end
Loading

Flow diagram for Unicode-safe installation preflight

flowchart TD
    Start["Receive installation input"] --> Control["Check control characters"]
    Control -->|control found| ControlRefuse["Refuse as control character"]
    Control -->|no control| Misdrawn["Run misdrawn_characters under LC_ALL=C"]
    Misdrawn -->|character found| MisdrawnRefuse["Refuse and report U+XXXX only"]
    Misdrawn -->|none found| Shape["Run existing value and path validation"]
    Shape -->|valid| Proceed["Make request or install"]
    Shape -->|invalid| ShapeRefuse["Refuse before naming the value"]
Loading

File-Level Changes

Change Details Files
Add locale-independent detection and refusal of terminal-misleading Unicode characters before values can be requested, processed, or echoed.
  • Define a 41-code-point misdrawn-character set, including supplementary-plane characters, and encode candidates to UTF-8 using Bash-compatible arithmetic and printf.
  • Check binary directories in both installation paths before path spelling, symlink handling, placement, or network requests.
  • Check repositories, refs, options, release tags, and redirect locations before validation messages can echo them or downstream operations use them.
  • Preserve control-character precedence and allow script-required joiners, non-joiners, tags, variation selectors, and other explicitly excluded format characters.
  • Use code-point-only refusal messages that avoid printing the offending value.
install.sh
openRepoShape
Expand regression coverage for refusal behavior, detection correctness, ordering, locale independence, and parity between the installer and launcher.
  • Cover all refused code points, multiple simultaneous characters, malformed UTF-8 adjacency, colon and Windows path cases, and both C and C.UTF-8 locales.
  • Verify no request, checkout, placement, symlink processing, or value echo occurs after refusal.
  • Test repository, ref, option, release-tag, and redirect-location inputs through both entry points.
  • Lift and execute the shell detector across broad BMP and supplementary-plane samples to ensure it names exactly the intended characters.
  • Assert the installer’s copied detector remains byte-for-byte identical to the launcher’s implementation.
tests/test_install_sh.py
tests/test_openreposhape_command.py

Assessment against linked issues

Issue Objective Addressed Explanation
#837 Refuse a bin directory containing a terminal-misleading character—including the specified bidi controls, line separators, U+200B, and U+FEFF—before making requests or printing any line that names the directory. ✅
#837 Perform the check consistently across locales and both installation entry points, identify refused characters without echoing the directory value, and continue to allow U+200C and U+200D. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-17T19:16:51Z) Independent review of #841 (#837) at cc5ae88e, for lane openRepoShape-2: READY

I reviewed the one commit after the stacked merge: git diff 8a559cb5 cc5ae88e. Nothing I found blocks the merge. There are two SHOULDs and four NITs.

Findings

  1. SHOULD: the set leaves out U+FFF9, U+FFFA and U+FFFB, which meet the PR's own reason for U+200B. At openRepoShape:473 and install.sh:346.

    • These are the interlinear annotation characters. They are format characters (Cf), and no script or emoji uses them. glibc's wcwidth gives each of them 0, as it does U+200B.
    • The PR's table says nothing about them.
    • Repro, with LC_ALL=C.UTF-8 and OPENREPOSHAPE_BIN_DIR=<H>/.local + U+FFF9 + /bin: openRepoShape --install exits 0 and places the command. Four lines then print the character: installed at <H>/.local\ufff9/bin/openRepoShape, 1 of 1 placed in …, the PATH note, and export PATH="<H>/.local\ufff9/bin:$PATH". U+FFFB behaves the same.
    • Fix: add FFF9-FFFB to both lists, add 0xFFF9 to 0xFFFB to MISDRAWN, and add a line to the comment.
      • I measured the function as it stands with FFF9-FFFB added: it names U+FFF9, U+FFFA and U+FFFB, and passes U+FFF8 and U+FFFC.
      • The lifted test already sweeps U+FE00 to U+FFFF, so it will hold the change.
    • The same rule takes in U+E0001 (LANGUAGE TAG, deprecated) and U+1D173 to U+1D17A (the musical format controls). Both have zero width and nothing needs them. The command places U+E0001 and U+1D173 today; I measured both. They need a four-byte branch in the encoder, so a follow-up is fine.
  2. SHOULD: file a follow-up before this lands, so Closes #837 does not close the only record of the repository and ref echo.

  3. NIT: the refusal's explanation is not true of U+00AD. At openRepoShape:547 and install.sh:437.

    • The refusal says "one that is invisible, ends a line or reorders the text around it". But glibc's wcwidth(U+00AD) is 1, and a terminal draws the soft hyphen as a hyphen, so ~/my + U+00AD + bin reads as ~/my-bin.
    • Refusing it is right, because it reads as another name. But someone told "invisible" will see a hyphen.
    • Fix: for example, "one that is invisible, is drawn as another character, ends a line or reorders the text around it", in both files and in drawn_as_another.
  4. NIT: some reasons for what is left out are inexact, or appear only in the PR body. At openRepoShape:446-472.

    • U+180E (MONGOLIAN VOWEL SEPARATOR) is a format character with width 0, not a mark that attaches to the character before it. Leaving it out is right on the same ground as U+200C (Mongolian needs it), and the comment should give that reason.
    • U+1160 (HANGUL JUNGSEONG FILLER) has wcwidth 0, so it is not "drawn as what it is". No precomposed syllable decomposes to it (I checked U+AC00 to U+D7A3), so it could join the list at no cost. U+115F, U+3164 and U+FFA0 are drawn as blank cells (widths 2, 2 and 1), like the spaces the PR allows.
    • The encoder only builds the two- and three-byte forms (U+0080 to U+FFFF, openRepoShape:488-493), so a four-byte entry would be encoded wrong. Say so beside the list.
    • The code comment names only U+200C, U+200D and the spaces as left out. The reasons for leaving out the variation selectors, tags, U+180E, U+034F and the fillers are only in the PR body.
  5. NIT: no test pins the order of the two checks.

  6. NIT: four identical lines of link setup appear in two test files. At tests/test_install_sh.py:917-920 and tests/test_openreposhape_command.py:905-908.

Checked and true

Could not verify

  • bash 3.2 (macOS), and Git Bash on Windows.
  • A real terminal emulator. I relied on glibc's wcwidth and on fribidi.
  • The LC_ALL=C survivors in a non-UTF-8 multibyte locale. The EUC-JP and GB18030 locales I built by hand decode correctly in glibc, but bash 5.2.21 hangs at startup under them.
  • Whether SonarCloud's duplication gate counts shell. The repository has no Sonar settings, and the Sonar server could not be reached from here.

Lane: openreposhape-2 (openRepoShape-2)

…tion and the redirect's location are refused unechoed for a misdrawn character too, the set takes in U+FFF9 to U+FFFB, U+1D173 to U+1D17A and U+E0001, and the refusal says a character may be drawn as another (#837)

Fix round 1 on rv841's review of #841.

The set (SHOULD 1). `MISDRAWN_CHARACTERS` gains FFF9-FFFB (the
interlinear annotation characters), 1D173-1D17A (the musical beam, tie,
slur and phrase controls) and E0001 (the deprecated language tag): format
characters of no width (glibc `wcwidth` 0) that nothing needs, which the
command placed and printed in four lines. The encoder builds the
four-byte form too, with bash 3.2's arithmetic and `%b`'s `\0NNN`. Every
other format character is left out for a reason the comment now gives:
U+200C, U+200D and U+180E, which Persian, the Indic scripts, emoji and
Mongolian need; the Egyptian hieroglyph and shorthand format controls,
with which those scripts build their signs; the tags, the variation
selectors, U+034F and U+1160, drawn as part of the character before them;
the signs drawn over the digits after them, which take a cell; and every
space and the blank Hangul fillers.

The repository and the ref (SHOULD 2). #837's line named the
`[![:print:]]` checks of both files: a UTF-8 locale's U+202E, U+200B,
U+FEFF or U+00AD passes them, and the refusal after them echoed the value.
Each now asks `misdrawn_characters` first, and refuses by the code point,
saying nothing else of the value: `checked_repo`, `checked_version` (the
version named and the newest release's tag), the option check and
`latest_from_redirect`'s location in install.sh; `shape_checkout` and
`ref_fault` (`$OPENREPOSHAPE_REF` and the record) in the command. First,
because `[![:print:]]` is the locale's, so the words do not change with it.

The words (NIT 3): "one that is invisible, is drawn as another character,
ends a line or reorders the text around it", because U+00AD is drawn as a
hyphen. The order (NIT 5): a tab before U+202E is refused as a control
character, and a row holds it.

Tests: `MISDRAWN` and the locale table take the twelve new code points and
the tab row; the lifted sweep takes U+1D100 to U+1D1FF, U+E0000 to U+E007F
and a sample of the planes past U+FFFF; `NOT_CONTROLS` takes a subdivision
flag (tags U+E0020 to U+E007F); a test of each file refuses a value holding
U+202E at every new site under `C` and `C.UTF-8`, through both entry
points, with no request, nothing placed and neither stream holding it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-17T20:42:17Z) Fix round 1 on #841 at 4123b9e5, on rv841's review

  1. SHOULD (set): FFF9-FFFB, 1D173-1D17A and E0001 are in both lists and in MISDRAWN (41 code points). The encoder builds the four-byte form with bash 3.2 arithmetic and %b's \0NNN. The comment says which forms it builds, and why each other format character stays out: the Egyptian hieroglyph and shorthand format controls (scripts need them), the tags, the variation selectors, the signs drawn over digits.
  2. SHOULD (repository and ref): closed here. Every [![:print:]] check in both files now asks misdrawn_characters first and refuses by code point, unechoed: install.sh's checked_repo, checked_version (a version and the newest tag), the option and the redirect location; the command's shape_checkout and ref_fault (the variable and the record). Tested under C and C.UTF-8 through both entry points: no request, nothing placed, neither stream holds the value.
  3. NIT (words): every refusal now says "one that is invisible, is drawn as another character, ends a line or reorders the text around it".
  4. NIT (reasons): U+180E (Mongolian needs it); U+1160 kept out, because it forms a syllable block with the consonant before it; U+115F, U+3164 and U+FFA0 drawn as blank cells; the variation selectors, tags and U+034F explained in the comment.
  5. NIT (order): a REFUSED_IN_EVERY_LOCALE row holds a tab and U+202E and expects HOLDS_A_CONTROL_CHARACTER. Your mutant E fails in both files.
  6. NIT (link setup): left. The four lines are under Sonar's 10-line threshold and came from install.sh and openRepoShape: both refuse a $OPENREPOSHAPE_BIN_DIR holding a control character before they name it, and install.sh refuses a : before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) #804.
    Tests at 4123b9e: the four Verify files 549 passed, and 549 on the merge with fc86720 (tree dc9918e5, never pushed); with the three common files 696; test_shape_doctor.py and three others 549 passed, 3 skipped, 1 xfailed; the three Git Bash files with CRLF write_text 491. The new and changed cases give 260 passed here and 199 failed over cc5ae88's shell files. 32 new mutants, all killed. The function swept over 81,317 names has 0 mismatches under C and C.UTF-8.

Lane: openreposhape-2 (openRepoShape-2)

@brettheap brettheap changed the title install.sh and openRepoShape: both refuse a $OPENREPOSHAPE_BIN_DIR holding a bidi control, a character of no width, a soft hyphen or U+2028 before any request and name it by its code point, so no line about $PATH shows the directory as another install.sh and openRepoShape: a $OPENREPOSHAPE_BIN_DIR, the repository or a ref holding a bidi control, a character of no width, a soft hyphen or U+2028 is refused by its code point before any request and never echoed, so no line shows the value as another Oct 17, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-17T21:03:57Z) Re-verification of #841 (#837) at 4123b9e5, for lane openRepoShape-2: READY

I reviewed git diff cc5ae88e 4123b9e5: one plain commit, with nothing new merged. All six findings from my first review are fixed or properly answered, and I found nothing new that blocks the merge.

Findings from the first review

  1. SHOULD 1 (the set): fixed.
    • U+FFF9 to U+FFFB, U+1D173 to U+1D17A and U+E0001 are in both lists and in MISDRAWN, which makes 41 code points.
    • The new four-byte branch is right. Its lead byte is 0xF0 | (code >> 18) and it adds three continuation bytes.
      • It uses only elif, [ -lt ], arithmetic shifts and masks, and %b with \0NNN. Every one of those exists in bash 3.2.
      • Every value it builds fits three octal digits.
    • I swept the function, lifted from each file, against Python's own decoding. That was 9,690 names, 6,648 of them past U+FFFF, including the whole of U+1D100 to U+1D1FF and U+E0000 to U+E007F, the Egyptian, shorthand and Kaithi format characters, and 11 crafted byte strings. There were 0 mismatches, under C and C.UTF-8.
    • The crafted strings included U+E0001's bytes split by an ASCII byte, an invalid byte before U+E0001, bytes past U+10FFFF, and a subdivision flag.
  2. SHOULD 2 (the repository and ref echo): fixed in this PR.
    • Every [![:print:]] site in both files now runs misdrawn_characters first:
      • install.sh: checked_repo :133, checked_version :161, the redirect location :216 and the option check :417;
      • the launcher: shape_checkout :725 and ref_fault :777. ref_fault also covers the record and record_ref's old value.
    • Each new check comes before any request that sends the value and before any line that prints it. That is so under C and C.UTF-8.
      • In install.sh, the newest tag reaches checked_version before say "release: …".
      • In the launcher, ref_fault runs at top level, but misdrawn_characters is defined above it.
    • I probed install.sh with U+E0001 and U+FFF9 in $OPENREPOSHAPE_REPO, U+1D173 in a version, and an escape plus U+FFF9 in the repository. Each was refused by its code point under both locales, nothing was echoed, and the stub curl logged 0 calls.
  3. NIT 3 (the words): fixed. Every refusal now says "is drawn as another character", which covers U+00AD's hyphen.
  4. NIT 4 (the reasons): fixed.
    • The comment now gives a reason for each character left out. I checked its width claims with glibc's wcwidth: the signs over digits are 1, and the Egyptian, shorthand, tag and selector characters are 0.
    • U+1160 stays out. The reason given holds where the character is used. After any other character it is invisible (width 0), as U+200C and U+180E are, and the set already allows that for a script's sake. Accepted.
  5. NIT 5 (the order): fixed. The a-tab-then-u+202e row kills my old mutant E in both files.
  6. NIT 6 (the link setup): left as it was. Accepted. The four lines are under Sonar's 10-line threshold, and they came from install.sh and openRepoShape: both refuse a $OPENREPOSHAPE_BIN_DIR holding a control character before they name it, and install.sh refuses a : before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) #804.

New

  • NIT: the PR title names only $OPENREPOSHAPE_BIN_DIR. (Coordinator: the title was updated at 20:42Z to name the repository and a ref too.) This round also refuses a repository, a version or ref, an option and the redirect's location. The squash message at landing should say so; the head commit's message already does.

Ruling on the gaps the writer left alone: a follow-up issue, not #841.

Checked and true

Could not verify

  • bash 3.2 (macOS), Git Bash on Windows, and any real terminal emulator.
  • The LC_ALL=C survivors in a multibyte locale that is not UTF-8 (unchanged from my first review).
  • Whether SonarCloud's duplication gate counts shell.

Lane: openreposhape-2 (openRepoShape-2)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 01:17
@brettheap
brettheap merged commit ca47b71 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/837-bin-dir-format-chars branch October 18, 2026 01:17

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="install.sh" line_range="369" />
<code_context>
+# `main` asks it of the directory, of an option, `checked_repo` of the
+# repository, `checked_version` of a version and `latest_from_redirect`
+# of the location it reads, each before any line prints the value.
+MISDRAWN_CHARACTERS=(00AD 061C 200B 200E-200F 2028-202E 2060-206F FEFF FFF9-FFFB 1D173-1D17A E0001)
+
+misdrawn_characters() {
</code_context>
<issue_to_address>
**Stray emoji tags remain invisible**

When a binary directory contains emoji tag characters outside a valid subdivision-flag sequence, the byte check excludes the emoji tag characters everywhere, not just inside a valid subdivision-flag sequence. A directory name with tag characters inserted between ordinary letters is therefore accepted even though terminals that ignore those tags display a different name, so the PATH note can still identify the wrong directory.

Recognize and allow tags only as part of a valid subdivision-flag sequence, and refuse tag characters elsewhere.
</issue_to_address>

### Comment 2
<location path="install.sh" line_range="390" />
<code_context>
+			fi
+			printf -v bytes '%b' "$bytes"
+			case "$value" in
+			*"$bytes"*)
+				printf -v name 'U+%04X' "$code"
+				found="${found:+$found, }$name"
</code_context>
<issue_to_address>
**Valid legacy-encoding names are refused**

When the directory name is encoded in a multibyte non-UTF-8 locale and its bytes overlap a listed character’s UTF-8 encoding, `misdrawn_characters` matches UTF-8 byte sequences without decoding the input, so a valid character in a multibyte non-UTF-8 locale whose encoding overlaps one of those sequences is reported as a forbidden code point and the installer refuses that directory. The launcher carries the same byte-matching logic, so it refuses the same names.

Decode and classify the value according to an explicitly supported input encoding, or limit this byte-sequence refusal to UTF-8 inputs and document the behavior for other encodings.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: install.sh:369, install.sh:390


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread install.sh
# `main` asks it of the directory, of an option, `checked_repo` of the
# repository, `checked_version` of a version and `latest_from_redirect`
# of the location it reads, each before any line prints the value.
MISDRAWN_CHARACTERS=(00AD 061C 200B 200E-200F 2028-202E 2060-206F FEFF FFF9-FFFB 1D173-1D17A E0001)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Stray emoji tags remain invisible

When a binary directory contains emoji tag characters outside a valid subdivision-flag sequence, the byte check excludes the emoji tag characters everywhere, not just inside a valid subdivision-flag sequence. A directory name with tag characters inserted between ordinary letters is therefore accepted even though terminals that ignore those tags display a different name, so the PATH note can still identify the wrong directory.

Recognize and allow tags only as part of a valid subdivision-flag sequence, and refuse tag characters elsewhere.

Prompt for AI agents
In `install.sh` at line 369:

**Stray emoji tags remain invisible**

When a binary directory contains emoji tag characters outside a valid subdivision-flag sequence, the byte check excludes the emoji tag characters everywhere, not just inside a valid subdivision-flag sequence. A directory name with tag characters inserted between ordinary letters is therefore accepted even though terminals that ignore those tags display a different name, so the PATH note can still identify the wrong directory.

Recognize and allow tags only as part of a valid subdivision-flag sequence, and refuse tag characters elsewhere.

Comment thread install.sh
fi
printf -v bytes '%b' "$bytes"
case "$value" in
*"$bytes"*)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Valid legacy-encoding names are refused

When the directory name is encoded in a multibyte non-UTF-8 locale and its bytes overlap a listed character’s UTF-8 encoding, misdrawn_characters matches UTF-8 byte sequences without decoding the input, so a valid character in a multibyte non-UTF-8 locale whose encoding overlaps one of those sequences is reported as a forbidden code point and the installer refuses that directory. The launcher carries the same byte-matching logic, so it refuses the same names.

Decode and classify the value according to an explicitly supported input encoding, or limit this byte-sequence refusal to UTF-8 inputs and document the behavior for other encodings.

Prompt for AI agents
In `install.sh` at line 390:

**Valid legacy-encoding names are refused**

When the directory name is encoded in a multibyte non-UTF-8 locale and its bytes overlap a listed character’s UTF-8 encoding, `misdrawn_characters` matches UTF-8 byte sequences without decoding the input, so a valid character in a multibyte non-UTF-8 locale whose encoding overlaps one of those sequences is reported as a forbidden code point and the installer refuses that directory. The launcher carries the same byte-matching logic, so it refuses the same names.

Decode and classify the value according to an explicitly supported input encoding, or limit this byte-sequence refusal to UTF-8 inputs and document the behavior for other encodings.

brettheap pushed a commit that referenced this pull request Oct 18, 2026
…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715)

Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path:

- `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept.

`scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…cked once, before `--version` answers and before any fetch, so a control character or a character a terminal draws as another in either is refused, never printed or sent (#866) (#872)

`--version` printed both values as typed and exited 0. A scaffold run sent
its fetch with them, and so did `--install` from stdin, and each named
them three times in its refusal. So an escape, a C1 control, U+202E or
U+200B in either value reached the terminal as it was typed.

One function, `refuse_bad_values`, now runs once the arguments are read,
and inside the `--version` case, which answers within the argument loop. It
refuses the broken record first, as before. Then it checks
`$OPENREPOSHAPE_REPO` with `checked_repo`, `shape_checkout`'s guard moved
whole: #841's misdrawn characters, then a leading `-` or `[![:print:]]` on
the clone URL. Then it checks `$OPENREPOSHAPE_REF` with
`ref_character_fault`, `ref_fault`'s misdrawn and control-character checks,
moved whole. `ref_fault` calls them in the same place. Every refusal keeps
the words it had.

The misdrawn characters are looked for in `$REPO` as typed, and the
control characters in it and in the clone URL, because `$(shape_clone_url)`
drops a trailing newline: a URL, an scp address or a path ending in one
passed, was printed by `--version` and was sent by a scaffold run and by
`--install` from stdin (rv866's SHOULD-1). The leading `-` is still looked
for in the URL's first character, so an owner/name such as `-acme/r`
passes as before. A test holds the order the checks state: the record,
then the repository, then the ref.

The ref's length and shape stay where a ref is used: `checked_ref` when
`--install` records, `shape_checkout` before `git checkout`, and setup.sh's
`--shape-ref` checks. So `--version` still prints any printable ref in
force, and an install that records no ref still takes one that names
nothing, as workBenches sets it.

`--version` refuses with exit 2, as a broken record already makes it do,
and does not respell the value. install.sh's `shown=` parse is unchanged
for every value it can hand the command: an owner/name that its own
`checked_repo` passed, and the tag that its `checked_version` passed. Both
pass this check, and `--version` prints them byte for byte.

`--help` still answers first. It names the variables, never their values.

Closes #866

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant