#!/usr/bin/env bash # A website, as an application. # # The dock and the launcher had nothing to feed them but installed packages, # while both macOS and Windows now turn a site into something with its own # icon and its own window. This is that, in about a hundred lines of shell. # # What must hold: # # 1. The entry it writes is a valid desktop entry that opens the site in app # mode. A malformed one is invisible rather than broken, which is worse. # 2. A name cannot escape the applications directory. The name comes from a # person typing into a launcher box, so "../../../.bashrc" is a thing it # will eventually be handed. # 3. Remove only ever removes its own. Sharing a name with a real # application must not delete that application. # 4. A missing icon does not fail the install. A web app with a generic icon # still works; an install that failed because a favicon 404'd does not. # 5. A browser that cannot do app mode is refused rather than given # something that opens a normal window and pretends. # # Runs against a throwaway XDG_DATA_HOME, so nothing here touches the real # applications directory. Network calls are stubbed. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" webapp="$repo_dir/bin/panama-webapp" findings=() note() { findings+=("$1"); } [[ -x "$webapp" ]] || { printf 'webapp contract: %s is not executable\n' "$webapp" >&2; exit 1; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT data="$work/data" stub="$work/bin" mkdir -p "$data/applications" "$stub" # curl fails for everything, which is the icon-less case. Item 4 says an # install must survive it. cat >"$stub/curl" <<'STUB' #!/usr/bin/env bash exit 1 STUB chmod +x "$stub/curl" # A chromium-family default browser. cat >"$stub/xdg-settings" <<'STUB' #!/usr/bin/env bash printf 'chromium.desktop\n' STUB chmod +x "$stub/xdg-settings" cat >"$stub/chromium" <<'STUB' #!/usr/bin/env bash exit 0 STUB chmod +x "$stub/chromium" mkdir -p "$data/applications" cat >"$data/applications/chromium.desktop" <&1; } # ── 1 & 4. Install writes a valid entry, even with no icon ────────────────── output="$(run install https://app.example.com "Example App")" status=$? (( status == 0 )) || note "install failed when the icon could not be fetched: $output" entry="$data/applications/panama-webapp-example-app.desktop" [[ -f "$entry" ]] || note 'install did not write a desktop entry' if [[ -f "$entry" ]]; then grep -q '^Type=Application$' "$entry" || note 'the entry is not an Application' grep -q '^Name=Example App$' "$entry" || note 'the entry does not carry the name given' grep -q -- '--app=https://app.example.com' "$entry" \ || note 'the entry does not open the site in app mode, so it would open an ordinary browser window' grep -q '^X-Panama-WebApp=' "$entry" \ || note 'the entry is not marked as a Panama web app, so remove cannot tell it from a real application' if command -v desktop-file-validate >/dev/null 2>&1; then desktop-file-validate "$entry" >/dev/null 2>&1 \ || note 'the entry does not pass desktop-file-validate, so a launcher may ignore it' fi fi # ── 2. A hostile name cannot write outside the directory ──────────────────── run install https://example.com "../../../../tmp/panama-escape" >/dev/null 2>&1 [[ -e "$work/panama-escape.desktop" || -e "/tmp/panama-escape.desktop" ]] \ && note 'a name containing path separators wrote outside the applications directory' # It should have landed as a slug inside the directory, or been refused. escaped="$(find "$data/applications" -name '*escape*' | head -1)" if [[ -n "$escaped" ]]; then [[ "$(dirname "$escaped")" == "$data/applications" ]] \ || note 'a hostile name escaped the applications directory' fi # A name with nothing usable in it is refused rather than producing a file # called ".desktop". run install https://example.com "///" >/dev/null 2>&1 \ && note 'a name with no usable characters was accepted' # ── 3. Remove only removes its own ────────────────────────────────────────── # A real application that happens to share a name. cat >"$data/applications/panama-webapp-decoy.desktop" <<'DECOY' [Desktop Entry] Type=Application Name=Decoy Exec=/bin/true DECOY run remove "Decoy" >/dev/null 2>&1 \ && note 'remove deleted an entry that is not a Panama web app' [[ -f "$data/applications/panama-webapp-decoy.desktop" ]] \ || note 'remove deleted a file it should have refused to touch' run remove "Example App" >/dev/null 2>&1 || note 'remove failed on a web app it installed' [[ -f "$entry" ]] && note 'remove left the entry behind' run remove "Not Installed" >/dev/null 2>&1 \ && note 'removing something that was never installed reported success' # ── 5. A browser without app mode is refused ──────────────────────────────── cat >"$stub/xdg-settings" <<'STUB' #!/usr/bin/env bash printf 'firefox.desktop\n' STUB cat >"$data/applications/firefox.desktop" <<'STUB' [Desktop Entry] Name=Firefox Exec=/usr/bin/firefox %U STUB output="$(run install https://example.com "Firefox Test" 2>&1)" status=$? (( status != 0 )) || note 'a browser with no app mode was accepted, so the entry would open an ordinary window' grep -qi 'app' <<<"$output" || note 'the refusal does not explain why' # ── The launcher command ──────────────────────────────────────────────────── command_file="$repo_dir/config/local/share/vicinae/scripts/install-web-app" [[ -x "$command_file" ]] || note 'there is no launcher command to install a web app' grep -q 'argument1' "$command_file" \ || note 'the launcher command takes no URL argument' if (( ${#findings[@]} > 0 )); then printf 'webapp contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'webapp contract: PASS\n'