Skip to content
Merged
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
96 changes: 86 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
# command already there as a symbolic link (pipx's, #563), a
# `$OPENREPOSHAPE_BIN_DIR` spelled for Windows where no `cygpath` can
# spell it the way bash does (#667), or a directory holding a control
# character or a `:`, which no line about `$PATH` can name (#760).
# character or a `:`, which no line about `$PATH` can name (#760), or
# a character a terminal draws as something other than itself (#837).
# git-filter-repo never changes it: only adopting needs it, and
# `adopt-project.py` refuses with its own install command where it is missing.
# What it changes is the summary (#595): `adoption:` says whether it is there
Expand Down Expand Up @@ -122,8 +123,16 @@ USAGE
# THE REPOSITORY, AS AN OWNER/NAME, because every URL below is built from it.
# The launcher also takes a URL or a path in this variable, for the clone
# setup.sh makes; there is no release at either, so this refuses them by
# name. A control character is refused before anything is echoed.
# name. A control character is refused before anything is echoed, and
# before it a character a terminal draws as something other than itself
# (#837), named by its code point: first, because `[![:print:]]` is the
# locale's, matching every byte past ASCII under `C` and none of those
# characters under a UTF-8 locale, where the refusal below would echo them.
checked_repo() {
local misdrawn
misdrawn="$(LC_ALL=C; misdrawn_characters "$REPO")"
[ -z "$misdrawn" ] ||
die "\$OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself ($misdrawn): 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."
case "$REPO" in
*[![:print:]]*) die "\$OPENREPOSHAPE_REPO carries a control character; it must be an owner/name on github.com, such as opensoft/openRepoShape." ;;
esac
Expand All @@ -139,14 +148,19 @@ checked_repo() {
# THE LAUNCHER'S `checked_ref`, TRANSCRIBED: the version reaches a URL here
# and `git checkout` once the command runs, so it is held to the same three
# checks in the same order (length, then a control character, then the
# shape, which is the only one that echoes it), plus the empty value. Not
# shared, because this file arrives alone down a pipe.
# shape, which is the only one that echoes it), plus the empty value, and,
# between the length and the control character, a character a terminal
# draws as something other than itself, as the launcher's `ref_fault` has
# it (#837). Not shared, because this file arrives alone down a pipe.
checked_version() {
local ref="$1" what="$2"
local ref="$1" what="$2" misdrawn
[ -n "$ref" ] || die "$what is empty. Name a release, such as v1.0.1, or none for the newest."
if [ "${#ref}" -gt 255 ]; then
die "$what is ${#ref} characters long; a release tag, branch or commit is at most 255 characters."
fi
misdrawn="$(LC_ALL=C; misdrawn_characters "$ref")"
[ -z "$misdrawn" ] ||
die "$what holds a character a terminal draws as something other than itself ($misdrawn): 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."
case "$ref" in
*[![:print:]]*) die "$what carries a control character, which is not a release tag, branch or commit." ;;
esac
Expand Down Expand Up @@ -195,10 +209,16 @@ latest_from_redirect() {
return 0
;;
esac
case "$value" in
*[![:print:]]*) FAULT="it redirects to a location that names no release" ;;
*) FAULT="it redirects to $value, which names no release" ;;
esac
# The location is named only when it is text a terminal draws as
# it is: no control character, and no character it draws as
# something other than itself (#837).
FAULT="it redirects to a location that names no release"
if [ -z "$(LC_ALL=C; misdrawn_characters "$value")" ]; then
case "$value" in
*[![:print:]]*) ;;
*) FAULT="it redirects to $value, which names no release" ;;
esac
fi
return 1
;;
esac
Expand Down Expand Up @@ -336,8 +356,52 @@ filter_repo_step() {
adoption_not_ready "it is not installed" "$how $found"
}

# THE CHARACTERS A TERMINAL DRAWS AS SOMETHING OTHER THAN THEMSELVES, AND
# WHICH OF THEM "$1" HOLDS (#837): the launcher's `MISDRAWN_CHARACTERS` and
# `misdrawn_characters`, copied as they are, for the reason the control
# check in `main` is copied: this file arrives alone down a pipe. The
# launcher says why these characters and no others, and which forms of
# UTF-8 the function builds (two, three and four bytes), and
# `tests/test_openreposhape_command.py` holds the two copies to one text.
# `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.


misdrawn_characters() {
local value="$1" entry code last bytes name found=""
for entry in "${MISDRAWN_CHARACTERS[@]}"; do
code=$((0x${entry%-*}))
last=$((0x${entry#*-}))
while [ "$code" -le "$last" ]; do
if [ "$code" -lt 2048 ]; then
printf -v bytes '\\0%o\\0%o' $((0xC0 | (code >> 6))) \
$((0x80 | (code & 63)))
elif [ "$code" -lt 65536 ]; then
printf -v bytes '\\0%o\\0%o\\0%o' $((0xE0 | (code >> 12))) \
$((0x80 | ((code >> 6) & 63))) $((0x80 | (code & 63)))
else
printf -v bytes '\\0%o\\0%o\\0%o\\0%o' \
$((0xF0 | (code >> 18))) $((0x80 | ((code >> 12) & 63))) \
$((0x80 | ((code >> 6) & 63))) $((0x80 | (code & 63)))
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.

printf -v name 'U+%04X' "$code"
found="${found:+$found, }$name"
;;
*) ;;
esac
code=$((code + 1))
done
done
printf '%s' "$found"
}

main() {
local version="" asked launcher_url bin_dir spelled target shown status=0 pinned=1
local misdrawn
local machine="this machine is ready"

# NOTHING A CHILD READS IS THIS SCRIPT. Piped, stdin is the pipe the
Expand All @@ -350,6 +414,9 @@ main() {
-h | --help) usage; exit 0 ;;
--) shift; continue ;;
-*)
misdrawn="$(LC_ALL=C; misdrawn_characters "$1")"
[ -z "$misdrawn" ] ||
die "an option holds a character a terminal draws as something other than itself ($misdrawn): 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."
case "$1" in
*[![:print:]]*) die "an option carries a control character. The one argument is a version, such as v1.0.1; with none, the newest release." ;;
esac
Expand Down Expand Up @@ -379,7 +446,8 @@ main() {
# print (`café` under LC_ALL=C) is no fault of the directory's. The check
# runs under `LC_ALL=C` and names U+0080 to U+009F's two bytes (`\302`,
# then `\200` to `\237`), so a C1 control is refused in every locale,
# whatever else the value holds; U+2028 and U+2029 are refused in none.
# whatever else the value holds; U+2028 and U+2029, which only a UTF-8
# locale's `[[:cntrl:]]` matched, are the next check's (#837).
# Left to the locale, `[[: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 are not controls, as
Expand All @@ -389,6 +457,14 @@ main() {
if (LC_ALL=C; case "$bin_dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; *) exit 1 ;; esac); then
die "the directory to install into holds a control character, such as a newline, a carriage return, a tab or an escape, and no line about \$PATH can print such a name as it is: a terminal acts on it. Nothing was installed. Set \$OPENREPOSHAPE_BIN_DIR to a directory without one, then run this line again."
fi
# A CHARACTER A TERMINAL DRAWS AS SOMETHING OTHER THAN ITSELF IS REFUSED
# NEXT (#837), as the command refuses it, and before any request: the
# name would read as another directory's in every line below. The
# refusal names each one by its code point, because nobody can see it,
# and says nothing else of the value.
misdrawn="$(LC_ALL=C; misdrawn_characters "$bin_dir")"
[ -z "$misdrawn" ] ||
die "the directory to install into holds a character a terminal draws as something other than itself ($misdrawn): 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."
# A WINDOWS SPELLING IS SPELLED THE WAY BASH SPELLS IT, THE COMMAND'S OWN
# RULE (#667, after #649), before it is named in a line or looked at: a
# value typed `D:\tools\bin` was echoed as typed, in the `install:`,
Expand Down
Loading
Loading