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.
This commit is contained in:
1 parent
449a20ce9b
commit
7a446364dc
3 files changed
+434
-43
No files matched your search
@@ -117,6 +117,13 @@ 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 | |
|
||||||
|
|||||||
@@ -201,11 +201,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 +252,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 +268,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)
|
||||||
|
|||||||
+388
-38
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user