# shellcheck shell=bash # # Finding the applications and deciding which of them are Chromium underneath. # # The detection is deliberately conservative. A wrong "yes" appends an unknown # argument to something that is not Chromium, and plenty of programs treat an # unrecognised argument as a file name to open - so every rule here is a # positive one, and anything that cannot be identified is reported as unknown # rather than guessed at. # Files that only ever sit next to a Chromium or Electron binary. Any single one # is conclusive; together they cover both bundled Electron (app.asar, the # swiftshader libraries) and plain CEF (libcef). # # Deliberately not in here: libEGL.so and libffmpeg.so. Chromium ships both, but # so does the system - /usr/lib/libEGL.so exists on any machine with Mesa - and # a marker that can be somebody else's file is not a marker. MCA_MARKERS=( chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak icudtl.dat v8_context_snapshot.bin snapshot_blob.bin resources.pak libvk_swiftshader.so LICENSES.chromium.html libcef.so ) # Shared directories, where a marker belongs to the system rather than to the # program that happens to live there. An application ships its payload in a # directory of its own; nothing unpacks Chromium straight into /usr/lib. MCA_SYSTEM_DIRS=( / /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32 /usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin /usr/local/lib /opt ) # Strings in a launcher script that mean it starts a Chromium or Electron # process, for the wrappers whose command line is assembled out of variables and # cannot be followed from the outside. # # Only strings that no other kind of program has a reason to contain. The word # "chromium" on its own is not one of them: /usr/bin/xdg-open lists every # browser it knows how to start, and that is not a browser. # # The flag file convention is an Arch packaging habit and says nothing about the # engine either, so it is not in here. MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint' # --------------------------------------------------------------------------- # Desktop entries # --------------------------------------------------------------------------- # The directories a desktop entry can come from, most specific first - which is # also XDG lookup order, so the first file found for an id is the one that is # actually used. mca_desktop_dirs() { local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d printf '%s\n' "$MCA_APPDIR" while IFS= read -r -d: d; do [[ -n $d ]] && printf '%s/applications\n' "${d%/}" done <<< "${dirs}:" } # mca_desktop_get # A single value from the [Desktop Entry] group. Desktop Action groups repeat # the same keys, and reading past the first group would pick up the wrong one. mca_desktop_get() { local file="$1" key="$2" awk -v key="$key" ' /^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next } inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit } ' "$file" 2>/dev/null } # _mca_desktop_read # Every key the scan needs, in one pass and without a single fork. There are a # couple of hundred desktop entries on an ordinary system, and doing this with # one awk per key per file is the difference between a menu that redraws and a # menu that pauses. DE_TYPE='' DE_HIDDEN='' DE_EXEC='' DE_NAME='' DE_CATEGORIES='' DE_MIME='' DE_OURS='' _mca_desktop_read() { local file="$1" line ingroup=0 DE_TYPE=''; DE_HIDDEN=''; DE_EXEC=''; DE_NAME='' DE_CATEGORIES=''; DE_MIME=''; DE_OURS='' while IFS= read -r line || [[ -n $line ]]; do case "$line" in '[Desktop Entry]'*) ingroup=1; continue ;; '['*) ingroup=0; continue ;; esac (( ingroup )) || continue # Locale variants are Name[de]= and never match these patterns, which # is what we want: the untranslated key is the identifying one. case "$line" in Exec=*) [[ -n $DE_EXEC ]] || DE_EXEC="${line#Exec=}" ;; Name=*) [[ -n $DE_NAME ]] || DE_NAME="${line#Name=}" ;; Type=*) DE_TYPE="${line#Type=}" ;; Hidden=*) DE_HIDDEN="${line#Hidden=}" ;; Categories=*) DE_CATEGORIES="${line#Categories=}" ;; MimeType=*) DE_MIME="${line#MimeType=}" ;; # Only a generated shadow. An entry we edited in place carries # X-MCA-Patched and is still the application's real entry, so it # has to stay in the scan. X-MCA-Generated=*) DE_OURS=1 ;; esac done < "$file" } # mca_exec_program # The program a desktop entry actually starts: the first token that is not an # environment prefix, resolved to an absolute path. The result is left in # MCA_PROG rather than printed - the scan calls this for every desktop entry on # the system, and a command substitution each time is a fork each time. # # Fails for entries this tool has no safe way to rewrite: anything routed # through a shell, where the real program is inside a quoted string. MCA_PROG='' mca_exec_program() { local line="$1" tok prog='' local -a tokens MCA_PROG='' # Field codes are placeholders, not arguments, and quotes only ever wrap # whole tokens here; splitting on whitespace is enough to find token one. read -r -a tokens <<< "$line" for tok in "${tokens[@]}"; do tok="${tok%\"}"; tok="${tok#\"}" tok="${tok%\'}"; tok="${tok#\'}" [[ -z $tok ]] && continue [[ $tok == *=* && $tok != /* ]] && continue # VAR=value prefix [[ $tok == env ]] && continue prog="$tok" break done [[ -n $prog ]] || return 1 case "${prog##*/}" in sh|bash|dash|zsh|fish) return 1 ;; esac # PATH is searched here rather than with `command -v`, which is a builtin # but would have to be read back through a command substitution, and that # is a fork per desktop entry. if [[ $prog != /* ]]; then local d found='' local -a pathdirs IFS=: read -r -a pathdirs <<< "$PATH" for d in "${pathdirs[@]}"; do [[ -n $d ]] || continue if [[ -x "$d/$prog" && ! -d "$d/$prog" ]]; then found="$d/$prog"; break; fi done [[ -n $found ]] || return 1 prog="$found" fi MCA_PROG="$prog" } # mca_exec_flatpak_id # The application id out of a `flatpak run ...` command line, in MCA_PROG. mca_exec_flatpak_id() { local line="$1" tok seen_run=0 local -a tokens read -r -a tokens <<< "$line" MCA_PROG='' for tok in "${tokens[@]}"; do if (( ! seen_run )); then [[ $tok == run ]] && seen_run=1 continue fi [[ $tok == -* || $tok == @@* || $tok == %* ]] && continue [[ $tok == *.*.* ]] || continue MCA_PROG="$tok" return 0 done return 1 } # --------------------------------------------------------------------------- # Is this Chromium? # --------------------------------------------------------------------------- # Answers are cached against size and mtime, because the systemd path unit can # fire several times in a row while a package installs and each miss costs a # scan of a 200 MB binary. # # The file is read once into memory rather than searched per lookup: a scan # asks about every program on the system, and an awk per question is most of # the time the scan takes. declare -A MCA_DETECT_MEMO=() declare -A MCA_DETECT_CACHE=() MCA_CACHE_LOADED=0 MCA_CACHE_DIRTY=0 # Size and mtime for every program the scan is about to ask about, collected in # one call. Checking a cache entry is still stale needs a stat, and one stat per # program on the system was most of what a warm scan spent its time on. declare -A MCA_STAT=() _mca_stat_batch() { local name st (( $# )) || return 0 while IFS=$'\t' read -r name st; do [[ -n $name ]] && MCA_STAT["$name"]="$st" done < <(stat -Lc '%n %s:%Y' -- "$@" 2>/dev/null) return 0 } _mca_cache_load() { local path stamp verdict (( MCA_CACHE_LOADED )) && return 0 MCA_CACHE_LOADED=1 [[ -r "$MCA_CACHEDIR/detect" ]] || return 0 while IFS=$'\t' read -r path stamp verdict; do [[ -n $path && -n $stamp ]] || continue MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict" done < "$MCA_CACHEDIR/detect" return 0 } # Written back once, at exit, instead of after every miss. mca_cache_flush() { local path entry tmp (( MCA_CACHE_DIRTY )) || return 0 mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0 tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0 for path in "${!MCA_DETECT_CACHE[@]}"; do entry="${MCA_DETECT_CACHE[$path]}" printf '%s\t%s\n' "$path" "$entry" >> "$tmp" done mv -f "$tmp" "$MCA_CACHEDIR/detect" 2>/dev/null || rm -f "$tmp" MCA_CACHE_DIRTY=0 return 0 } # _mca_has_markers _mca_has_markers() { local dir="$1" m s [[ -d $dir ]] || return 1 dir="${dir%/}" for s in "${MCA_SYSTEM_DIRS[@]}"; do [[ $dir == "$s" ]] && return 1 done for m in "${MCA_MARKERS[@]}"; do [[ -e "$dir/$m" ]] && return 0 done [[ -e "$dir/resources/app.asar" || -e "$dir/app.asar" ]] && return 0 return 1 } # _mca_script_target