Compare commits

..
5 Commits
Author SHA1 Message Date
Felitendo 0fbe8bdb39 chore: 1.5.1
release / Debian package (push) Failing after 13s
release / RPM package (push) Failing after 22s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
2026-09-15 19:12:54 +02:00
Felitendo a992efb0cb fix: pass Brave's autoscroll feature name to browsers 2026-09-15 19:01:01 +02:00
Felitendo b97ccf0379 chore: toned down readme 2026-09-15 08:44:43 +02:00
Felitendo 7a446364dc feat: identify AppImages by reading their squashfs name table
Every AppImage came out as "cannot tell" because the payload is a
compressed filesystem and none of the Chromium markers is on disk to be
found. The names are reachable, though: squashfs keeps the name of
everything it holds in one table of a few kilobytes, and a name is all
this question needs. That table is now read and inflated in place -
nothing is unpacked, and the image is never run, which a scan started by
the path unit has no business doing anyway.

So a Tauri AppImage such as Modrinth is recognised as WebKitGTK and
dropped from the list instead of being offered as something that could be
switched on - there is no autoscroll in WebKit to ask for - and an
Electron one is covered like any other application.

Left as "cannot tell": lzo and lz4, which squashfs stores as bare blocks
no tool will read without their own framing, and the original ISO9660
layout.

Alongside:

- The detect cache carries a format line. Its entries are keyed on size
  and mtime, so an AppImage that has not changed would otherwise keep
  answering the way an older version decided it did, forever.

- A marker list for whole-tree searches, which is what a Flatpak, a snap
  and an AppImage need. icudtl.dat, snapshot_blob.bin and resources.pak
  are out of it: Flutter ships an icudtl.dat in data/, and next to a
  binary those names are evidence while four directories down they are
  not. The Flatpak and snap searches now share that list.
2026-09-14 14:06:23 +02:00
Felitendo 449a20ce9b chore: added funding 2026-09-08 14:33:51 +02:00
6 changed files with 477 additions and 59 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: Felitendo
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: felitendo
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+1 -1
View File
@@ -8,7 +8,7 @@
# Overridable so a packager can pass the version it is actually building # Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds # (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry. # straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.5.0 VERSION ?= 1.5.1
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
+16 -5
View File
@@ -53,13 +53,13 @@ You can also press `[4]` for more settings.
## How to install ## How to install
**Arch (for the cachyos enjoyers)** **Arch**
```bash ```bash
yay -S middleclick-autoscroll yay -S middleclick-autoscroll
``` ```
**Fedora (for the mentally stable)** **Fedora**
```bash ```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \ sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
@@ -67,7 +67,7 @@ sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
sudo dnf install middleclick-autoscroll sudo dnf install middleclick-autoscroll
``` ```
**Bazzite (for the reboot enjoyers)** **Bazzite**
```bash ```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \ sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
@@ -76,7 +76,7 @@ sudo rpm-ostree install middleclick-autoscroll
systemctl reboot systemctl reboot
``` ```
**Debian (for the elderly)** **Debian**
```bash ```bash
sudo install -d -m 0755 /etc/apt/keyrings sudo install -d -m 0755 /etc/apt/keyrings
@@ -87,7 +87,7 @@ echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] https://felit
sudo apt update && sudo apt install middleclick-autoscroll sudo apt update && sudo apt install middleclick-autoscroll
``` ```
**openSUSE (for the both of you)** **openSUSE**
```bash ```bash
sudo rpm --import https://felitendo.github.io/middleclick-autoscroll/KEY.gpg sudo rpm --import https://felitendo.github.io/middleclick-autoscroll/KEY.gpg
@@ -113,10 +113,21 @@ Browsers get the same thing but this time without "blink":
But both do the same ¯\_(ツ)_/¯ But both do the same ¯\_(ツ)_/¯
Except in Helium and Brave, which have their own names for it
(`HeliumMiddleClickAutoscroll`, and Brave's is actually spelled
`MiddelButtonClickAutoscroll`), so browsers get those as well.
New apps are picked up by a systemd path unit that watches the New apps are picked up by a systemd path unit that watches the
relevant directories. If you hate systemd; `middleclick-autoscroll apply` does the relevant directories. If you hate systemd; `middleclick-autoscroll apply` does the
same thing manually. same thing manually.
AppImages get looked into as well. The payload is a squashfs image glued onto
the runtime, and squashfs keeps a small table with the name of every file in
there - so that table gets read and unpacked (a few kb, nothing is extracted
and the thing is never run) and the Chromium files are looked for in it. An
Electron AppImage is covered like any other app; a Tauri one is left alone,
because WebKitGTK simply has no autoscroll to switch on.
## Commands ## Commands
| Command | | | Command | |
+44 -9
View File
@@ -26,10 +26,11 @@ runtime flags - but it is not on the list of flags Chromium warns about, so the
browser does not put a bar reading "You are using an unsupported command-line browser does not put a bar reading "You are using an unsupported command-line
flag" above every page. That spelling only works from Chromium 124 onwards, flag" above every page. That spelling only works from Chromium 124 onwards,
which is why everything else keeps the first one: an application that embeds an which is why everything else keeps the first one: an application that embeds an
older Chromium, such as Steam's CEF, has no such bar to show anyway. Helium older Chromium, such as Steam's CEF, has no such bar to show anyway. Two
knows the feature under a name of its own and is asked for browsers know the feature under a name of their own and ignore Chromium's:
*HeliumMiddleClickAutoscroll* as well; a name a browser does not know is Helium, which is asked for *HeliumMiddleClickAutoscroll* as well, and Brave,
ignored. It works on any distribution: which is asked for *MiddelButtonClickAutoscroll* - spelled the way Brave spells
it. A name a browser does not know is ignored. It works on any distribution:
which of the routes below an application takes is read off its launcher, not which of the routes below an application takes is read off its launcher, not
assumed from where the launcher came from. assumed from where the launcher came from.
@@ -201,11 +202,37 @@ status screen leaves the row out where there is nothing to report.
Turn it off under *Settings* to keep the desktop's middle-click paste. Turn it off under *Settings* to keep the desktop's middle-click paste.
# WHAT CANNOT BE DETECTED # APPIMAGES
An AppImage keeps its payload in a compressed filesystem, so there is no way to An AppImage is the AppImage runtime, an ordinary executable, with a squashfs
tell from the outside whether it contains Chromium. Those are listed as *cannot image appended to it. The payload is compressed, so none of the files that
tell* and left alone until they are switched on from the applications screen. identify Chromium is on disk where it could be found.
The names are, though. squashfs keeps the name of everything it holds in one
table of its own, a few kilobytes of it, and a name is all this question needs -
so that table is read and decompressed in place. Nothing is unpacked, and the
image is never run: this can happen from the watcher, and starting a program
because it was installed is not something a scan gets to do.
Two kinds of image are still out of reach. One is packed with *lzo* or *lz4*,
which squashfs stores as bare blocks that neither tool will read without the
framing their own file formats add. The other is the original AppImage layout,
which is an ISO9660 filesystem rather than a squashfs one. Both are listed as
*cannot tell* and left alone until they are switched on from the applications
screen.
# WHAT HAS NO FLAG AT ALL
Autoscroll is a Blink feature, so only an application drawn by Blink can be
given it. An application built on WebKitGTK - which is what Tauri uses on
Linux, and GNOME Web, and anything else linked against libwebkit2gtk - is not
one of those. WebKit has no equivalent feature to ask for, on any command line
or in any configuration file.
Such an application is identified as what it is and then left out of the list
entirely, rather than being offered as something that could be switched on. An
application that is absent from *list* is absent because there is nothing this
tool could do for it.
# FILES # FILES
@@ -226,7 +253,10 @@ _~/.config/kwinrc_
_~/.cache/middleclick-autoscroll/detect_ _~/.cache/middleclick-autoscroll/detect_
Which programs were found to be Chromium, keyed by size and modification Which programs were found to be Chromium, keyed by size and modification
time. Safe to delete. time. Its first line says which version of the program wrote it, and a
file from an older one is ignored rather than trusted - an entry for a
file that has not changed would otherwise never be looked at again.
Safe to delete.
# ENVIRONMENT # ENVIRONMENT
@@ -239,6 +269,11 @@ Bash 4.2 or newer and GNU coreutils. The watcher needs a systemd user session;
without one everything else works and applications installed later are picked without one everything else works and applications installed later are picked
up at the next *apply* rather than on their own. up at the next *apply* rather than on their own.
*gzip* is what reads the name table of an AppImage, and *xz* or *zstd* the ones
packed with those instead. None of the three is required: an image whose
compressor has no tool installed is listed as *cannot tell*, exactly like the
two formats there is no reader for at all.
# SEE ALSO # SEE ALSO
*systemctl*(1), *flatpak*(1), *snap*(8), *kwriteconfig6*(1) *systemctl*(1), *flatpak*(1), *snap*(8), *kwriteconfig6*(1)
+13 -6
View File
@@ -35,15 +35,19 @@ MCA_PRETTY="Middle-Click Autoscroll"
# the one that always works. # the one that always works.
MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll" MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll"
# Helium ships the feature under its own name and does not answer to the # Two browsers do not answer to the Chromium name. Helium ships the feature
# Chromium one, so browsers are asked for both. A name a browser does not know # under its own name and sets the runtime flag from that alone. Brave takes
# is ignored, which is what makes one list safe to hand to all of them. # the generated feature away and switches the flag on from one of its own,
MCA_BROWSER_FLAG="--enable-features=MiddleClickAutoscroll,HeliumMiddleClickAutoscroll" # spelled MiddelButtonClickAutoscroll - the typo is in Brave's source, and the
# correct spelling does nothing. So browsers are asked for all three. A name a
# browser does not know is ignored, which is what makes one list safe to hand
# to all of them.
MCA_BROWSER_FLAG="--enable-features=MiddleClickAutoscroll,HeliumMiddleClickAutoscroll,MiddelButtonClickAutoscroll"
# The feature names on their own, for merging into a list that an application # The feature names on their own, for merging into a list that an application
# (or the user) already carries, and for taking them back out again. # (or the user) already carries, and for taking them back out again.
MCA_FEATURE="MiddleClickAutoscroll" MCA_FEATURE="MiddleClickAutoscroll"
MCA_BROWSER_FEATURES="MiddleClickAutoscroll,HeliumMiddleClickAutoscroll" MCA_BROWSER_FEATURES="MiddleClickAutoscroll,HeliumMiddleClickAutoscroll,MiddelButtonClickAutoscroll"
# Bumped when the flags above change, and when what gets written where does. # Bumped when the flags above change, and when what gets written where does.
# An installation that was set up by a version with a different answer is taken # An installation that was set up by a version with a different answer is taken
@@ -52,7 +56,10 @@ MCA_BROWSER_FEATURES="MiddleClickAutoscroll,HeliumMiddleClickAutoscroll"
# #
# 3 is what takes -noverifyfiles back out of the Steam entries an older version # 3 is what takes -noverifyfiles back out of the Steam entries an older version
# put it in. # put it in.
MCA_FLAG_SCHEME=3 #
# 4 adds Brave's name to the browser list, which an entry edited in place would
# otherwise never get.
MCA_FLAG_SCHEME=4
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}" MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}" MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}"
+388 -38
View File
@@ -21,6 +21,22 @@ MCA_MARKERS=(
libvk_swiftshader.so LICENSES.chromium.html libcef.so libvk_swiftshader.so LICENSES.chromium.html libcef.so
) )
# The markers that still mean something when the whole of a tree is searched
# rather than just the directory the binary sits in - inside a Flatpak, a snap
# or an AppImage, where there is no "next to the binary" to look at.
#
# Three of the names above are missing here on purpose. icudtl.dat is ICU's
# data file and not Chromium's: a Flutter application ships one in data/, and
# a whole-tree search would find it. snapshot_blob.bin and resources.pak are
# the same kind of shared name. Next to a binary they are still evidence,
# because nothing but Chromium unpacks its payload there - a few directories
# deeper they are not.
MCA_MARKERS_STRICT=(
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
v8_context_snapshot.bin libvk_swiftshader.so LICENSES.chromium.html
libcef.so app.asar
)
# Shared directories, where a marker belongs to the system rather than to the # 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 # program that happens to live there. An application ships its payload in a
# directory of its own; nothing unpacks Chromium straight into /usr/lib. # directory of its own; nothing unpacks Chromium straight into /usr/lib.
@@ -309,14 +325,27 @@ _mca_stat_batch() {
return 0 return 0
} }
# The first line of the cache file, and the reason it is there: the verdicts
# below it are keyed on size and mtime, so an entry for a file that has not
# changed is never looked at again. A cache written when a verdict meant
# something else - before an AppImage could come out as anything but "no" -
# would therefore keep answering the old way forever. Bumping this is how such
# a cache gets dropped instead.
MCA_CACHE_FORMAT='# middleclick-autoscroll detect 2'
_mca_cache_load() { _mca_cache_load() {
local path stamp verdict local path stamp verdict first=1
(( MCA_CACHE_LOADED )) && return 0 (( MCA_CACHE_LOADED )) && return 0
MCA_CACHE_LOADED=1 MCA_CACHE_LOADED=1
[[ -r "$MCA_CACHEDIR/detect" ]] || return 0 [[ -r "$MCA_CACHEDIR/detect" ]] || return 0
while IFS=$'\t' read -r path stamp verdict; do while IFS=$'\t' read -r path stamp verdict; do
if (( first )); then
first=0
[[ $path == "$MCA_CACHE_FORMAT" ]] || return 0
continue
fi
[[ -n $path && -n $stamp ]] || continue [[ -n $path && -n $stamp ]] || continue
MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict" MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict"
done < "$MCA_CACHEDIR/detect" done < "$MCA_CACHEDIR/detect"
@@ -331,6 +360,7 @@ mca_cache_flush() {
mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0 mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0
tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0 tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0
printf '%s\n' "$MCA_CACHE_FORMAT" > "$tmp"
for path in "${!MCA_DETECT_CACHE[@]}"; do for path in "${!MCA_DETECT_CACHE[@]}"; do
entry="${MCA_DETECT_CACHE[$path]}" entry="${MCA_DETECT_CACHE[$path]}"
printf '%s\t%s\n' "$path" "$entry" >> "$tmp" printf '%s\t%s\n' "$path" "$entry" >> "$tmp"
@@ -362,6 +392,26 @@ _mca_has_markers() {
return 1 return 1
} }
# _mca_find_markers <directory> <depth>
# The same question for a whole tree, which is the shape a Flatpak, a snap and
# an unpacked AppImage come in: everything the application ships is somewhere
# under one root and there is no single directory that is "next to the binary".
# Hence the narrower list - see MCA_MARKERS_STRICT.
_mca_find_markers() {
local root="$1" depth="$2" m
local -a names=()
[[ -d $root ]] || return 1
for m in "${MCA_MARKERS_STRICT[@]}"; do
(( ${#names[@]} )) && names+=(-o)
names+=(-name "$m")
done
[[ -n "$(find "$root" -maxdepth "$depth" \
\( "${names[@]}" \) -print -quit 2>/dev/null)" ]]
}
# The plain assignments the script made before it handed over, for # The plain assignments the script made before it handed over, for
# _mca_script_subst to read. A variable of its own rather than something passed # _mca_script_subst to read. A variable of its own rather than something passed
# around: every caller of _mca_script_target reads it through a command # around: every caller of _mca_script_target reads it through a command
@@ -521,18 +571,295 @@ mca_flags_candidates() {
fi fi
} }
# mca_is_chromium <program> # ---------------------------------------------------------------------------
# Succeeds when the program is a Chromium, CEF or Electron process. # Looking inside an AppImage
mca_is_chromium() { # ---------------------------------------------------------------------------
local prog="$1" verdict real stamp cached #
# An AppImage is the AppImage runtime - an ordinary ELF executable - with a
# squashfs image appended to it. The payload is compressed, so nothing the
# application ships is on disk where the marker check could find it, and for a
# long time that made every AppImage a "cannot tell".
#
# It does not have to be. squashfs keeps the names of everything it holds in
# one table of its own, the table is a few kilobytes, and the names are the
# whole of what this question needs. So that table is read and inflated here -
# rather than unpacking a hundred megabytes, and rather than running the image
# to ask it, which is the one thing a scan started by a path unit must never
# do.
#
# Everything below fails closed. The moment a field is not where it should be,
# or the compressor is one there is no tool for, the answer goes back to
# "cannot tell" and the application is left to the applications screen.
[[ -n $prog && -e $prog ]] || return 1 # _mca_bytes <file> <offset> <count>
# Count bytes from an offset, as numbers, in MCA_BYTES. od rather than a shell
# read because a variable cannot hold a NUL byte and these headers are full of
# them - and a whole header at a time rather than a field at a time, because
# the first question below is asked about every program on the system and one
# od per field would be four forks per program.
MCA_BYTES=()
_mca_bytes() {
local file="$1" off="$2" count="$3"
MCA_BYTES=()
# read stops on end of input rather than on the delimiter it was given, so
# it reports failure for input that is perfectly complete. The count is
# the check.
read -r -d '' -a MCA_BYTES \
< <(od -An -tu1 -j"$off" -N"$count" -v -- "$file" 2>/dev/null)
(( ${#MCA_BYTES[@]} == count ))
}
# _mca_slice <file> <offset> <length>
# Length bytes from an offset, on stdout.
_mca_slice() {
dd if="$1" bs=65536 iflag=skip_bytes,count_bytes \
skip="$2" count="$3" status=none 2>/dev/null
}
# _mca_word <index>
# One 64-bit little-endian field out of MCA_BYTES, in MCA_WORD. Assigns rather
# than prints for the same reason _mca_bytes reads a whole header at a time:
# printing means a command substitution, and that is a fork per field.
#
# Fails for the all-ones value squashfs writes down for a table that is not in
# the image, which is a case the callers below have to tell from a real offset
# and bash arithmetic - signed, 64-bit - cannot represent.
MCA_WORD=0
_mca_word() {
local at="$1"
(( ${#MCA_BYTES[@]} >= at + 8 )) || return 1
(( MCA_BYTES[at+7] == 255 && MCA_BYTES[at+6] == 255 )) && return 1
MCA_WORD=$(( (MCA_BYTES[at] | MCA_BYTES[at+1] << 8 \
| MCA_BYTES[at+2] << 16 | MCA_BYTES[at+3] << 24) \
| ((MCA_BYTES[at+4] | MCA_BYTES[at+5] << 8 \
| MCA_BYTES[at+6] << 16 | MCA_BYTES[at+7] << 24) << 32) ))
return 0
}
# _mca_appimage_offset <file>
# Where the appended image starts, in MCA_APPIMAGE_AT: directly after the ELF,
# which is where its section header table ends. This is how the runtime works
# it out for --appimage-offset, and it is exact.
#
# Searching for the squashfs magic instead would not be: the runtime carries a
# copy of it in its own code. Neither is the AppImage magic at byte 8 of the
# ELF header a way in - it is there to be zeroed, and a build pipeline that
# does not want its image picked up by a desktop integration daemon does
# exactly that. So the offset is computed for any ELF at all and the caller
# finds out whether there is an image at it.
MCA_APPIMAGE_AT=0
_mca_appimage_offset() {
local shoff shentsize shnum
MCA_APPIMAGE_AT=0
_mca_bytes "$1" 0 64 || return 1
# \x7fELF, and then the class byte: 2 for the 64-bit header, 1 for the
# 32-bit one, which puts every field after it somewhere else.
(( MCA_BYTES[0] == 127 && MCA_BYTES[1] == 69 \
&& MCA_BYTES[2] == 76 && MCA_BYTES[3] == 70 )) || return 1
if (( MCA_BYTES[4] == 2 )); then
_mca_word 40 || return 1
shoff="$MCA_WORD"
shentsize=$(( MCA_BYTES[58] | MCA_BYTES[59] << 8 ))
shnum=$(( MCA_BYTES[60] | MCA_BYTES[61] << 8 ))
elif (( MCA_BYTES[4] == 1 )); then
shoff=$(( MCA_BYTES[32] | MCA_BYTES[33] << 8 \
| MCA_BYTES[34] << 16 | MCA_BYTES[35] << 24 ))
shentsize=$(( MCA_BYTES[46] | MCA_BYTES[47] << 8 ))
shnum=$(( MCA_BYTES[48] | MCA_BYTES[49] << 8 ))
else
return 1
fi
(( shoff > 0 && shentsize > 0 && shnum > 0 )) || return 1
MCA_APPIMAGE_AT=$(( shoff + shentsize * shnum ))
return 0
}
# _mca_inflate <compressor> <file> <offset> <length>
# One squashfs metadata block, decompressed onto stdout.
#
# lzo and lz4 are missing because squashfs stores them as bare blocks and
# neither lzop nor the lz4 tool will read one without the framing their own
# file format puts around it. An image compressed with either comes out as
# "cannot tell", which is where it started.
_mca_inflate() {
local comp="$1" file="$2" off="$3" len="$4"
case "$comp" in
1)
# squashfs stores a zlib stream and gzip only reads its own
# container, but underneath both are the same deflate data with a
# different wrapper around it - so the wrapper is swapped: zlib's
# two header bytes are dropped and a minimal gzip header put in
# front. gzip then writes every byte of the block and complains
# about the trailer it did not get, which is why its status is
# thrown away here and the check is on the output instead.
(( len > 2 )) || return 1
{
printf '\037\213\010\000\000\000\000\000\000\003'
_mca_slice "$file" $(( off + 2 )) $(( len - 2 ))
} | { gzip -dc 2>/dev/null || true; }
;;
2)
mca_have xz || return 1
_mca_slice "$file" "$off" "$len" \
| { xz -dc --format=lzma 2>/dev/null || true; }
;;
4)
mca_have xz || return 1
_mca_slice "$file" "$off" "$len" | { xz -dc 2>/dev/null || true; }
;;
6)
mca_have zstd || return 1
_mca_slice "$file" "$off" "$len" | { zstd -dc 2>/dev/null || true; }
;;
*) return 1 ;;
esac
}
# _mca_squashfs_names <file> <offset>
# The directory table of the image at that offset, inflated onto stdout. It is
# not parsed: the names sit in it as plain text between the records that
# describe them, and a name is all the caller is looking for.
#
# Fails unless the walk lands exactly on the end of the table. That is the
# integrity check - the block sizes adding up to the table's own length is
# what says the fields were read from a real superblock and that the output is
# the whole of the names rather than some of them - and it is why the caller
# has to collect this before trusting it, never pipe it.
_mca_squashfs_names() {
local file="$1" base="$2"
local comp dir_start end field header size pos
_mca_bytes "$file" "$base" 96 || return 1
comp=$(( MCA_BYTES[20] | MCA_BYTES[21] << 8 ))
_mca_word 72 || return 1
dir_start="$MCA_WORD"
(( dir_start > 0 )) || return 1
# Where the names stop: the first table squashfs writes after them. Which
# one that is depends on the image - there is no fragment table when
# nothing was packed into a fragment, and no export table unless it was
# asked for - so they are tried in the order they are written and the
# first one that is actually there wins. bytes_used closes the list for an
# image that has none of them.
end=0
for field in 80 88 48 40; do
_mca_word "$field" || continue
(( MCA_WORD > dir_start )) || continue
end="$MCA_WORD"
break
done
(( end > dir_start )) || return 1
# Names for a hundred thousand files would still fit in a fraction of
# this. A table that claims more than it is a table that was misread.
(( end - dir_start > 8388608 )) && return 1
pos=$(( base + dir_start ))
end=$(( base + end ))
while (( pos < end )); do
_mca_bytes "$file" "$pos" 2 || return 1
header=$(( MCA_BYTES[0] | MCA_BYTES[1] << 8 ))
size=$(( header & 0x7fff ))
(( size > 0 && pos + 2 + size <= end )) || return 1
# The top bit says the block was stored as it is, which squashfs does
# for the ones compression made no smaller.
if (( header & 0x8000 )); then
_mca_slice "$file" $(( pos + 2 )) "$size" || return 1
else
_mca_inflate "$comp" "$file" $(( pos + 2 )) "$size" || return 1
fi
pos=$(( pos + 2 + size ))
done
(( pos == end ))
}
# _mca_appimage_verdict <file>
# What the image appended to this file contains, left in MCA_APPIMAGE:
# yes - Chromium, CEF or Electron
# no - the names of everything inside were read and none of them is
# unknown - there is an image, but its contents could not be read
# none - not an AppImage; there is nothing appended to this ELF
MCA_APPIMAGE=none
_mca_appimage_verdict() {
local file="$1" base names m
local -a args=()
MCA_APPIMAGE=none
_mca_appimage_offset "$file" || return 0
base="$MCA_APPIMAGE_AT"
# hsqs, the squashfs magic. Nothing there means nothing was appended, so
# this is an ordinary executable and not an AppImage at all.
_mca_bytes "$file" "$base" 4 || return 0
(( MCA_BYTES[0] == 104 && MCA_BYTES[1] == 115 \
&& MCA_BYTES[2] == 113 && MCA_BYTES[3] == 115 )) || return 0
# From here on there is an image, so the worst this can end on is "cannot
# tell" - never "no", which would be an answer about contents that were
# never read.
MCA_APPIMAGE=unknown
names="$(mktemp "${TMPDIR:-/tmp}/mca-names.XXXXXX")" || return 0
# Nothing at all in the table means every block failed to inflate - a
# compressor whose tool is not installed - which is a "cannot tell" too.
if _mca_squashfs_names "$file" "$base" > "$names" 2>/dev/null \
&& [[ -s $names ]]
then
for m in "${MCA_MARKERS_STRICT[@]}"; do args+=(-e "$m"); done
if grep -qaF "${args[@]}" -- "$names"; then
MCA_APPIMAGE=yes
else
MCA_APPIMAGE=no
fi
fi
rm -f -- "$names"
return 0
}
# mca_detect_verdict <program>
# What running this program starts, left in MCA_VERDICT:
# yes - Chromium, CEF or Electron
# no - something else
# unknown - an image whose payload could not be read, so neither answer has
# been earned and the applications screen offers it as a choice
#
# Assigns rather than returns three states through an exit code, and is the one
# place the memo and the on-disk cache are consulted.
MCA_VERDICT=''
mca_detect_verdict() {
local prog="$1" real stamp cached
MCA_VERDICT=no
[[ -n $prog && -e $prog ]] || return 0
# Memoized under the path as given, so the same launcher named twice in a # Memoized under the path as given, so the same launcher named twice in a
# scan costs nothing at all the second time. # scan costs nothing at all the second time.
if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then
[[ ${MCA_DETECT_MEMO[$prog]} == yes ]] MCA_VERDICT="${MCA_DETECT_MEMO[$prog]}"
return $? return 0
fi fi
_mca_cache_load _mca_cache_load
@@ -546,26 +873,45 @@ mca_is_chromium() {
if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then
cached="${MCA_DETECT_CACHE[$prog]}" cached="${MCA_DETECT_CACHE[$prog]}"
if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then
verdict="${cached#*$'\t'}" MCA_VERDICT="${cached#*$'\t'}"
MCA_DETECT_MEMO[$prog]="$verdict" MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
[[ $verdict == yes ]] return 0
return $?
fi fi
fi fi
real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog" real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog"
verdict=no MCA_DETECT_UNSURE=0
if _mca_detect_uncached "$real"; then verdict=yes; fi if _mca_detect_uncached "$real"; then
MCA_VERDICT=yes
elif (( MCA_DETECT_UNSURE )); then
MCA_VERDICT=unknown
else
MCA_VERDICT=no
fi
MCA_DETECT_MEMO[$prog]="$verdict" MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
if [[ -n $stamp ]]; then if [[ -n $stamp ]]; then
MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$verdict" MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$MCA_VERDICT"
MCA_CACHE_DIRTY=1 MCA_CACHE_DIRTY=1
fi fi
[[ $verdict == yes ]] return 0
} }
# mca_is_chromium <program>
# Succeeds when the program is a Chromium, CEF or Electron process. "Cannot
# tell" is not that, so it fails here - anything that has to treat the two
# differently asks mca_detect_verdict instead.
mca_is_chromium() {
mca_detect_verdict "$1"
[[ $MCA_VERDICT == yes ]]
}
# Set by _mca_detect_uncached when it reaches the end without finding anything
# and the reason is that something could not be read, rather than that there
# was nothing there. Only mca_detect_verdict reads it, straight after the call.
MCA_DETECT_UNSURE=0
_mca_detect_uncached() { _mca_detect_uncached() {
local real="$1" depth="${2:-0}" dir target local real="$1" depth="${2:-0}" dir target
@@ -590,15 +936,28 @@ _mca_detect_uncached() {
_mca_has_markers "$dir" && return 0 _mca_has_markers "$dir" && return 0
[[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0 [[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0
# An AppImage keeps all of that inside a filesystem appended to itself, so
# there is nothing next to the binary to find - but the names of everything
# in there can be read, and that settles it either way.
_mca_appimage_verdict "$real"
case "$MCA_APPIMAGE" in
yes) return 0 ;;
no) return 1 ;;
esac
# Last resort: Chromium's own argument table is in the binary. -m1 stops at # Last resort: Chromium's own argument table is in the binary. -m1 stops at
# the first hit, so this reads far less than the file size suggests. # the first hit, so this reads far less than the file size suggests.
#
# AppImages are the one thing this cannot see through - their payload is a
# compressed filesystem - which is why they come out as unknown and are
# left to the applications screen.
grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0 grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0
grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0 grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0
# What is left is an image that could not be read - an unsupported
# compressor - or the older AppImage layout, which is an ISO9660 filesystem
# and has no name table of this shape at all. Either way the answer is not
# "no", it is "nobody looked".
if [[ $MCA_APPIMAGE == unknown || $real == *.AppImage || $real == *.appimage ]]; then
MCA_DETECT_UNSURE=1
fi
return 1 return 1
} }
@@ -717,10 +1076,12 @@ mca_scan() {
# Steam is Chromium inside, but nothing about it can be changed # Steam is Chromium inside, but nothing about it can be changed
# from a command line argument; it has its own module. # from a command line argument; it has its own module.
kind=steam kind=steam
elif mca_is_chromium "$prog"; then else
(( c_browser[i] )) && kind=browser || kind=app mca_detect_verdict "$prog"
elif [[ $prog == *.AppImage || $prog == *.appimage ]]; then case "$MCA_VERDICT" in
kind=unknown yes) (( c_browser[i] )) && kind=browser || kind=app ;;
unknown) kind=unknown ;;
esac
fi fi
[[ $kind == no ]] && continue [[ $kind == no ]] && continue
@@ -813,13 +1174,7 @@ mca_snap_is_chromium() {
for d in "${MCA_SNAP_DIRS[@]}"; do for d in "${MCA_SNAP_DIRS[@]}"; do
root="$d/$name/current" root="$d/$name/current"
[[ -d $root ]] || continue if _mca_find_markers "$root" 5; then
if [[ -n "$(find "$root" -maxdepth 5 \
\( -name 'chrome_crashpad_handler' -o -name 'app.asar' \
-o -name 'libcef.so' -o -name 'v8_context_snapshot.bin' \
-o -name 'chrome-sandbox' \) \
-print -quit 2>/dev/null)" ]]
then
MCA_DETECT_MEMO[snap:$name]=yes MCA_DETECT_MEMO[snap:$name]=yes
return 0 return 0
fi fi
@@ -842,12 +1197,7 @@ mca_flatpak_is_chromium() {
fi fi
loc="$(flatpak info --show-location "$id" 2>/dev/null)" loc="$(flatpak info --show-location "$id" 2>/dev/null)"
if [[ -n $loc && -d "$loc/files" ]] \ if [[ -n $loc ]] && _mca_find_markers "$loc/files" 4; then
&& [[ -n "$(find "$loc/files" -maxdepth 4 \
\( -name 'chrome_crashpad_handler' -o -name 'app.asar' \
-o -name 'libcef.so' -o -name 'v8_context_snapshot.bin' \) \
-print -quit 2>/dev/null)" ]]
then
MCA_DETECT_MEMO[flatpak:$id]=yes MCA_DETECT_MEMO[flatpak:$id]=yes
return 0 return 0
fi fi