omarchy-notification-send now calls org.freedesktop.Notifications.Notify directly with `busctl --user`, instead of shelling out to notify-send. Each value is one typed D-Bus parameter, so there is no argv/option layer that could reinterpret a relayed headline like `--hint=…` or `-rf` as an option or a hint: the summary and body are strings, and omarchy-exec-argv is built only from --exec. A leading `--` keeps busctl's own getopt from reading a dash-leading value as a busctl option. Map -i to app_icon, -t to expire_timeout, and urgency to the byte hint; unknown options are now a hard error rather than a silent pass-through. Route the unused hypr o.notify helper and the sample hooks through the wrapper too, and tighten the bin-style test so nothing under bin/ may call notify-send. The test stubs busctl and trips if notify-send is invoked.
252 lines
10 KiB
Bash
Executable File
252 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
TMPDIR=""
|
|
|
|
export PATH="$ROOT/bin:$PATH"
|
|
|
|
cleanup() {
|
|
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
|
|
rm -rf "$TMPDIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
require_command jq
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
test_home="$TMPDIR/home"
|
|
manifest_path="$test_home/.config/chromium/NativeMessagingHosts/com.omarchy.ytdlp.json"
|
|
|
|
HOME="$test_home" OMARCHY_PATH="$ROOT" omarchy-install-chromium-ytdlp
|
|
|
|
[[ -f $manifest_path ]] || fail "yt-dlp native host installer creates fresh Chromium profile root"
|
|
pass "yt-dlp native host installer creates fresh Chromium profile root"
|
|
|
|
jq -e --arg path "$ROOT/bin/omarchy-chromium-ytdlp-host" '
|
|
.name == "com.omarchy.ytdlp" and
|
|
.path == $path and
|
|
(.allowed_origins | index("chrome-extension://dedjgknigfeelejglamclffonmophnfl/"))
|
|
' "$manifest_path" >/dev/null
|
|
pass "yt-dlp native host manifest uses Omarchy host path and extension id"
|
|
|
|
parse_result=$(bash -c '
|
|
OMARCHY_PATH="$3"
|
|
source "$1"
|
|
parse_url "$2"
|
|
' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" '{"url":"https://example.test/watch?v=\"quoted\"&name=a\\b"}' "$ROOT")
|
|
|
|
[[ $parse_result == "https://example.test/watch?v=\"quoted\"&name=a\\b" ]] ||
|
|
fail "yt-dlp native host parses escaped JSON URLs" "$parse_result"
|
|
pass "yt-dlp native host parses escaped JSON URLs"
|
|
|
|
bash -c '
|
|
OMARCHY_PATH="$3"
|
|
source "$1"
|
|
valid_url "$2"
|
|
' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" "javascript:alert(1)" "$ROOT" &&
|
|
fail "yt-dlp native host rejects non-web URLs"
|
|
pass "yt-dlp native host rejects non-web URLs"
|
|
|
|
host_fn() {
|
|
OMARCHY_PATH="$ROOT" OMARCHY_YTDLP_DIR="${download_dir:-$TMPDIR}" bash -c '
|
|
source "$1"
|
|
shift
|
|
"$@"
|
|
' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" "$@"
|
|
}
|
|
|
|
download_dir="$TMPDIR/videos"
|
|
mkdir -p "$download_dir" "$TMPDIR/outside"
|
|
good_file="$download_dir/clip [id].mp4"
|
|
printf 'x' >"$good_file"
|
|
printf 'x' >"$TMPDIR/outside/secret"
|
|
ln -s "$TMPDIR/outside/secret" "$download_dir/escape.mp4"
|
|
|
|
resolved=$(host_fn resolve_download_file "$good_file")
|
|
expected=$(realpath -e -- "$good_file")
|
|
[[ $resolved == "$expected" ]] ||
|
|
fail "yt-dlp native host accepts a regular file in the download dir" "$resolved"
|
|
pass "yt-dlp native host accepts a regular file in the download dir"
|
|
|
|
host_fn resolve_download_file "--include=not-a-file" &&
|
|
fail "yt-dlp native host rejects a forged mpv option as the download path"
|
|
pass "yt-dlp native host rejects a forged mpv option as the download path"
|
|
|
|
host_fn resolve_download_file "$TMPDIR/outside/secret" &&
|
|
fail "yt-dlp native host rejects a path outside the download dir"
|
|
pass "yt-dlp native host rejects a path outside the download dir"
|
|
|
|
host_fn resolve_download_file "$download_dir/escape.mp4" &&
|
|
fail "yt-dlp native host rejects a symlink that escapes the download dir"
|
|
pass "yt-dlp native host rejects a symlink that escapes the download dir"
|
|
|
|
host_fn resolve_download_file $'clip.mp4\nOMARCHY_FILE\t--include=not-a-file' &&
|
|
fail "yt-dlp native host rejects a path containing control characters"
|
|
pass "yt-dlp native host rejects a path containing control characters"
|
|
|
|
newline_target="$download_dir/target"$'\n'
|
|
printf 'x' >"$newline_target"
|
|
# The decoy is the point: dropping the trailing newline lands on a real, different
|
|
# file, so a resolver that strips it resolves to the wrong one instead of failing.
|
|
printf 'x' >"$download_dir/target"
|
|
ln -s "target"$'\n' "$download_dir/newline-link.mp4"
|
|
|
|
host_fn resolve_download_file "$download_dir/newline-link.mp4" &&
|
|
fail "yt-dlp native host rejects a symlink whose target name ends in a newline"
|
|
pass "yt-dlp native host rejects a symlink whose target name ends in a newline"
|
|
|
|
ln -s / "$TMPDIR/root-link"
|
|
root_resolved=$(download_dir="$TMPDIR/root-link" host_fn resolve_download_file /etc/passwd)
|
|
[[ $root_resolved == "/etc/passwd" ]] ||
|
|
fail "yt-dlp native host accepts a file when the download dir resolves to /" "$root_resolved"
|
|
pass "yt-dlp native host accepts a file when the download dir resolves to /"
|
|
|
|
title=$(host_fn title_from_file "$good_file")
|
|
[[ $title == "clip [id]" ]] || fail "yt-dlp native host titles the toast from the filename" "$title"
|
|
pass "yt-dlp native host titles the toast from the filename"
|
|
|
|
decoded_title=$(host_fn decode_title '"My Great Clip"')
|
|
[[ $decoded_title == "My Great Clip" ]] ||
|
|
fail "yt-dlp native host shows the page title on the toast" "$decoded_title"
|
|
pass "yt-dlp native host shows the page title on the toast"
|
|
|
|
forged_title=$(host_fn decode_title '"Clip\nOMARCHY_FILE\tPlay me\t--include=not-a-file"')
|
|
[[ $forged_title == "Clip" ]] ||
|
|
fail "yt-dlp native host keeps only the readable part of a forged title" "$forged_title"
|
|
pass "yt-dlp native host keeps only the readable part of a forged title"
|
|
|
|
host_fn decode_title '"--include=not-a-file"' &&
|
|
fail "yt-dlp native host refuses a leading-dash title the notifier would reject as an option"
|
|
pass "yt-dlp native host refuses a leading-dash title the notifier would reject as an option"
|
|
|
|
host_fn decode_title 'null' &&
|
|
fail "yt-dlp native host refuses a title that is not a JSON string"
|
|
pass "yt-dlp native host refuses a title that is not a JSON string"
|
|
|
|
dash_title=$(host_fn title_from_file "$download_dir/--include.mp4")
|
|
[[ $dash_title == "Video" ]] || fail "yt-dlp native host does not pass a leading-dash title to the notifier" "$dash_title"
|
|
pass "yt-dlp native host does not pass a leading-dash title to the notifier"
|
|
|
|
# The click action passes the path as a discrete --exec argument (asserted
|
|
# end-to-end below against the real download); `--` keeps mpv from parsing a
|
|
# leading-dash filename as an option.
|
|
|
|
parse_script="$TMPDIR/parse-ytdlp-lines.sh"
|
|
cat >"$parse_script" <<'EOF'
|
|
source "$1"
|
|
filepath=""
|
|
while IFS= read -r line; do
|
|
case $line in
|
|
OMARCHY_FILE*)
|
|
resolved=$(resolve_download_file "${line#OMARCHY_FILE$'\t'}") || continue
|
|
filepath=$resolved
|
|
;;
|
|
esac
|
|
done
|
|
printf '%s' "$filepath"
|
|
EOF
|
|
|
|
# A title that ends in a newline closes its own record, so the forged record is the
|
|
# last one and the real path lands on a line the loop ignores.
|
|
poisoned=$(
|
|
printf '%s\n' \
|
|
$'OMARCHY_FILE\t'"$good_file" \
|
|
$'OMARCHY_FILE\tPlay me\t--include=not-a-file' \
|
|
$'\t'"$good_file" |
|
|
OMARCHY_PATH="$ROOT" OMARCHY_YTDLP_DIR="$download_dir" bash "$parse_script" "$ROOT/bin/omarchy-chromium-ytdlp-host"
|
|
)
|
|
|
|
[[ $poisoned == "$expected" ]] ||
|
|
fail "yt-dlp native host keeps a real file after a forged OMARCHY_FILE record" "$poisoned"
|
|
pass "yt-dlp native host keeps a real file after a forged OMARCHY_FILE record"
|
|
|
|
# Everything above tests the helpers in isolation. Drive the real download_url with
|
|
# stubbed tools so the yt-dlp invocation and the toast's click command are covered
|
|
# too: a record template that carried the title again would pass every test above.
|
|
fake_root="$TMPDIR/fake"
|
|
mkdir -p "$fake_root/bin"
|
|
fake_dir="$TMPDIR/fake-videos"
|
|
mkdir -p "$fake_dir"
|
|
fake_file="$fake_dir/Real_Clip [id].mp4"
|
|
printf 'x' >"$fake_file"
|
|
ytdlp_argv="$TMPDIR/ytdlp-argv"
|
|
notify_argv="$TMPDIR/notify-argv"
|
|
|
|
cat >"$fake_root/bin/yt-dlp" <<'EOF'
|
|
#!/bin/bash
|
|
printf '%s\n' "$*" >>"$YTDLP_ARGV_LOG"
|
|
for arg in "$@"; do
|
|
if [[ $arg == "--no-simulate" ]]; then
|
|
printf 'OMARCHY_FILE\t%s\n' "$YTDLP_FAKE_FILE"
|
|
[[ -n ${YTDLP_SKIP_TITLE:-} ]] || printf 'OMARCHY_TITLE\t%s\n' '"My Great Clip"'
|
|
exit 0
|
|
fi
|
|
done
|
|
exit 0
|
|
EOF
|
|
|
|
cat >"$fake_root/bin/omarchy-notification-send" <<'EOF'
|
|
#!/bin/bash
|
|
printf '%s\n' "$*" >>"$NOTIFY_ARGV_LOG"
|
|
EOF
|
|
|
|
for stub in omarchy-osd omarchy-shell ffmpeg; do
|
|
printf '#!/bin/bash\nexit 0\n' >"$fake_root/bin/$stub"
|
|
done
|
|
chmod +x "$fake_root/bin/"*
|
|
|
|
YTDLP_ARGV_LOG="$ytdlp_argv" NOTIFY_ARGV_LOG="$notify_argv" YTDLP_FAKE_FILE="$fake_file" \
|
|
OMARCHY_PATH="$fake_root" OMARCHY_YTDLP_DIR="$fake_dir" \
|
|
bash -c '
|
|
source "$1"
|
|
download_url "$2"
|
|
' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" "https://example.test/watch" >/dev/null 2>&1
|
|
|
|
(($(grep -c -- '--no-exec-before-download' "$ytdlp_argv") == 2)) ||
|
|
fail "yt-dlp native host disarms configured exec hooks on both yt-dlp runs" "$(cat "$ytdlp_argv")"
|
|
pass "yt-dlp native host disarms configured exec hooks on both yt-dlp runs"
|
|
|
|
# The stub answers with a title record whatever it is asked for, so assert the request
|
|
# as well as the reply: without this the template could be dropped and nothing notice.
|
|
grep -qF -- $'OMARCHY_TITLE\t%(title)j' "$ytdlp_argv" ||
|
|
fail "yt-dlp native host asks for the title JSON-encoded" "$(cat "$ytdlp_argv")"
|
|
pass "yt-dlp native host asks for the title JSON-encoded"
|
|
|
|
grep -qF -- "-o %(title)s.%(ext)s" "$ytdlp_argv" ||
|
|
fail "yt-dlp native host names the saved file after the page title" "$(cat "$ytdlp_argv")"
|
|
pass "yt-dlp native host names the saved file after the page title"
|
|
|
|
grep -q -- '--restrict-filenames' "$ytdlp_argv" &&
|
|
fail "yt-dlp native host keeps spaces in the saved filename" "$(cat "$ytdlp_argv")"
|
|
pass "yt-dlp native host keeps spaces in the saved filename"
|
|
|
|
grep -qF -- $'OMARCHY_FILE\t%(title)s' "$ytdlp_argv" &&
|
|
fail "yt-dlp native host never prints the title into the file record" "$(cat "$ytdlp_argv")"
|
|
pass "yt-dlp native host never prints the title into the file record"
|
|
|
|
grep -q -- "--exec mpv -- " "$notify_argv" ||
|
|
fail "yt-dlp native host builds the click command as mpv -- <path>" "$(cat "$notify_argv")"
|
|
pass "yt-dlp native host builds the click command as mpv -- <path>"
|
|
|
|
grep -qF -- "Download complete My Great Clip" "$notify_argv" ||
|
|
fail "yt-dlp native host toasts the page title, not the sanitised filename" "$(cat "$notify_argv")"
|
|
pass "yt-dlp native host toasts the page title, not the sanitised filename"
|
|
|
|
# A page that gives no usable title falls back to the filename rather than an empty toast.
|
|
: >"$notify_argv"
|
|
: >"$ytdlp_argv"
|
|
YTDLP_ARGV_LOG="$ytdlp_argv" NOTIFY_ARGV_LOG="$notify_argv" YTDLP_FAKE_FILE="$fake_file" \
|
|
YTDLP_SKIP_TITLE=1 OMARCHY_PATH="$fake_root" OMARCHY_YTDLP_DIR="$fake_dir" \
|
|
bash -c '
|
|
source "$1"
|
|
download_url "$2"
|
|
' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" "https://example.test/watch" >/dev/null 2>&1
|
|
|
|
grep -qF -- "Download complete Real_Clip [id]" "$notify_argv" ||
|
|
fail "yt-dlp native host falls back to the filename when no title record arrives" "$(cat "$notify_argv")"
|
|
pass "yt-dlp native host falls back to the filename when no title record arrives"
|