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
43 changes: 40 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@
# it): the lines above say which;
# 2 refused before anything was installed: a version that is not a ref, no
# release found, a fetch that failed, a file that is not the command, a
# command already there as a symbolic link (pipx's, #563), or a
# 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).
# spell it the way bash does (#667), or a directory holding a control
# character or a `:`, which no line about `$PATH` can name (#760).
# 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 @@ -365,6 +366,29 @@
die "curl is not installed, and this installer fetches with it. Install curl, or install by hand with: gh api repos/$REPO/contents/openRepoShape -H 'Accept: application/vnd.github.raw' | bash -s -- --install"

bin_dir="${OPENREPOSHAPE_BIN_DIR:-$HOME/.local/bin}"
# A DIRECTORY HOLDING A CONTROL CHARACTER IS REFUSED FIRST (#760), before
# any line below names it: a newline breaks a line in two, a carriage
# return rewrites it on the terminal and an escape is the terminal's to
# act on, and the `install:`, `preflight:` and `done:` lines would print
# the directory as typed. The command refuses the same value, but only
# from the release that added the check, and only after this file had
# fetched it. The `case` is `install_commands`'s in `openRepoShape`,
# copied for the reason the drive-letter one below is, and the refusal
# says nothing of the value. `[[:cntrl:]]` and not `[![:print:]]`, which
# `checked_repo` uses: a directory with a letter the locale does not
# 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.
# 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
# under `C` (measured with bash 5.2.21). The locale is `C` in a subshell,
# so that the match is byte by byte whatever a libc says of a byte in a
# UTF-8 locale, and the locale of every line below is as it was.
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 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 All @@ -375,7 +399,7 @@
# anything is asked of the network. The `case` is `install_commands`'s in
# `openRepoShape`, copied: this file arrives alone down a pipe, and
# `tests/test_install_sh.py` feeds one value through both. The refusal
# does not echo the value: it may hold a newline that forges a line.
# does not echo the value.
case "$bin_dir" in
[A-Za-z]:/* | [A-Za-z]:\\*)
spelled=""
Expand All @@ -390,6 +414,19 @@
bin_dir="$spelled"
;;
esac
# A `:` SPLITS A `$PATH` WHATEVER THE QUOTING (#760), so no line about
# `$PATH` can name such a directory; the command refuses it from the
# release that added the check (#667), and this file learnt of it only
# after the fetch. Checked after the Windows spelling, as the command
# checks it: a drive spelling holds a `:` by definition, is respelled
# without one, and was refused above where it could not be. The
# control-character arm is above, so the directory is named here, as the
# command names it, and no value that can forge a line gets this far.
case "$bin_dir" in

Check failure on line 425 in install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaFJe6V3uvmeKE72Lkpv&open=AaFJe6V3uvmeKE72Lkpv&pullRequest=804
*:*)
die "install.sh would place openRepoShape in '$bin_dir', which holds a ':', and a ':' is what splits a \$PATH into its directories, so no line about \$PATH can name it. Nothing was installed. Set \$OPENREPOSHAPE_BIN_DIR to a directory without one, then run this line again."
;;
esac

# A LINK IS NOT A PLACE TO INSTALL TO (#563), and this installer says so
# itself, before anything is asked of the network. pipx places the
Expand Down
46 changes: 30 additions & 16 deletions openRepoShape
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,10 @@ record_ref() {
# nothing is read but the quote itself, which is written `'\''`. Every other
# directory keeps the line this command has always printed, byte for byte:
# README says `export PATH=…`, and a space or an apostrophe is safe in
# double quotes. A `:` splits a `$PATH` whatever the quoting, and a newline
# breaks the printed line in two, so `install_commands` refuses both before
# this is asked.
# double quotes. A `:` splits a `$PATH` whatever the quoting, and a control
# character (a newline breaks the printed line in two, an escape is the
# terminal's to act on) is no name a line can print as it is, so
# `install_commands` refuses both before this is asked.
export_path_line() {
local dir="$1" backslash='\' quote="'" rest quoted=""
case "$dir" in
Expand Down Expand Up @@ -444,19 +445,32 @@ export_path_line() {
install_commands() {
local dir="${OPENREPOSHAPE_BIN_DIR:-$HOME/.local/bin}"
local name target source verb placed=0 recording=0 record spelled=""
# NO LINE ABOUT `$PATH` CAN NAME A DIRECTORY HOLDING A NEWLINE OR A `:`
# (#667): the colon is what splits a `$PATH`, and a newline breaks the
# line the note prints in two. Refused before anything is placed, as the
# Windows spelling below is. The newline goes first, and its refusal says
# nothing of the value, so that no refusal after it echoes one that can
# forge a line of output. The colon is checked after the Windows
# spelling, which holds one by definition, is respelled without one, and
# refuses what it cannot respell.
case "$dir" in
*$'\n'*)
die "--install would place this command in a directory whose name holds a newline, which no line about \$PATH can name: it would break in two. Nothing was placed. Set \$OPENREPOSHAPE_BIN_DIR to a directory without one, then run this again."
;;
esac
# NO LINE ABOUT `$PATH` CAN NAME A DIRECTORY HOLDING A CONTROL CHARACTER
# OR A `:` (#667, #760): the colon is what splits a `$PATH`; a newline
# breaks the line the note prints in two; a carriage return rewrites the
# line on the terminal (`/tmp/x^MFORGED is not on $PATH`) and an escape
# is the terminal's to act on, in the note and in every refusal that
# names the directory. Refused before anything is placed, as the Windows
# spelling below is. The control characters go first, and their refusal
# says nothing of the value, so that no refusal after it echoes one that
# can forge a line of output. It is `[[:cntrl:]]` and not
# `[![:print:]]`, which is what this file holds the repository and the
# ref to: a name with a letter the locale does not print, as `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. 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 under `C`
# (measured with bash 5.2.21). The locale is `C` in a subshell, so that
# the match is byte by byte whatever a libc says of a byte in a UTF-8
# locale, and the locale of every line below is as it was. The colon is
# checked after the Windows spelling, which holds one by definition, is
# respelled without one, and refuses what it cannot respell.
if (LC_ALL=C; case "$dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; *) exit 1 ;; esac); then
die "--install would place this command in a directory whose name 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 placed. Set \$OPENREPOSHAPE_BIN_DIR to a directory without one, then run this again."
fi
# A WINDOWS SPELLING IS SPELLED AGAIN THE WAY BASH SPELLS IT (#649), with
# `cygpath -u`, which Git Bash has, before anything is checked or placed:
# `D:\tools\bin` becomes `/d/tools/bin`. Kept as typed, every line below
Expand Down
134 changes: 132 additions & 2 deletions tests/test_install_sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@
import pytest

from conftest import BASH, BASH_SKIP, REPO, bash_path
from test_openreposhape_command import (BIN_DIR_SPELLINGS, COMMAND,
CYGPATH_STATES, INSTALL_NEEDS,
from test_openreposhape_command import (BIN_DIR_SPELLINGS, C1_BIN_DIRS,
COMMAND, COMMAND_REFUSAL_ENDS,
CYGPATH_STATES, HOLDS_A_COLON,
HOLDS_A_CONTROL_CHARACTER,
INSTALL_NEEDS, LOCALES,
NO_FILE_NAMED_SO,
NO_NON_ASCII_INTO_GIT_BASH,
NOT_CONTROLS, UNNAMEABLE_BIN_DIRS,
assert_a_bin_dir_is_refused,
command_env, machine_path,
put_cygpath, put_on_path, run_cmd,
typed_bin_dir, write_script)
Expand Down Expand Up @@ -839,6 +846,129 @@ def test_the_symlink_check_looks_at_the_directory_as_bash_spells_it(tmp_path):
assert packaged.read_bytes() == b"#!/usr/bin/env python3\n# the package's own\n"


# --- a directory no line about `$PATH` can name (#760) -----------------------

#: How install.sh's refusals of one end: the command's remedy, for this file.
INSTALLER_REFUSAL_ENDS = (
"Nothing was installed. Set $OPENREPOSHAPE_BIN_DIR to a directory "
"without one, then run this line again.")


@pytest.mark.parametrize("value, said", UNNAMEABLE_BIN_DIRS)
def test_the_installer_refuses_what_the_command_refuses_before_any_request(
tmp_path, value, said):
"""ONE `$OPENREPOSHAPE_BIN_DIR`, a `:` or a control character, through
install.sh and through the command it installs (#760).

The command refuses it with exit 2, but install.sh learnt of it only
after its fetch, said only "the <version> openRepoShape's --install
refused (exit 2)", and a release older than the refusal placed the
command under it and printed it as typed. So install.sh carries a copy
of both `case` arms, and the two must never answer one value
differently: each is refused with exit 2 and the remedy, nothing is
placed or made, and install.sh has asked the network nothing. A colon's
refusal names the directory; a control character's never does."""
net = Net(in_its_own_directory(tmp_path, "installer"))
command_home = in_its_own_directory(tmp_path, "command")
seen = {path.name for path in net.tmp_path.iterdir()}
typed_installer = value.format(d=bash_path(net.tmp_path))
typed_command = value.format(d=bash_path(command_home))

installer = net.run(env={"OPENREPOSHAPE_BIN_DIR": typed_installer})
command = run_cmd("--install", home=command_home, cwd=command_home,
env={"OPENREPOSHAPE_BIN_DIR": typed_command})

assert net.calls() == [], net.calls()
assert_a_bin_dir_is_refused(installer, typed_installer, said,
INSTALLER_REFUSAL_ENDS)
assert_a_bin_dir_is_refused(command, typed_command, said,
COMMAND_REFUSAL_ENDS)
net.refused(installer)
assert {path.name for path in net.tmp_path.iterdir()} - seen \
<= {"gh-config"}
assert list(command_home.iterdir()) == []


@NO_FILE_NAMED_SO
def test_the_installer_holds_the_default_directory_to_the_colon_refusal(
tmp_path):
"""No variable at all, and a `$HOME` that holds a `:`: the directory is
`$HOME/.local/bin`, and install.sh refuses it as the command does,
naming it (#667, #760)."""
net = Net(tmp_path)
home = tmp_path / "a:b"
result = net.run(env={"HOME": str(home)})
assert net.calls() == [], net.calls()
assert_a_bin_dir_is_refused(result, f"{home}/.local/bin", HOLDS_A_COLON,
INSTALLER_REFUSAL_ENDS)
net.refused(result)


@NO_FILE_NAMED_SO
def test_a_control_character_is_refused_before_the_symlink_check_names_it(
tmp_path):
"""The symlink refusal (#563) names `$target`, so a directory holding a
carriage return that has pipx's link in it would have install.sh print
the value as typed (#760): the control character is refused first, and
the link stays as it was."""
net = Net(tmp_path)
bin_dir = tmp_path / "a\rforged" / "bin"
bin_dir.mkdir(parents=True)
link = bin_dir / "openRepoShape"
link.symlink_to(tmp_path / "nowhere")
result = net.run(env={"OPENREPOSHAPE_BIN_DIR": str(bin_dir)})
assert net.calls() == [], net.calls()
assert_a_bin_dir_is_refused(result, str(bin_dir),
HOLDS_A_CONTROL_CHARACTER,
INSTALLER_REFUSAL_ENDS)
assert link.is_symlink()


@NO_NON_ASCII_INTO_GIT_BASH
@pytest.mark.parametrize("locale", LOCALES)
@pytest.mark.parametrize("name", NOT_CONTROLS)
def test_the_installer_places_a_directory_with_an_unprinted_letter(
tmp_path, locale, name):
"""The refusal is `[[:cntrl:]]` and not `[![:print:]]`, which
`checked_repo` uses (#760): under `LC_ALL=C` the two bytes of an e with
an acute accent are not printable, so the second would refuse a
directory called `caf` and that letter for a fault it has not got. It
is installed into, in either locale, and named as typed, and so is a
no-break space, whose first byte is the C1 controls' (`0xC2`) and
whose second is the one past them, and so are U+2028 and U+2029 (see
`NOT_CONTROLS`). The line is looked for among the lines split at a
newline and nothing else: `splitlines` splits at those two as well."""
net = Net(tmp_path)
net.bin = tmp_path / name
result = net.run(env={"OPENREPOSHAPE_BIN_DIR": bash_path(net.bin),
"LC_ALL": locale})
assert result.returncode == 0, result.stdout + result.stderr
net.installed(result)
assert f"preflight: {bash_path(net.bin)}/openRepoShape --preflight" \
in result.stdout.split("\n"), result.stdout


@NO_NON_ASCII_INTO_GIT_BASH
@pytest.mark.parametrize("locale", LOCALES)
@pytest.mark.parametrize("value", C1_BIN_DIRS)
def test_the_installer_refuses_a_c1_control_in_every_locale_before_any_request(
tmp_path, value, locale):
"""A C1 control (U+0080 to U+009F) in the directory, under `C` and
under `C.UTF-8`, alone, after a byte that is not UTF-8, and after a
colon (#760): exit 2 with the control-character refusal, no request,
and nothing of the value in either stream. Under a UTF-8 locale a byte
that is not UTF-8 anywhere in the value makes bash match byte by byte,
and `[[:cntrl:]]` then matches neither byte of U+009B, as it matches
neither under `C`."""
net = Net(tmp_path)
typed = value.format(d=bash_path(tmp_path))
result = net.run(env={"OPENREPOSHAPE_BIN_DIR": typed, "LC_ALL": locale})
assert net.calls() == [], net.calls()
assert_a_bin_dir_is_refused(result, typed, HOLDS_A_CONTROL_CHARACTER,
INSTALLER_REFUSAL_ENDS)
net.refused(result)


#: A curl that RUNS, in either script: tab-indented, alone or as the value
#: of an assignment. Usage text and refusals carry curl lines too, indented
#: with spaces, and are printed, never run.
Expand Down
Loading
Loading