Merge pull request #2652 from fastfetch-cli/dev

Release v2.70.0
This commit is contained in:
Carter Li
2026-10-14 10:42:20 +08:00
committed by GitHub
367 changed files with 11110 additions and 2566 deletions
-5
View File
@@ -1,5 +0,0 @@
[codespell]
check-filenames =
builtin = clear,rare,usage,informal
skip = */.git,*/cmake-build-*,*/.idea,*/completions,*/presets,*/screenshots,*/tests,*/3rdparty,*/logo/ascii,./src/detection/gpu/asahi_drm.h,./src/detection/cpu/cpu_x86.c,./src/detection/gpu/gpu_bsddrm.c,./src/detection/displayserver/linux/wayland/*-protocol.*,./build,./build-*,./build_*,./src/common/impl/genconfig.c
ignore-words-list = iterm,compiletime,unknwn,pengwin,siduction,master,slave,sur,doas,conexant,ags,bu,renderD,SME,Carmel,kinda,symbole,connexion,comando,processus,lokale,adres,titel,som,environnement,Sistem,Lokal,Lokal,Sistem,Paket,Hava,statics,IIterator,whitelist
+13
View File
@@ -1,2 +1,15 @@
# Normalise text files to LF in the repository and check them out with LF, so a CRLF
# never enters an object and never shows up as a whole-file diff on Windows.
* text=auto eol=lf
# The vendored AMD Display Library SDK ships with CRLF endings of its own. Leaving it
# alone keeps `Dep (3rdparty): upgrades display-library` from carrying a 7000-line
# whitespace diff, and a re-vendor will not fight the rule above.
src/3rdparty/display-library/** -text
# The logo fixtures store the exact bytes fastfetch prints, ANSI escapes included.
# They are demo data rather than text, and are not read by any test.
*.fflogo -text
*.h linguist-language=c
*.c linguist-language=c
+163
View File
@@ -0,0 +1,163 @@
name: Reusable Android
on:
workflow_call:
inputs:
abi:
required: true
type: string
permissions:
contents: read
env:
CMAKE_BUILD_TYPE: ${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }}
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: uname -a
run: uname -a
# The NDK comes with the runner image, so nothing is downloaded here. The image is pinned to
# ubuntu-24.04 rather than ubuntu-latest because which NDK a build used is part of what it is.
#
# The image's *latest* NDK, not its default one: the default is r27d (clang 18), while fastfetch
# is built and shipped on Termux with clang 21. Testing three majors behind the compiler the
# supported path uses buys nothing and rejects code clang 21 accepts.
- name: print NDK version
run: |
echo "default: ${ANDROID_NDK_HOME}"
echo "latest: ${ANDROID_NDK_LATEST_HOME}"
test -f "${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake"
cat "${ANDROID_NDK_LATEST_HOME}/source.properties"
"${ANDROID_NDK_LATEST_HOME}"/toolchains/llvm/prebuilt/*/bin/clang --version
# freetype is the one library this build wants that the NDK does not ship: its headers are not
# in the sysroot and the runner image has no libfreetype-dev, so without this step
# ff_lib_enable(FREETYPE ...) prints "Library: missing: FREETYPE" and
# src/detection/terminalfont/terminalfont_android.c compiles only its #else branch. That is the
# branch the shipped Termux build never takes, so the freetype code would rot untested here.
#
# Handing it the host's copy is sound, for two reasons. fastfetch never links freetype --
# BINARY_LINK_TYPE is dlopen, so ff_lib_enable skips its target_link_libraries and only the
# include path and FF_HAVE_FREETYPE are needed. And freetype's public headers carry no host
# assumptions: FT_SIZEOF_LONG and FT_SIZEOF_INT in freetype/config/integer-types.h are derived
# from FT_ULONG_MAX/FT_UINT_MAX, which come from the *target's* <limits.h>, so the same copy
# describes FT_FaceRec -- and the offset of the family_name that terminalfont_android.c reads --
# correctly for aarch64, armv7 and x86_64 alike.
#
# pkg-config is the only route that can find it: the NDK toolchain leaves PKG_CONFIG_PATH,
# PKG_CONFIG_LIBDIR and PKG_CONFIG_SYSROOT_DIR untouched, so the ordinary host search path is
# what gets used.
#
# The .pc is written here rather than taken from libfreetype-dev, whose own copy carries
# `Requires.private: zlib, bzip2, libpng, harfbuzz, libbrotlidec`. pkgconf -- which is what
# `pkg-config` has been on Ubuntu since 22.10 -- fails outright on a private requirement it
# cannot resolve, even for --exists, and libfreetype-dev only pulls in libpng-dev and
# zlib1g-dev. Nothing is linked, so the include directory is the whole contract. A Version
# field is mandatory (pkgconf treats a .pc without one as not found), hence the sed.
- name: install freetype headers
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libfreetype-dev
test -f /usr/include/freetype2/ft2build.h
VERSION=$(awk '/^#define FREETYPE_(MAJOR|MINOR|PATCH)/ {printf "%s%s", sep, $3; sep="."}' /usr/include/freetype2/freetype/freetype.h)
test -n "${VERSION}"
mkdir -p freetype-pc
cat > freetype-pc/freetype2.pc <<'EOF'
prefix=/usr
includedir=${prefix}/include
Name: FreeType 2
Description: A free, high-quality, and portable font engine.
Version: 0
Cflags: -I${includedir}/freetype2
EOF
sed -i "s/^Version: .*/Version: ${VERSION}/" freetype-pc/freetype2.pc
PKG_CONFIG_PATH="$PWD/freetype-pc" pkg-config --cflags freetype2
# android-24 (Android 7.0) is Termux's own floor, and it is the floor for a second, independent
# reason: libcamera2ndk.so only exists from API 24, fastfetch links it unconditionally, and the
# NDK has no such thing as a weak *library* reference -- a build aimed lower would fail to load
# on exactly the devices it was aimed at. The level is also what makes this job check anything:
# at 24 the NDK marks AImageDecoder_* / AndroidBitmap_compress (30) and AMediaCodec_getName /
# AMediaCodec_releaseName (28) unavailable, which is what CMakeLists.txt's
# -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability pair is built on.
# Building against a newer platform would quietly stop enforcing it.
#
# BUILD_TESTS stays off. The test binaries are host programs, there is no device here to run
# them on, and `ctest` therefore cannot follow -- unlike every other job in this workflow set.
# The platform branch of the build is what this job covers.
- name: configure project
env:
# Picks up the freetype2.pc written by the step above. The rest of the search path is left
# alone, so zlib and every other host .pc file is still found exactly as before.
PKG_CONFIG_PATH: ${{ github.workspace }}/freetype-pc
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=Off -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake" -DANDROID_ABI=${{ inputs.abi }} -DANDROID_PLATFORM=android-24 -DCMAKE_INSTALL_PREFIX=/data/data/com.termux/files/usr .
# A silent regression back to "Library: missing: FREETYPE" would look exactly like a passing
# build -- the module would simply stop being compiled, and the only trace would be a line in
# the configure log nobody diffs. Assert that the define actually reached the compile line.
- name: check freetype was detected
run: |
grep -q -- '-DFF_HAVE_FREETYPE=1' CMakeFiles/libfastfetch.dir/flags.make || {
echo "::error::freetype was not detected, so terminalfont_android.c compiled its #else branch only"
grep -o -- '-DFF_HAVE_[A-Z0-9_]*' CMakeFiles/libfastfetch.dir/flags.make | sort -u
exit 1
}
- name: build project
run: cmake --build . --target package --verbose -j4
- name: print ELF header
run: readelf --wide -h fastfetch
- name: print dynamic dependencies
run: readelf --wide -d fastfetch
# libjnigraphics.so, libmediandk.so and libcamera2ndk.so are what provide the entry points
# CMakeLists.txt weakens. A weak reference resolves to null instead of failing the load, and
# FF_ANDROID_API_AT_LEAST() reads the platform level rather than the symbol -- so a library
# dropped from the link line still passes the guard on a device new enough, and the call goes
# through a null pointer. DT_NEEDED is the only place that mistake is visible.
- name: check linked libraries
run: |
for lib in libjnigraphics.so libmediandk.so libcamera2ndk.so; do
readelf --wide -d fastfetch | grep -q "NEEDED.*\[${lib}\]" || { echo "::error::${lib} is not in DT_NEEDED"; exit 1; }
done
# Every reference to an entry point the NDK marks unavailable at API 24 has to be weak: a strong
# one turns a device below API 28/30 into a load failure instead of a graceful downgrade, which
# is the entire reason those three files are compiled with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__.
# `nm -D` prints a weak undefined symbol as `w` and a strong undefined one as `U`.
#
# The families are whole prefixes on purpose: every AImageDecoder* entry point is API 30 or 31,
# but the frame-info and header-info ones spell the prefix AImageDecoderFrameInfo / HeaderInfo
# without an underscore, and matching only `AImageDecoder_` would leave them unchecked. The
# AMediaCodec ones are named one by one because the rest of that API -- createDecoderByType,
# createEncoderByType, delete -- is API 21 and is correctly a strong reference.
- name: check weak references
run: |
nm -D fastfetch | grep -E ' (AImageDecoder|AndroidBitmap_compress|AMediaCodec_getName$|AMediaCodec_releaseName$)' > weak.txt
cat weak.txt
test -s weak.txt || { echo "::error::none of these entry points is referenced any more -- if the last use is gone on purpose, this check and the -Werror=unguarded-availability pair it guards should go with it"; exit 1; }
if grep -qv '^ *w ' weak.txt; then
echo "::error::expected every one of these to be a weak reference:"
grep -v '^ *w ' weak.txt
exit 1
fi
- name: print size
run: |
file fastfetch
ls -l fastfetch
- name: upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fastfetch-android-${{ inputs.abi }}
path: ./fastfetch-*.*
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
run: cp .github/benchmark-index.html "$RUNNER_TEMP/benchmark-index.html"
- name: store benchmark result
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
uses: benchmark-action/github-action-benchmark@4322e5726e6334590d251fc4f92bec0efafc45dc # v1.22.2
with:
name: Fastfetch
tool: customSmallerIsBetter
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
sudo pkg install -y llvm cmake git pkgconf binutils wayland vulkan-headers vulkan-loader libxcb libXrandr libX11 libdrm glib dconf dbus sqlite3-tcl egl opencl ocl-icd v4l_compat chafa libelf lua54 libva libvdpau
- name: CMake configuration
run: env CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
run: env CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: CMake build
run: cmake --build . --target package --verbose -j4
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
sudo pkg install -y cmake git pkgconf binutils wayland vulkan-headers vulkan-loader libxcb libXrandr libX11 libdrm glib dconf dbus sqlite3 egl opencl ocl-icd v4l_compat chafa lua54 libva libvdpau
- name: CMake configuration
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: CMake build
run: cmake --build . --target package --verbose -j4
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: run VM
uses: uraimo/run-on-arch-action@460cb8e6d9f726a588fc9b5e681c8a6cab09ae41 # v3.2.0
uses: uraimo/run-on-arch-action@fa1f3e7de95534497266c7265950b5188355fbfb # v3.2.1
id: runcmd
with:
arch: armv7
@@ -30,8 +30,8 @@ jobs:
curl -L https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
echo -e 'Acquire::https::Verify-Peer "false";\nAcquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99ignore-certificates
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
cmake --build . --target package --verbose -j4
./fastfetch --list-features
time ./fastfetch -c presets/ci.jsonc --stat false
+4 -4
View File
@@ -43,7 +43,7 @@ jobs:
run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
- name: install required packages
run: sudo apt-get update && sudo apt-get install -y gcc-13 jq libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libddcutil-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
run: sudo apt-get update && sudo apt-get install -y gcc-13 jq libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
- name: install linuxbrew packages
run: |
@@ -52,19 +52,19 @@ jobs:
- name: Initialize CodeQL
if: inputs.arch == 'amd64'
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: c
- name: configure project
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr .
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: build project
run: cmake --build . --target package --verbose -j4
- name: perform CodeQL analysis
if: inputs.arch == 'amd64'
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
- name: list features
run: ./fastfetch --list-features
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
- name: install required packages
run: sudo apt-get update && sudo apt-get install -y gcc-13 gcc-13-multilib libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libddcutil-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
run: sudo apt-get update && sudo apt-get install -y gcc-13 gcc-13-multilib libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
- name: install linuxbrew packages
run: |
@@ -40,7 +40,7 @@ jobs:
run: cmake --version
- name: configure project
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -DCMAKE_C_FLAGS="-m32 -march=i686 -mtune=i686" -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_VERSION=5.10.0 -DCMAKE_SYSTEM_PROCESSOR=i686 -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=i386 -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr .
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -DCMAKE_C_FLAGS="-m32 -march=i686 -mtune=i686 -fno-pie" -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_VERSION=5.10.0 -DCMAKE_SYSTEM_PROCESSOR=i686 -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=i386 -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: build project
run: cmake --build . --target package --verbose -j4
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: set up QEMU for loong64
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- name: build in loong64 container
run: |
@@ -37,13 +37,13 @@ jobs:
libdconf-dev libdbus-1-dev libmagickcore-dev \
libsqlite3-dev librpm-dev \
libegl-dev libglx-dev ocl-icd-opencl-dev \
libpulse-dev libdrm-dev \
libdrm-dev \
libelf-dev libefl-all-dev \
liblua5.4-dev \
libvdpau-dev libva-dev \
rpm
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
cmake --build . --target package --verbose -j$(nproc)
./fastfetch --list-features
time ./fastfetch -c presets/ci.jsonc --stat false
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: run VM
uses: uraimo/run-on-arch-action@460cb8e6d9f726a588fc9b5e681c8a6cab09ae41 # v3.2.0
uses: uraimo/run-on-arch-action@fa1f3e7de95534497266c7265950b5188355fbfb # v3.2.1
id: runcmd
with:
arch: ${{ inputs.arch }}
@@ -31,8 +31,8 @@ jobs:
uname -a
apt-get update && apt-get install -y software-properties-common
add-apt-repository -y ppa:ubuntu-toolchain-r/test
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libchafa-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libchafa-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
cmake --build . --target package --verbose -j4
./fastfetch --list-features
time ./fastfetch -c presets/ci.jsonc --stat false
+7 -1
View File
@@ -29,7 +29,13 @@ jobs:
- name: install required packages with MacPorts # Homebrew dropped Intel support
if: inputs.arch == 'amd64'
run: |
curl -fsSL https://github.com/macports/macports-base/releases/download/v2.12.6/MacPorts-2.12.6-15-Sequoia.pkg -o "$RUNNER_TEMP/MacPorts.pkg"
# resolve the latest MacPorts release; "15-Sequoia" must keep matching the runner image (macos-15-intel, set in ci.yml)
macports_tag=$(curl -fsSL -H "Authorization: Bearer ${{ github.token }}" \
https://api.github.com/repos/macports/macports-base/releases/latest \
| sed -n 's/^[[:space:]]*"tag_name": "\(v[0-9][^"]*\)".*/\1/p')
macports_version=${macports_tag#v}
echo "Installing MacPorts $macports_version"
curl -fsSL "https://github.com/macports/macports-base/releases/download/${macports_tag}/MacPorts-${macports_version}-15-Sequoia.pkg" -o "$RUNNER_TEMP/MacPorts.pkg"
sudo installer -pkg "$RUNNER_TEMP/MacPorts.pkg" -target /
echo /opt/local/bin >> "$GITHUB_PATH"
echo /opt/local/sbin >> "$GITHUB_PATH"
+2 -2
View File
@@ -24,12 +24,12 @@ jobs:
run: |
cat /etc/alpine-release
uname -a
apk add cmake samurai vulkan-loader-dev libxcb-dev libxrandr-dev rpm-dev wayland-dev libdrm-dev dconf-dev imagemagick-dev chafa-dev zlib-dev dbus-dev mesa-dev opencl-dev sqlite-dev networkmanager-dev pulseaudio-dev ddcutil-dev elfutils-dev lua5.4-dev quickjs-ng-dev libvdpau-dev libva-dev gcc g++
apk add cmake samurai vulkan-loader-dev libxcb-dev libxrandr-dev rpm-dev wayland-dev libdrm-dev dconf-dev imagemagick-dev chafa-dev zlib-dev dbus-dev mesa-dev opencl-dev sqlite-dev networkmanager-dev elfutils-dev lua5.4-dev quickjs-ng-dev libvdpau-dev libva-dev gcc g++
shell: alpine.sh --root {0}
- name: build
run: |
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DIS_MUSL=ON -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -GNinja .
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DIS_MUSL=ON -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" -GNinja .
cmake --build . --target package --verbose -j4
shell: alpine.sh {0}
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
sudo pkgin -y install clang cmake git pkgconf wayland vulkan-headers dconf dbus sqlite3 ImageMagick libva libvdpau
- name: CMake configuration
run: CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
run: CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: CMake build
run: cmake --build . --target package --verbose -j4
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
run: uname -a
- name: configure project
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_PULSE=OFF -DENABLE_DDCUTIL=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF -DENABLE_IMAGE_LOGO=OFF
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF -DENABLE_IMAGE_LOGO=OFF
- name: build project
run: cmake --build . --target package --verbose -j4
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
sudo pkg install gcc14 cmake git pkg-config glib2 dbus sqlite-3 imagemagick ninja
- name: CMake configuration
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: CMake build
run: cmake --build . --verbose -j4
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
sudo pkg_add -r llvm-21.1.8p4 cmake git wayland vulkan-headers vulkan-loader glib2 dconf dbus sqlite3 imagemagick chafa lua-5.4.8p0 # pkg-config is preinstalled
- name: CMake configuration
run: CC=clang-21 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
run: CC=clang-21 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
- name: CMake build
run: cmake --build . --target package --verbose -j4
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: run VM
uses: vmactions/solaris-vm@a11f407d3720a3717673d4194f19ee63b284977e # v1.4.0
uses: vmactions/solaris-vm@87d3c436511cef802cd1637f86f73b1a97715c8c # v1.4.1
with:
usesh: true
envs: 'CMAKE_BUILD_TYPE'
@@ -28,7 +28,7 @@ jobs:
run: |
export PKG_CONFIG_PATH=/usr/lib/64/pkgconfig
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja .
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
cmake --build . --verbose -j4
./fastfetch --list-features
time ./fastfetch -c presets/ci.jsonc --stat false
-24
View File
@@ -1,24 +0,0 @@
name: Reusable Spellcheck
on:
workflow_call:
permissions:
contents: read
jobs:
spellcheck:
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install codespell
shell: bash
run: |
pip3 install codespell
codespell --version
- name: Run Spellchecker
run: codespell
+3 -3
View File
@@ -45,11 +45,11 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-msys2
uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0
uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
with:
msystem: ${{ inputs.msystem }}
update: true
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-imagemagick mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-quickjs-ng mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-imagemagick mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers
- name: print msys version
run: uname -a
@@ -61,7 +61,7 @@ jobs:
run: cmake --build . --verbose -j4
- name: copy necessary dlls
run: cp /${{ inputs.msystem-lower }}/bin/{lua55,libqjs-0}.dll .
run: cp /${{ inputs.msystem-lower }}/bin/lua55.dll .
- name: list features
run: ./fastfetch --list-features
+16 -6
View File
@@ -5,13 +5,7 @@ on:
- pull_request
jobs:
spellcheck:
permissions:
contents: read
uses: ./.github/workflows/build-spellcheck.yml
no-features-test:
needs: spellcheck
name: No-features-test
permissions:
contents: read
@@ -78,6 +72,21 @@ jobs:
contents: read
uses: ./.github/workflows/build-musl-amd64.yml
android:
needs: no-features-test
name: Android-${{ matrix.abi }}
permissions:
contents: read
strategy:
matrix:
abi:
- arm64-v8a
- armeabi-v7a
- x86_64
uses: ./.github/workflows/build-android.yml
with:
abi: ${{ matrix.abi }}
macos-hosts:
needs: no-features-test
name: macOS-${{ matrix.arch }}
@@ -196,6 +205,7 @@ jobs:
- linux-loong64
- linux-vms
- musl-amd64
- android
- macos-hosts
- freebsd-amd64
- openbsd-amd64
+47
View File
@@ -1,3 +1,50 @@
# 2.70.0
Changes:
* LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits.
* Interfaces that carry no unicast address are no longer reported, since both the addresses and the interface properties are now enumerated from the unicast address table. (LocalIP, Windows)
* libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package.
* QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General)
* The Command module option `useStdErr` has been replaced with `useOutput`, which selects which of the command's output streams to capture: `"stdout"`, `"stderr"` or `"both"`.
* It defaults to `"both"`, so a command that logs to stderr now has that output captured instead of discarded.
* Set it to `"stdout"` or `"stderr"` to restore the old single-stream behavior. (Command)
* The Shell module now reports the shell from the password database (`/etc/passwd`) instead of the `$SHELL` environment variable, which may differ when the login shell was changed after the session started. (Title)
* The Disk custom format no longer provides `{size-available}`. `{size-free}` now reports whichever of the two the `disk.useAvailable` option selects, and the raw byte counts are available as `{bytes-used}` and `{bytes-total}`. (Disk)
* This also means `{size-free}` now changes with `disk.useAvailable`; it used to always report the file system's free size.
* The Disk JSON output no longer includes `bytes.available` and `bytes.used`. (Disk)
* `bytes.free` and `bytes.total` are unchanged; the used size can be derived as `total - free`.
Features:
* Added Sound detection on Android. (Sound, Android)
* Added display detection on Android over binder, which needs neither a child process nor a permission. (Display, Android)
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
* Added media detection on Android when fastfetch runs as an app UID. (Media, Android)
* Requires `Notification access` permission
* Added app display-name detection on Android, so the media module names the player the way the app names itself instead of by its package name, and the terminal module reports the label Termux is installed under. (Media, Terminal, Android)
* Performance improvements & internal cleanups
* Added Finnish language support. (General)
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
* Added an ALSA fallback to Sound on Linux, so a machine that runs no sound server reports its output device and volume instead of an error. (Sound, Linux)
Bugfixes:
* Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash)
* Fixed external monitors detected via DDC/CI being randomly reported as built-in on Linux. (#2615, Brightness, Linux)
* Fixed the Vulkan driver name being concatenated with the name of a previously enumerated device on multi-GPU systems. (#2619, Vulkan)
* Fixed the foot terminal font size not being parsed when written as `Font Name-size`, e.g. `Berkeley Mono-12`. (#2624, TerminalFont, Linux)
* Fixed `/boot` being hidden by default on Haiku, where it is the system volume. (Disk, Haiku)
* Improved Snapdragon X1/X2 model name detection on Linux. (#2625, CPU, Linux)
* Fixed the Android build failing with `GL/gl.h` not found, because the NDK ships the EGL headers that turned OpenGL detection on but not the desktop GL headers it includes. (OpenGL, Android)
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
* Fixed assertion failure when percentage exceeds 100. (Percent)
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
* Fixed Android display detection on devices running Android 17. (Display, Android)
* Fixed Windows volumes with a per-user quota (such as network shares) reporting a wrong total size and a wrapped-around used size, because the total size was quota-limited while the free size was not. (#2651, Disk, Windows)
* On Windows 10 1809 (build 17763) and later, the whole volume's capacity and free space are now reported by default. Set `disk.useAvailable` to `true` to keep reporting the space that is available to the current user, quota included.
Logos:
* Added filled Artix logo (#2631)
* Renamed PostmarketOS to nura (#2626)
# 2.69.0
Changes:
+121 -39
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23
project(fastfetch
VERSION 2.69.0
VERSION 2.70.0
LANGUAGES C
DESCRIPTION "Fast neofetch-like system information tool"
HOMEPAGE_URL "https://github.com/fastfetch-cli/fastfetch"
@@ -60,6 +60,7 @@ endif()
include(CheckIncludeFile)
include(CheckFunctionExists)
include(CheckSymbolExists)
if(LINUX)
CHECK_INCLUDE_FILE("linux/version.h" HAVE_LINUX_VERSION_H)
@@ -92,8 +93,6 @@ cmake_dependent_option(ENABLE_EGL "Enable egl" ON "LINUX OR FreeBSD OR OpenBSD O
cmake_dependent_option(ENABLE_GLX "Enable glx" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR GNU" OFF)
cmake_dependent_option(ENABLE_OPENCL "Enable opencl" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR WIN32 OR ANDROID OR SunOS OR Haiku OR GNU" OFF)
cmake_dependent_option(ENABLE_FREETYPE "Enable freetype" ON "ANDROID" OFF)
cmake_dependent_option(ENABLE_PULSE "Enable pulse" ON "LINUX OR ANDROID OR GNU" OFF)
cmake_dependent_option(ENABLE_DDCUTIL "Enable ddcutil" ON "LINUX" OFF)
cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR DragonFly OR Haiku OR GNU" OFF)
cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF)
@@ -123,7 +122,6 @@ option(ENABLE_EMBEDDED_PCIIDS "Embed pci.ids into fastfetch, requires `python`"
option(ENABLE_EMBEDDED_AMDGPUIDS "Embed amdgpu.ids into fastfetch, requires `python`" OFF)
option(ENABLE_WORDEXP "Enable using of wordexp(3) if available, instead of glob(3)" ON)
option(ENABLE_LUA "Enable lua scripting for format strings" ON)
option(ENABLE_QUICKJS "Enable quickjs scripting for format strings" OFF)
option(ENABLE_LIBZFS "Enable libzfs" ON)
option(ENABLE_WCWIDTH "Detect wide-width characters with wcwidth when calculating logo and separator width. Disable for a smaller and slightly faster build, but logos containing CJK/emoji may be measured too narrow and render incorrectly. Such usecases are uncommon." ON)
set(DEFAULT_STRUCTURE "Title:Separator:OS:Host:Kernel:Uptime:Packages:Shell:Display:DE:WM:WMTheme:Theme:Icons:Font:Cursor:Terminal:TerminalFont:CPU:GPU:Memory:Swap:Disk:LocalIp:Battery:PowerAdapter:Locale:Break:Colors" CACHE STRING "Default module structure for detection")
@@ -283,6 +281,22 @@ if(ENABLE_LTO AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
endif()
endif()
# Release and MinSizeRel drop the symbol table and the unwind metadata; Debug and RelWithDebInfo keep
# both, so those stay fully debuggable. CMake does not strip MinSizeRel on its own.
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
if(NOT APPLE) # ld: warning: -s is obsolete
# The symbol table is dead weight: 437 KB (16%) of a MinGW build, 91 KB (3%) of a GCC build.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-s")
endif()
# Drops .eh_frame / .eh_frame_hdr on ELF (74 KB) and .pdata / .xdata on PE (26 KB). Every C++
# translation unit here is built with -fno-exceptions -fno-rtti and no source file contains a
# single try / catch, so no exception can ever unwind through these frames. The only loss is
# backtrace() / gdb / perf / crash-dump stack walking in a release binary.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-asynchronous-unwind-tables")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-asynchronous-unwind-tables")
endif()
#######################
# Target FS structure #
#######################
@@ -440,7 +454,29 @@ fastfetch_encode_c_string("${DATATEXT_JSON_HELP}" DATATEXT_JSON_HELP)
configure_file(src/fastfetch_config.h.in fastfetch_config.h @ONLY)
configure_file(src/fastfetch_datatext.h.in fastfetch_datatext.h @ONLY)
if(APPLE)
# `mediaremoted` serves now-playing data only to clients whose bundle id starts
# with "com.apple.": -[MRDMediaRemoteClient isAllowedAccessToDataFromPlayerPath:]
# accepts the 0x200 entitlement bit either for the private
# com.apple.mediaremote.now-playing-read-access entitlement or for that prefix,
# and it derives the client's identity from the code signature / XPC audit token
# rather than from the process's own NSBundle. Both the embedded
# CFBundleIdentifier and the ad-hoc signing identifier below therefore have to
# carry the prefix; keep them in sync through these variables.
set(FASTFETCH_BUNDLE_ID "com.apple.fastfetch")
set(FLASHFETCH_BUNDLE_ID "com.apple.flashfetch")
# configure_file() inputs are not configure dependencies on their own, so
# without this an edit to Info.plist.in never even regenerates Info.plist.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/common/apple/Info.plist.in")
# Each executable gets its own plist: the file is embedded with -sectcreate and read
# back by the system as the bundle id, so two targets sharing one would leave one of
# them advertising an identifier that its code signature does not match.
set(BUNDLE_ID "${FASTFETCH_BUNDLE_ID}")
configure_file(src/common/apple/Info.plist.in Info.plist @ONLY)
if(BUILD_FLASHFETCH)
set(BUNDLE_ID "${FLASHFETCH_BUNDLE_ID}")
configure_file(src/common/apple/Info.plist.in Info.plist.flashfetch @ONLY)
endif()
unset(BUNDLE_ID)
endif()
####################
@@ -463,6 +499,7 @@ file(GENERATE OUTPUT logo_builtin.h CONTENT "${LOGO_BUILTIN_H}")
set(LIBFASTFETCH_SRC
src/common/impl/commandoption.c
src/common/impl/debug.c
src/common/impl/duration.c
src/common/impl/font.c
src/common/impl/format.c
@@ -492,6 +529,7 @@ set(LIBFASTFETCH_SRC
src/common/impl/FFPlatform.c
src/common/impl/smbios.c
src/common/impl/strutil.c
src/common/impl/zlib.c
src/detection/bluetoothradio/bluetoothradio.c
src/detection/bootmgr/bootmgr.c
src/detection/chassis/chassis.c
@@ -640,9 +678,11 @@ if(LINUX)
)
elseif(ANDROID)
list(APPEND LIBFASTFETCH_SRC
src/common/android/applabel.c
src/common/android/binder.c
src/common/android/dex.c
src/common/android/package.c
src/common/android/zip.c
src/common/impl/dbus.c
src/common/impl/io_unix.c
src/common/impl/netif_linux.c
@@ -707,6 +747,7 @@ elseif(ANDROID)
src/detection/packages/packages_nix.c
src/detection/poweradapter/poweradapter_nosupport.c
src/detection/processes/processes_linux.c
src/detection/sound/sound_android.c
src/detection/sound/sound_linux.c
src/detection/swap/swap_linux.c
src/detection/terminalfont/terminalfont_android.c
@@ -1035,6 +1076,7 @@ elseif(APPLE)
src/detection/libc/libc_apple.c
src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c
src/detection/localip/localip_apple.c
src/detection/gamepad/gamepad_apple.m
src/detection/media/media_apple.m
src/detection/memory/memory_apple.c
@@ -1094,10 +1136,10 @@ elseif(WIN32)
src/common/impl/processing_windows.c
src/common/impl/FFPlatform_windows.c
src/common/impl/binary_windows.c
src/common/impl/debug_windows.c
src/common/impl/kmod_windows.c
src/common/windows/getline.c
src/common/windows/com.c
src/common/windows/folders.c
src/common/windows/registry.c
src/common/windows/unicode.c
src/common/windows/variant.cpp
@@ -1533,11 +1575,15 @@ if(APPLE AND BINARY_LINK_TYPE STREQUAL "dlopen")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/opt/homebrew/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,/opt/local/lib")
endif()
# Termux installs the libraries fastfetch dlopens -- freetype among them -- under TARGET_DIR_ROOT, which
# is not on the Android linker's default search path (DT_RUNPATH, LD_LIBRARY_PATH and the default
# namespace). Termux's own clang injects -rpath=$PREFIX/lib into every binary it links, so a build made
# inside Termux finds them; a plain NDK build, which is what the CI does, does not.
if(ANDROID)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64")
else()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib")
endif()
endif()
@@ -1645,6 +1691,13 @@ if(FreeBSD OR OpenBSD OR NetBSD)
unset(CMAKE_REQUIRED_DEFINITIONS)
endif()
if(OpenBSD)
check_symbol_exists("getexecpath" "unistd.h" HAVE_GETEXECPATH)
if(HAVE_GETEXECPATH)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GETEXECPATH=1)
endif()
endif()
if(HAVE_PIPE2)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_PIPE2)
endif()
@@ -1720,9 +1773,18 @@ function(ff_lib_enable VARNAME PKGCONFIG_NAMES CMAKE_NAME) # [CMAKE_TARGET_NAME]
endif()
endif()
else()
set(${VARNAME}_INCLUDE_DIRS ${${CMAKE_NAME}_INCLUDE_DIRS})
set(${VARNAME}_LIBRARIES ${${CMAKE_NAME}_LIBRARIES})
set(${VARNAME}_CFLAGS_OTHER ${${CMAKE_NAME}_CFLAGS_OTHER})
# find_package modules disagree on how they name their result variables. Newer ones
# export <PackageName>_* (FindVulkan sets Vulkan_INCLUDE_DIRS), but older ones only
# set the upper-case form: FindFreetype sets FREETYPE_INCLUDE_DIRS and
# FREETYPE_LIBRARIES and has no Freetype_INCLUDE_DIRS at all. Assigning
# unconditionally would therefore wipe the include path the module had just
# provided, and the package would be "found" with nothing to compile against.
# Fill in only the gaps.
foreach(_FF_SUFFIX INCLUDE_DIRS LIBRARIES CFLAGS_OTHER)
if(NOT DEFINED ${VARNAME}_${_FF_SUFFIX} OR "${${VARNAME}_${_FF_SUFFIX}}" STREQUAL "")
set(${VARNAME}_${_FF_SUFFIX} ${${CMAKE_NAME}_${_FF_SUFFIX}})
endif()
endforeach()
endif()
endif()
endif()
@@ -1841,27 +1903,16 @@ ff_lib_enable(OPENCL
"OpenCL"
"OpenCL"
)
# `Freetype` is the module CMake ships (Modules/FindFreetype.cmake); there is no FindFreeType2, so
# this name never resolved and only pkg-config could ever turn freetype on.
ff_lib_enable(FREETYPE
"freetype2"
"FreeType2"
)
ff_lib_enable(PULSE
"libpulse"
"Pulse"
)
ff_lib_enable(DDCUTIL
"ddcutil"
"Ddcutil"
"Freetype"
)
ff_lib_enable(ELF
"libelf"
"libelf"
)
ff_lib_enable(QUICKJS
"qjs"
"qjs"
"qjs"
)
if(ENABLE_LUA)
# Lua is a special case since its CMake config does not follow the usual pattern
find_package(Lua QUIET)
@@ -1954,10 +2005,10 @@ elseif(APPLE)
PRIVATE "-framework SystemConfiguration"
PRIVATE "-framework VideoToolbox"
PRIVATE "-weak_framework CoreDisplay"
PRIVATE "-F /System/Library/PrivateFrameworks"
PRIVATE "-weak_framework DisplayServices"
PRIVATE "-weak_framework MediaRemote"
PRIVATE "-weak_framework SkyLight"
)
elseif(WIN32)
target_link_libraries(libfastfetch
@@ -2155,7 +2206,17 @@ if(NOT WIN32)
endif()
endif()
if(NOT ENABLE_WORDEXP)
message(STATUS "wordexp.h not found or disabled, glob used instead")
# wordexp.h is not the only thing that can be missing. bionic declares glob() and globfree()
# only from API 28, so an NDK build aimed at 24 has neither and a fallback that assumed glob
# would have nothing to call. The symbol is what has to be checked, not the header: <glob.h>
# is present at every level, it just declares nothing below 28.
check_symbol_exists("glob" "glob.h" HAVE_GLOB)
if(HAVE_GLOB)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GLOB=1)
message(STATUS "wordexp.h not found or disabled, glob used instead")
else()
message(STATUS "neither wordexp.h nor glob.h is usable, config paths will not be expanded")
endif()
endif()
if(ENABLE_THREADS AND CMAKE_USE_PTHREADS_INIT)
CHECK_INCLUDE_FILE("pthread_np.h" HAVE_PTHREAD_NP)
@@ -2214,6 +2275,15 @@ set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "")
set_target_properties(fastfetch PROPERTIES LINKER_LANGUAGE C)
if(WIN32)
# version.rc embeds manifest.xml (RT_MANIFEST) and logo.ico (ICON) as *resource
# references*, not as #includes, so the resource compiler never reports them and
# CMake would keep a stale .res when only the manifest or the icon changes. Both
# are plain files rather than generated ones, so declare them by hand; the source
# file property is per-path, which is why flashfetch below needs no repeat.
set_property(SOURCE src/common/windows/version.rc APPEND PROPERTY OBJECT_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/manifest.xml"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/logo.ico"
)
target_sources(fastfetch
PRIVATE src/common/windows/version.rc
)
@@ -2221,6 +2291,15 @@ elseif(APPLE)
target_link_options(fastfetch
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
)
# `LINKER:` options are handed to ld verbatim and are not registered as build
# dependencies, so without this ninja never relinks when Info.plist changes.
set_property(TARGET fastfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist")
# ld derives the ad-hoc code-signing identifier from the output file name and
# ignores the embedded CFBundleIdentifier, so set it explicitly.
add_custom_command(TARGET fastfetch POST_BUILD
COMMAND codesign --force --sign - --identifier "${FASTFETCH_BUNDLE_ID}" "$<TARGET_FILE:fastfetch>"
VERBATIM
)
endif()
if(BINARY_LINK_TYPE STREQUAL "static")
@@ -2246,8 +2325,15 @@ if (BUILD_FLASHFETCH)
PRIVATE src/common/windows/version.rc
)
elseif(APPLE)
# flashfetch embeds its own plist so that its CFBundleIdentifier matches the
# identifier it is signed with below (see the APPLE block near the top).
target_link_options(flashfetch
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist.flashfetch
)
set_property(TARGET flashfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist.flashfetch")
add_custom_command(TARGET flashfetch POST_BUILD
COMMAND codesign --force --sign - --identifier "${FLASHFETCH_BUNDLE_ID}" "$<TARGET_FILE:flashfetch>"
VERBATIM
)
endif()
@@ -2275,15 +2361,6 @@ if (BUILD_TESTS)
PRIVATE libfastfetch
)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(fastfetch-test-cpu
tests/cpu.c
)
target_link_libraries(fastfetch-test-cpu
PRIVATE libfastfetch
)
endif()
add_executable(fastfetch-test-strbuf
tests/strbuf.c
)
@@ -2396,10 +2473,14 @@ if (BUILD_TESTS)
PRIVATE libfastfetch
)
add_executable(fastfetch-test-gpu-pci
tests/gpu_pci.c
)
target_link_libraries(fastfetch-test-gpu-pci
PRIVATE libfastfetch
)
enable_testing()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test(NAME test-cpu COMMAND fastfetch-test-cpu)
endif()
add_test(NAME test-cache COMMAND fastfetch-test-cache)
add_test(NAME test-logo COMMAND fastfetch-test-logo)
add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf)
@@ -2418,6 +2499,7 @@ if (BUILD_TESTS)
add_test(NAME test-temps COMMAND fastfetch-test-temps)
add_test(NAME test-endian COMMAND fastfetch-test-endian)
add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian)
add_test(NAME test-gpu-pci COMMAND fastfetch-test-gpu-pci)
endif()
##################
+2 -7
View File
@@ -96,7 +96,7 @@ cmake -DBUILD_TESTS=On -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_X11=OFF
Required: CMake ≥ 3.21 and a C23 compiler (GCC, Clang or MSVC). Everything else is optional.
Optional dependencies are auto-detected: libpci, libdrm, vulkan, wayland, xcb, xrandr, dbus, sqlite3, rpm, imagemagick{6,7}, chafa, zlib, egl, glx, opencl, freetype, pulse, ddcutil, elf, libzfs, and more.
Optional dependencies are auto-detected: libpci, libdrm, vulkan, wayland, xcb, xrandr, dbus, sqlite3, rpm, imagemagick{6,7}, chafa, zlib, egl, glx, opencl, freetype, elf, libzfs, and more.
---
@@ -587,10 +587,6 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
| Module options | `FF<Name>Options` | `FFCPUOptions` |
| Detection results | `FF<Name>Result` | `FFCPUResult` |
### Spelling
CI runs codespell (`.codespellrc`). Known false positives are listed in `ignore-words-list` (`iterm`, `compiletime`, and various non-English distro words). Add new words there rather than changing the code.
### Compiler warnings
The build enables `-Wall -Wextra -Wconversion` plus several `-Werror`s:
@@ -684,7 +680,7 @@ cmake --build build
cd build && ctest --output-on-failure
```
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs.
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), Android, macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus a benchmark job. The Android job is the one that does not run what it builds: it cross-compiles for arm64-v8a, armeabi-v7a and x86_64 with the NDK at API 24 and inspects the result instead, because CI has no device or emulator. Its dynamic-symbol checks are what keep the weak references the platform branch depends on from turning into load failures on older devices.
**If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
@@ -707,7 +703,6 @@ Coverage focuses on the core data structures and the formatting engine in `commo
```sh
clang-format -i <changed files> # format
codespell # spelling
cmake -B build -DBUILD_TESTS=On && cmake --build build -j
cd build && ctest --output-on-failure # tests
./build/fastfetch --format json # verify JSON output is well-formed
+15 -33
View File
@@ -248,7 +248,20 @@ Set the key to a white space.
### Q: How can I display images on Windows?
As of April 2025:
#### Windows Terminal
Windows Terminal supports the sixel image protocol only.
```jsonc
{
"logo": {
"type": "sixel", // DO NOT USE "auto"
"source": "C:/path/to/image.png", // ~ can be used to refer to the Windows user home directory
"width": <image-width-in-chars>, // Optional
"height": <image-height-in-chars> // Optional
}
}
```
#### mintty and Wezterm
@@ -260,38 +273,7 @@ In `config.jsonc`:
"logo": {
"type": "iterm",
"source": "C:/path/to/image.png",
"width": <num-in-chars>
}
}
```
#### Windows Terminal
Windows Terminal supports the sixel image protocol only.
* If you installed fastfetch through MSYS2:
1. Install imagemagick: `pacman -S mingw-w64-<subsystem>-x86_64-imagemagick`
2. In `config.jsonc`:
```jsonc
{
"logo": {
"type": "sixel", // DO NOT USE "auto"
"source": "C:/path/to/image.png", // Do NOT use `~` as fastfetch is a native Windows program and doesn't apply cygwin path conversion
"width": <image-width-in-chars>, // Optional
"height": <image-height-in-chars> // Optional
}
}
```
* If you installed fastfetch via scoop or downloaded the binary directly from the GitHub Releases page:
1. Convert your image manually to sixel format using [any online image conversion service](https://www.google.com/search?q=convert+image+to+sixel)
2. In `config.jsonc`:
```jsonc
{
"logo": {
"type": "raw", // DO NOT USE "auto"
"source": "C:/path/to/image.sixel",
"width": <image-width-in-chars>, // Required
"height": <image-height-in-chars> // Required
"width": <image-width-in-chars>
}
}
```
+3 -1
View File
@@ -33,7 +33,9 @@ _fastfetch() {
if type _filedir &>/dev/null; then
_filedir
elif type compgen &>/dev/null; then
COMPREPLY+=($(compgen -f -- "$cur"))
local -a files
readarray -t files < <(compgen -f -- "$cur")
COMPREPLY+=("${files[@]}")
fi
return
;;
+1 -1
View File
@@ -2,7 +2,7 @@ Source: fastfetch
Section: universe/utils
Priority: optional
Maintainer: Carter Li <zhangsongcui@live.cn>
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libpulse-dev, libdrm-dev, libddcutil-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libdrm-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
Standards-Version: 4.0.0
Homepage: https://github.com/fastfetch-cli/fastfetch
+1
View File
@@ -630,6 +630,7 @@
"cs": "Czech",
"de": "German",
"es": "Spanish",
"fi": "Finnish",
"fr": "French",
"gl": "Galician",
"he": "Hebrew",
+30 -11
View File
@@ -277,7 +277,7 @@
"batteryFormat": {
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
"type": "string"
},
"biosFormat": {
@@ -325,7 +325,7 @@
"type": "string"
},
"cpuFormat": {
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name (x86 only)\n 14. {technology}: CPU technology (x86 only)",
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count",
"type": "string"
},
"cpucacheFormat": {
@@ -353,7 +353,7 @@
"type": "string"
},
"diskFormat": {
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used\n 2. {size-total}: Size total\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free\n 26. {size-available}: Size available",
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used (formatted)\n 2. {size-total}: Size total (formatted)\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free (formatted)\n 26. {bytes-used}: Size used (in bytes)\n 27. {bytes-total}: Size total (in bytes)",
"type": "string"
},
"diskioFormat": {
@@ -465,7 +465,7 @@
"type": "string"
},
"poweradapterFormat": {
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model name\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"type": "string"
},
"processesFormat": {
@@ -1123,6 +1123,11 @@
"const": "es",
"description": "Spanish"
},
{
"type": "string",
"const": "fi",
"description": "Finnish"
},
{
"type": "string",
"const": "fr",
@@ -1914,7 +1919,7 @@
"$ref": "#/$defs/percent"
},
"ddcciSleep": {
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection\nSee <https://www.ddcutil.com/performance_options/#option-sleep-multiplier> for details",
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection",
"oneOf": [
{
"type": "null",
@@ -2376,10 +2381,24 @@
"description": "Command text to execute",
"type": "string"
},
"useStdErr": {
"description": "Whether to use stderr instead of stdout for command output",
"type": "boolean",
"default": false
"useOutput": {
"description": "Which of the command's output streams to capture\nThe stream that is not captured is discarded",
"type": "string",
"oneOf": [
{
"const": "stdout",
"description": "Capture stdout only and discard stderr"
},
{
"const": "stderr",
"description": "Capture stderr only and discard stdout"
},
{
"const": "both",
"description": "Capture both stdout and stderr"
}
],
"default": "both"
},
"parallel": {
"description": "Whether to execute the command in parallel with other commands\nThis can improve performance when using multiple commands, but may cause issues with some commands",
@@ -2697,7 +2716,7 @@
},
"useAvailable": {
"type": "boolean",
"description": "Use f_bavail (lpFreeBytesAvailableToCaller for Windows) instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results",
"description": "Use f_bavail instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results\nOn Windows, set to true to make fastfetch honors the quota settings",
"default": false
},
"percent": {
@@ -4073,7 +4092,7 @@
"const": "publicip"
},
"url": {
"description": "URL of the public IP detection server to use. Only HTTP is supported\nIf empty, `http://ipinfo.io/json` is requested (`http://v6.ipinfo.io/json` when `ipv6` is set), with HTTP compression and TCP Fast Open enabled. A non-empty URL disables both and makes `ipv6` have no effect",
"description": "URL of the public IP detection server to use. Only HTTP is supported\nIf empty, `http://ipinfo.io/json` is requested (`http://v6.ipinfo.io/json` when `ipv6` is set), with TCP Fast Open enabled. A non-empty URL disables both and makes `ipv6` have no effect",
"type": "string",
"format": "url"
},
+6
View File
@@ -466,6 +466,12 @@ static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
}
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainAnyC(const FFstrbuf* strbuf, const char* cs) {
assert(cs != nullptr);
return strpbrk(strbuf->chars, cs) != nullptr;
}
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
assert(start <= strbuf->length);
+560
View File
@@ -0,0 +1,560 @@
#include "common/android/applabel.h"
#include "common/android/binder.h"
#include "common/android/dex.h"
#include "common/android/zip.h"
#include "common/debug.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
// The package manager, and the one method that describes an app to a caller that is not the shell.
// IPackageManager has always been part of framework.jar -- it is not one of the frameworks that
// moved into an APEX -- so there is one path and no fallback.
#define FF_ANDROID_LABEL_SERVICE "package"
#define FF_ANDROID_LABEL_DESCRIPTOR "android.content.pm.IPackageManager"
#define FF_ANDROID_LABEL_JAR "/system/framework/framework.jar"
#define FF_ANDROID_LABEL_STUB "Landroid/content/pm/IPackageManager$Stub;"
#define FF_ANDROID_LABEL_TRANSACTION "TRANSACTION_getApplicationInfo"
#define FF_ANDROID_LABEL_TABLE "resources.arsc"
// The longest package name AIDL accepts is 255 characters, and the request is one string16 of it, the
// interface token, a 64-bit flags argument and a 32-bit user id.
// clang-format off
#define FF_ANDROID_LABEL_REQUEST_SIZE \
(12 + ((4 + 2 * (sizeof(FF_ANDROID_LABEL_DESCRIPTOR) - 1) + 3) & ~3u) \
+ ((4 + 2 * 255 + 3) & ~3u) + 8u + 4u)
// clang-format on
// An ApplicationInfo parcel runs 2.0 to 3.2 KB on the test device, and the surplus is what a reply
// with more split names and directories than any of those would spend.
#define FF_ANDROID_LABEL_REPLY_SIZE (16 * 1024)
// Long enough for the `/data/app/~~<22 base64 chars>==/<package>-<22 chars>==/base.apk` the platform
// installs to, with room to spare.
#define FF_ANDROID_LABEL_PATH_SIZE 512
// ---------------------------------------------------------------------------------------------
// The resource table
// ---------------------------------------------------------------------------------------------
//
// `resources.arsc` is a ResTable: a header, a global string pool, and then one ResTable_package per
// package it defines, each carrying its own pools for type names and entry keys followed by the
// types themselves. Resolving `labelRes` is two indices deep -- the package's number names the
// package and the type, the low half names the entry -- and every step below reads the file rather
// than a table of what the file is expected to hold, because the layout is the platform's and the
// numbers are per APK.
// ResChunk_header: type(2) headerSize(2) size(4). Every chunk in the file starts with one.
#define FF_ARSC_TABLE_TYPE 0x0002
#define FF_ARSC_STRING_POOL_TYPE 0x0001
#define FF_ARSC_TABLE_PACKAGE_TYPE 0x0200
#define FF_ARSC_TABLE_TYPE_TYPE 0x0201
// ResStringPool: header(8) stringCount(4) styleCount(4) flags(4) stringsStart(4) stylesStart(4)
#define FF_ARSC_POOL_HEADER_SIZE 28
#define FF_ARSC_OFF_POOL_CHUNK_SIZE 4
#define FF_ARSC_OFF_POOL_COUNT 8
#define FF_ARSC_OFF_POOL_FLAGS 16
#define FF_ARSC_OFF_POOL_STRINGS_START 20
// A UTF-8 pool says so with this bit, and then holds the strings as UTF-8 rather than as UTF-16.
#define FF_ARSC_POOL_FLAG_UTF8 0x0100
// ResTable_package: header(8) id(4) name(256, a UTF-16 array) typeStrings(4) lastPublicType(4) ...
#define FF_ARSC_OFF_PACKAGE_ID 8
#define FF_ARSC_OFF_PACKAGE_TYPE_STRINGS (8 + 4 + 256)
// ResTable_type: header(8) id(1) flags(1) reserved(2) entryCount(4) entriesStart(4) config(...)
#define FF_ARSC_OFF_TYPE_ID 8
#define FF_ARSC_OFF_TYPE_FLAGS 9
#define FF_ARSC_OFF_TYPE_ENTRY_COUNT 12
#define FF_ARSC_OFF_TYPE_ENTRIES_START 16
#define FF_ARSC_OFF_TYPE_CONFIG 20
// The two shapes a type can take that this does not read: a sparse type stores its offsets as
// index-offset pairs rather than as a flat table, and a 16-bit one stores them at half width. Neither
// is in the APKs measured, and both would resolve an entry to the wrong offset rather than to none,
// so they are refused rather than read as if they were the plain shape.
#define FF_ARSC_TYPE_FLAG_SPARSE 0x01
#define FF_ARSC_TYPE_FLAG_OFFSET16 0x02
// ResTable_entry: size(2) flags(2), where size is the header's own length and the value follows it.
#define FF_ARSC_ENTRY_FLAG_COMPLEX 0x0001
// Res_value: size(2) res0(1) dataType(1) data(4). TYPE_STRING makes data an index into the global
// string pool; every other type is either a number or a reference to another resource.
#define FF_ARSC_OFF_VALUE_DATA_TYPE 3
#define FF_ARSC_OFF_VALUE_DATA 4
#define FF_ARSC_TYPE_STRING 0x03
// A type chunk's offset table has one word per entry, and this is what an entry with no value in that
// configuration looks like. It is not zero: offset zero is a real entry, at the start of the block.
#define FF_ARSC_NO_ENTRY 0xffffffffu
// The type a label resolves to. `string` is what `@string/...` compiles to, and it is what tells a
// label from the icons and banners that sit beside it in the same reply -- those point at `mipmap`
// or `drawable` entries whose value is a path inside the APK, so resolving them would hand back
// `res/9w.png` instead of a name.
#define FF_ANDROID_LABEL_STRING_TYPE "string"
static uint16_t readU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t readU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
// One ResStringPool, of which a table has three.
typedef struct FFAndroidLabelPool {
const uint8_t* table;
size_t tableSize;
size_t offset;
uint32_t chunkSize;
uint32_t count;
uint32_t headerSize;
uint32_t stringsStart;
bool utf8;
} FFAndroidLabelPool;
[[gnu::nonnull(2), nodiscard]] static bool poolInit(FFAndroidLabelPool* pool, const uint8_t* table, size_t tableSize, size_t offset) {
if (offset > tableSize || tableSize - offset < FF_ARSC_POOL_HEADER_SIZE) {
return false;
}
if (readU16(table + offset) != FF_ARSC_STRING_POOL_TYPE) {
return false;
}
pool->table = table;
pool->tableSize = tableSize;
pool->offset = offset;
pool->chunkSize = readU32(table + offset + FF_ARSC_OFF_POOL_CHUNK_SIZE);
pool->count = readU32(table + offset + FF_ARSC_OFF_POOL_COUNT);
pool->headerSize = readU16(table + offset + 2);
pool->stringsStart = readU32(table + offset + FF_ARSC_OFF_POOL_STRINGS_START);
pool->utf8 = (readU32(table + offset + FF_ARSC_OFF_POOL_FLAGS) & FF_ARSC_POOL_FLAG_UTF8) != 0;
return pool->headerSize >= FF_ARSC_POOL_HEADER_SIZE && pool->chunkSize > pool->headerSize;
}
// The length in front of a string. A UTF-8 pool writes one or two bytes, a UTF-16 pool one or two
// 16-bit words, and the top bit of the first says which of the two it is.
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength8(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position >= pool->tableSize) {
return false;
}
uint32_t value = pool->table[(*position)++];
if (value & 0x80) {
if (*position >= pool->tableSize) {
return false;
}
value = ((value & 0x7F) << 8) | pool->table[(*position)++];
}
*length = value;
return true;
}
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength16(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position + 2 > pool->tableSize) {
return false;
}
uint32_t value = readU16(pool->table + *position);
*position += 2;
if (value & 0x8000) {
if (*position + 2 > pool->tableSize) {
return false;
}
value = ((value & 0x7FFF) << 16) | readU16(pool->table + *position);
*position += 2;
}
*length = value;
return true;
}
[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) {
char encoded[4] = {};
uint32_t length = 0;
if (code < 0x80) {
encoded[length++] = (char) code;
} else if (code < 0x800) {
encoded[length++] = (char) (0xC0 | (code >> 6));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else if (code < 0x10000) {
encoded[length++] = (char) (0xE0 | (code >> 12));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else {
encoded[length++] = (char) (0xF0 | (code >> 18));
encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
}
ffStrbufAppendNS(result, length, encoded);
}
// One string out of a pool.
//
// The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they
// differ for everything outside ASCII. Decoding `characters` bytes is the trap: a label of a few
// multi-byte characters comes back as its first character followed by replacement bytes. Measured on
// the test device, where every resource table read is a UTF-8 pool, this was the difference between
// the right label and mojibake.
[[gnu::nonnull(1, 3), nodiscard]] static bool poolGet(const FFAndroidLabelPool* pool, uint32_t index, FFstrbuf* result) {
if (index >= pool->count) {
return false;
}
const size_t offsets = pool->offset + pool->headerSize;
if (offsets + ((size_t) index + 1) * 4 > pool->tableSize) {
return false;
}
size_t position = pool->offset + pool->stringsStart + readU32(pool->table + offsets + (size_t) index * 4);
if (position >= pool->tableSize) {
return false;
}
if (pool->utf8) {
uint32_t characters = 0;
uint32_t bytes = 0;
if (!readLength8(pool, &position, &characters) || !readLength8(pool, &position, &bytes)) {
return false;
}
// An empty string is reported as a miss rather than written: the caller sets its fallback
// before asking, and ffStrbufSetNS() with a length of zero *clears* the buffer, which would
// throw that fallback away instead of leaving it alone.
if (bytes == 0 || bytes > pool->tableSize - position) {
return false;
}
ffStrbufSetNS(result, bytes, (const char*) (pool->table + position));
return true;
}
uint32_t units = 0;
if (!readLength16(pool, &position, &units)) {
return false;
}
if (units == 0 || (size_t) units * 2 > pool->tableSize - position) {
return false;
}
for (uint32_t i = 0; i < units; i++) {
uint32_t code = readU16(pool->table + position + (size_t) i * 2);
// A code point outside the basic plane arrives as a surrogate pair, and the two halves are
// not characters of their own: half of a pair encoded on its own is not valid UTF-8.
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < units) {
const uint32_t low = readU16(pool->table + position + ((size_t) i + 1) * 2);
if (low >= 0xDC00 && low <= 0xDFFF) {
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
i++;
}
}
appendCodePoint(result, code);
}
return true;
}
// The one ResTable_package this table defines, with the two pools a lookup starts from.
typedef struct FFAndroidLabelTable {
const uint8_t* data;
size_t size;
FFAndroidLabelPool values;
FFAndroidLabelPool types;
size_t packageOffset;
size_t packageHeaderSize;
size_t packageSize;
uint32_t packageId;
} FFAndroidLabelTable;
[[gnu::nonnull(1), nodiscard]] static bool tableInit(FFAndroidLabelTable* table, const uint8_t* data, size_t size) {
// ResTable_header: header(8) packageCount(4). The header's own size is where the global pool
// starts, which is 12 in every table measured -- but it is the field rather than the number.
if (size < 12 || readU16(data) != FF_ARSC_TABLE_TYPE) {
return false;
}
table->data = data;
table->size = size;
const uint32_t headerSize = readU16(data + 2);
if (!poolInit(&table->values, data, size, headerSize)) {
return false;
}
size_t position = (size_t) headerSize + table->values.chunkSize;
while (position + 8 <= size) {
const uint16_t type = readU16(data + position);
const uint16_t chunkHeader = readU16(data + position + 2);
const uint32_t chunkSize = readU32(data + position + 4);
if (chunkSize < 8 || chunkSize > size - position) {
break;
}
// A table can define several packages -- an app that links a shared library's resources has
// more than one -- and the id in the resource reference is what picks the one to read.
if (type == FF_ARSC_TABLE_PACKAGE_TYPE && chunkSize >= chunkHeader && chunkHeader >= FF_ARSC_OFF_PACKAGE_TYPE_STRINGS + 4) {
table->packageOffset = position;
table->packageHeaderSize = chunkHeader;
table->packageSize = chunkSize;
table->packageId = readU32(data + position + FF_ARSC_OFF_PACKAGE_ID);
if (!poolInit(&table->types, data, size, position + readU32(data + position + FF_ARSC_OFF_PACKAGE_TYPE_STRINGS))) {
return false;
}
return true;
}
position += chunkSize;
}
return false;
}
// A type chunk whose ResTable_config is all zeroes is the one that applies when nothing about the
// device or the locale matches a more specific chunk. An app with a translation has several chunks
// for one type, and picking the first one that happens along would answer with whatever language it
// was written in.
[[gnu::nonnull(1), nodiscard]] static bool isDefaultConfig(const uint8_t* config, size_t length) {
if (length == 0) {
return false;
}
for (size_t i = 0; i < length; i++) {
if (config[i] != 0) {
return false;
}
}
return true;
}
[[gnu::nonnull(1, 5), nodiscard]] static bool resolveIn(const FFAndroidLabelTable* table, uint32_t typeId, uint32_t entryId, bool defaultOnly, FFstrbuf* result) {
const size_t end = table->packageOffset + table->packageSize;
for (size_t position = table->packageOffset + table->packageHeaderSize; position + 8 <= end;) {
const uint16_t type = readU16(table->data + position);
const uint16_t header = readU16(table->data + position + 2);
const uint32_t size = readU32(table->data + position + 4);
if (size < 8 || size > end - position) {
return false;
}
if (type == FF_ARSC_TABLE_TYPE_TYPE && header >= FF_ARSC_OFF_TYPE_CONFIG) {
const uint8_t id = table->data[position + FF_ARSC_OFF_TYPE_ID];
const uint8_t flags = table->data[position + FF_ARSC_OFF_TYPE_FLAGS];
if (id == typeId && (flags & (FF_ARSC_TYPE_FLAG_SPARSE | FF_ARSC_TYPE_FLAG_OFFSET16)) == 0 && size >= header) {
const uint32_t entryCount = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRY_COUNT);
const uint32_t entriesStart = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRIES_START);
const size_t offsets = position + header;
if (entryId < entryCount && offsets + ((size_t) entryId + 1) * 4 <= end) {
if (defaultOnly && !isDefaultConfig(table->data + position + FF_ARSC_OFF_TYPE_CONFIG, (size_t) header - FF_ARSC_OFF_TYPE_CONFIG)) {
position += size;
continue;
}
const uint32_t offset = readU32(table->data + offsets + (size_t) entryId * 4);
// `entriesStart` is relative to the chunk, not to the package, and both it and
// the entry's own offset come out of the file.
if (offset != FF_ARSC_NO_ENTRY && entriesStart <= size && offset <= size - entriesStart) {
const size_t entry = position + entriesStart + offset;
const size_t value = entry + readU16(table->data + entry);
if (value >= entry && value + 8 <= end && (readU16(table->data + entry + 2) & FF_ARSC_ENTRY_FLAG_COMPLEX) == 0
&& table->data[value + FF_ARSC_OFF_VALUE_DATA_TYPE] == FF_ARSC_TYPE_STRING) {
return poolGet(&table->values, readU32(table->data + value + FF_ARSC_OFF_VALUE_DATA), result);
}
}
}
}
}
position += size;
}
return false;
}
// Resolves one resource id to its string, and only when the type it names is a `string`.
//
// The type name is the whole reason this reads the package's type pool rather than trusting the id:
// `labelRes` and the icon, logo and banner ids that sit beside it in the same reply are all resource
// ids of the same package, and the difference between them is the type they point at. A `mipmap` id
// resolves perfectly well -- to `res/9w.png`.
[[gnu::nonnull(1, 3), nodiscard]] static bool tableResolve(const FFAndroidLabelTable* table, uint32_t resourceId, FFstrbuf* result) {
const uint32_t typeId = (resourceId >> 16) & 0xFF;
if (((resourceId >> 24) & 0xFF) != table->packageId || typeId == 0) {
return false;
}
FFstrbuf typeName;
ffStrbufInit(&typeName);
const bool isString = poolGet(&table->types, typeId - 1, &typeName) && ffStrbufEqualS(&typeName, FF_ANDROID_LABEL_STRING_TYPE);
ffStrbufDestroy(&typeName);
if (!isString) {
return false;
}
// The configuration that applies is the unqualified one; a chunk that only applies to a locale
// or a density is the fallback rather than the answer, which is what the second pass is for.
return resolveIn(table, typeId, resourceId & 0xFFFF, true, result)
|| resolveIn(table, typeId, resourceId & 0xFFFF, false, result);
}
// ---------------------------------------------------------------------------------------------
// The reply
// ---------------------------------------------------------------------------------------------
// The reply's own copy of the APK path. `sourceDir` is a string in the ApplicationInfo parcel and it
// ends in `.apk`; nothing else in the reply does, which is what makes it findable without walking
// the parcel's field order -- that would mean recovering `ApplicationInfo.writeToParcel` out of the
// device's dex and then framing sixty fields of unknown width, to reach one string that is already
// recognizable.
[[gnu::nonnull(1, 3), nodiscard]] static bool findApkPath(const uint8_t* data, size_t size, char* result, size_t capacity) {
for (size_t i = 8; i < size; i++) {
if (data[i] != '/') {
continue;
}
size_t end = i;
while (end < size && data[end] >= 0x20 && data[end] < 0x7F) {
end++;
}
const size_t length = end - i;
if (length >= 5 && length < capacity && memcmp(data + end - 4, ".apk", 4) == 0) {
memcpy(result, data + i, length);
result[length] = '\0';
return true;
}
i = end;
}
return false;
}
// ---------------------------------------------------------------------------------------------
typedef struct FFAndroidLabelMapping {
const uint8_t* data;
size_t size;
} FFAndroidLabelMapping;
[[gnu::nonnull(1)]] static void labelUnmap(FFAndroidLabelMapping* mapping) {
if (mapping->data != nullptr) {
munmap((void*) mapping->data, mapping->size);
}
}
[[gnu::nonnull(2), nodiscard]] static const char* labelMap(const char* path, FFAndroidLabelMapping* mapping) {
const int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(%s) failed: %s", path, strerror(errno));
return "open(apk) failed";
}
struct stat status = {};
const bool failed = fstat(fd, &status) != 0;
if (failed || status.st_size <= 0) {
FF_DEBUG("fstat(%s) reported %s", path, failed ? strerror(errno) : "an empty file");
close(fd);
return "fstat(apk) failed";
}
void* mapped = mmap(nullptr, (size_t) status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (mapped == MAP_FAILED) {
FF_DEBUG("mmap(%s, %lld bytes) failed: %s", path, (long long) status.st_size, strerror(errno));
return "mmap(apk) failed";
}
mapping->data = mapped;
mapping->size = (size_t) status.st_size;
return nullptr;
}
// One reply buffer for every call this file makes, and it makes one per query.
static uint8_t s_reply[FF_ANDROID_LABEL_REPLY_SIZE];
const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result) {
// The transaction code comes out of the jar rather than from a table here: it is a property of
// the `.aidl` the device's ROM was built from, and a vendor that inserts a method ahead of this
// one shifts every code behind it. See common/android/dex.h.
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
const FFDexStaticIntRequest lookup = {
.classDescriptor = FF_ANDROID_LABEL_STUB,
.fieldName = FF_ANDROID_LABEL_TRANSACTION,
.result = &transaction,
};
const char* error = ffDexStaticInts(FF_ANDROID_LABEL_JAR, &lookup, 1);
if (error != nullptr) {
return error;
}
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
return "framework.jar does not declare IPackageManager.getApplicationInfo";
}
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
error = ffBinderOpen(&binder);
if (error != nullptr) {
return error;
}
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_ANDROID_LABEL_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
if (error != nullptr) {
FF_DEBUG("Looking up \"%s\" failed: %s", FF_ANDROID_LABEL_SERVICE, error);
return error;
}
uint8_t request[FF_ANDROID_LABEL_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_ANDROID_LABEL_DESCRIPTOR);
ffBinderParcelPutString16(&parcel, packageName);
// `getApplicationInfo(String, long flags, int userId)`. The flags argument is 64-bit from Android
// 13 on and 32-bit before it, and the wider one is what works on both: a release that reads an
// int takes the low word and leaves the zero high word unread, because an AIDL stub stops
// reading once its own arguments are in. The narrow one is not symmetric -- a release that reads
// a long would take half of its own flags argument for the user id.
ffBinderParcelPutU64(&parcel, 0);
ffBinderParcelPutI32(&parcel, 0);
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
if (error != nullptr) {
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
return "The package manager rejected the request";
}
// The AIDL status first, then the presence marker of a nullable Parcelable: a package the caller
// is not allowed to see answers an empty ApplicationInfo rather than an error.
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
return "The package manager refused to describe the app";
}
if (ffBinderReadI32(reply.data, reply.size, 4) == 0) {
FF_DEBUG("The package manager described \"%s\" as an empty ApplicationInfo", packageName);
return nullptr;
}
char path[FF_ANDROID_LABEL_PATH_SIZE];
if (!findApkPath(reply.data, reply.size, path, sizeof(path))) {
return "The reply carries no APK path";
}
FF_DEBUG("\"%s\" is installed at \"%s\"", packageName, path);
[[gnu::cleanup(labelUnmap)]] FFAndroidLabelMapping mapping = {};
error = labelMap(path, &mapping);
if (error != nullptr) {
return error;
}
FFAndroidZipEntry table = {};
error = ffAndroidZipFindEntry(mapping.data, mapping.size, FF_ANDROID_LABEL_TABLE, &table);
if (error != nullptr) {
return error;
}
if (table.method != FF_ZIP_METHOD_STORED) {
FF_DEBUG("\"%s\" stores resources.arsc with compression method %u", packageName, (unsigned) table.method);
return "The resource table is stored compressed";
}
FFAndroidLabelTable resources = {};
if (!tableInit(&resources, table.payload, table.payloadSize)) {
return "The resource table could not be read";
}
// Every word of the reply shaped like one of this package's resource ids, in the order they
// appear, and the first one that resolves to a `string` is the label. See the header for what
// that rule was audited against and why it beats recovering the parcel's field order.
for (size_t offset = 8; offset + 4 <= reply.size; offset += 4) {
if (tableResolve(&resources, readU32(reply.data + offset), result)) {
FF_DEBUG("\"%s\" shows itself as \"%s\"", packageName, result->chars);
return nullptr;
}
}
FF_DEBUG("No resource id in the reply of \"%s\" resolves to a string", packageName);
return nullptr;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "fastfetch.h"
// The name an app presents to the user, which is not its package name: an app installed as
// `com.termux` presents itself as `Termux:Launcher` on the launcher, in its notifications and on
// the settings screen.
//
// Every route that answers with an *already resolved* label is closed to an app UID, and each one
// was measured on the test device rather than assumed:
//
// * `dumpsys package` needs android.permission.DUMP.
// * `cmd package dump` is filtered down to 929 bytes for an app UID, against 1.2 MB for the shell.
// * The task descriptions the activity service hands out only ever describe the caller's own task.
// * `getApplicationInfo` answers with `labelRes` -- a resource id -- and leaves `nonLocalizedLabel`
// null, so there is no string in the reply to read.
//
// What is open is the APK itself. `getApplicationInfo` carries its path (`sourceDir`), APKs are
// world readable, and the `resources.arsc` inside one is *stored* rather than deflated -- the
// platform expects to mmap it -- so resolving an id out of it needs no decompressor. That is the
// route this takes: one transaction, one mapping, no fork and no child process.
//
// The label is the resource id in the reply whose type is `string`. That is a rule about the reply
// rather than about the parcel's field order, and it was audited against the manifest's own
// `android:label` over every package installed on the test device: nine in ten have exactly one
// `string` candidate and it is the right one every time, a handful more have two and the first is
// still the right one, and the rest have none because their label is a literal string rather than a
// reference (`android:label="Example"`), so the reply carries no id at all. No package resolved to
// a wrong string; the counts behind that are in doc/app-label.md.
//
// Writes the label into `result` when it can be resolved, and leaves `result` alone otherwise --
// which includes the cases above where there is nothing to resolve. The caller sets its fallback
// first, so that an unresolved label costs nothing but the query.
//
// Returns nullptr on success, a static message when the query itself could not be made. An error is
// not a fallback: it says the label was not looked for, not that the app has none.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result);
+30 -5
View File
@@ -1,5 +1,8 @@
#include "common/android/binder.h"
#include "common/debug.h"
#include "common/io.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
@@ -7,14 +10,18 @@
const char* ffBinderOpen(FFBinder* binder) {
*binder = (FFBinder) { .fd = -1, .shared = nullptr, .sharedSize = 0, .protocolVersion = 0 };
const int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC);
FF_AUTO_CLOSE_FD int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(" FF_BINDER_DEVICE ") failed: %s", strerror(errno));
return "Failed to open " FF_BINDER_DEVICE;
}
struct binder_version version = { .protocol_version = 0 };
if (ioctl(fd, BINDER_VERSION, &version) != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) {
close(fd);
const int versionIoctl = ioctl(fd, BINDER_VERSION, &version);
if (versionIoctl != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) {
FF_DEBUG("BINDER_VERSION reported protocol %u, expected %u (%s)",
version.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION,
versionIoctl != 0 ? strerror(errno) : "version mismatch");
return "Unsupported binder protocol version";
}
@@ -22,7 +29,7 @@ const char* ffBinderOpen(FFBinder* binder) {
// addresses in BR_REPLY, so it has to stay mapped for as long as the handle is used.
void* shared = mmap(nullptr, FF_BINDER_SHARED_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, fd, 0);
if (shared == MAP_FAILED) {
close(fd);
FF_DEBUG("mmap(" FF_BINDER_DEVICE ") failed: %s", strerror(errno));
return "Failed to mmap " FF_BINDER_DEVICE;
}
@@ -32,6 +39,7 @@ const char* ffBinderOpen(FFBinder* binder) {
.sharedSize = FF_BINDER_SHARED_SIZE,
.protocolVersion = version.protocol_version,
};
fd = -1; // ownership moved into *binder, which ffBinderClose() lets go
return nullptr;
}
@@ -55,6 +63,8 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
reply->fdCount = 0;
if (parcel->truncated) {
FF_DEBUG("The parcel was truncated at %zu bytes, the caller buffer holds %zu",
parcel->size, parcel->capacity);
return "Binder parcel does not fit the caller buffer";
}
@@ -86,6 +96,7 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
.read_buffer = (binder_uintptr_t) (uintptr_t) readBuffer,
};
if (ioctl(binder->fd, BINDER_WRITE_READ, &exchange) != 0) {
FF_DEBUG("BINDER_WRITE_READ(handle=%u, code=%u) failed: %s", handle, code, strerror(errno));
return "BINDER_WRITE_READ failed";
}
@@ -103,8 +114,10 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
break;
case BR_FAILED_REPLY:
FF_DEBUG("BR_FAILED_REPLY for handle=%u, code=%u", handle, code);
return "Binder transaction failed";
case BR_DEAD_REPLY:
FF_DEBUG("BR_DEAD_REPLY: handle=%u is dead", handle);
return "Binder service is not running";
case BR_INCREFS:
@@ -123,6 +136,8 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
case BR_REPLY: {
if ((size_t) (end - cursor) < sizeof(struct binder_transaction_data)) {
FF_DEBUG("BR_REPLY is truncated: %zu of %zu bytes left",
(size_t) (end - cursor), sizeof(struct binder_transaction_data));
return "Truncated binder reply";
}
struct binder_transaction_data data;
@@ -140,6 +155,8 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
const char* error = nullptr;
if (payloadSize > 0) {
if (payloadSize > reply->capacity) {
FF_DEBUG("The reply carries %zu payload bytes, the caller buffer holds %zu",
payloadSize, reply->capacity);
error = "Binder reply does not fit the caller buffer";
} else {
memcpy(reply->data, payload, payloadSize);
@@ -211,6 +228,7 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
// later transaction on them fails with BR_FAILED_REPLY, which says nothing about the
// real cause. Reported, but only when nothing more specific is already at hand.
if (ioctl(binder->fd, BINDER_WRITE_READ, &release) != 0 && error == nullptr) {
FF_DEBUG("BC_ACQUIRE/BC_FREE_BUFFER failed: %s", strerror(errno));
error = "Releasing the binder reply failed";
}
@@ -218,10 +236,13 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, u
}
default:
FF_DEBUG("Unexpected binder command 0x%08x for handle=%u, code=%u", command, handle, code);
return "Unexpected binder command";
}
}
FF_DEBUG("The binder reply for handle=%u, code=%u held no BR_REPLY (%zu bytes read)",
handle, code, (size_t) exchange.read_consumed);
return "No reply from binder";
}
@@ -238,12 +259,16 @@ const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t t
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
FF_DEBUG("The service manager returned a status parcel for %s, code=%u", name, transactionCode);
return "Service manager rejected the request";
}
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
if (exception != 0) {
FF_DEBUG("The service manager raised exception %d for %s", exception, name);
return "Service manager raised an exception";
}
if (reply.handleCount == 0) {
FF_DEBUG("The service manager returned no handle for %s", name);
return "Service is not registered";
}
+912 -161
View File
File diff suppressed because it is too large Load Diff
+141 -4
View File
@@ -24,7 +24,144 @@
#include "fastfetch.h"
// Resolves the int value of `<classDescriptor>.<fieldName>` out of the `classes.dex` of `jarPath`.
// `classDescriptor` is the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
// Returns nullptr on success, a static message otherwise.
[[gnu::nonnull(1, 2, 3, 4), nodiscard]] const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result);
// What a request is written when the jar does not answer it, on every path out of ffDexStaticInts --
// a jar that cannot be opened and a field the class does not declare both leave it behind, so a
// caller reads a result rather than what its own stack happened to hold there. No transaction code
// is negative: AIDL numbers them from `IBinder.FIRST_CALL_TRANSACTION`, which is 1.
//
// This is what keeps the reader a lookup and nothing more. Whether a code is one the caller cannot do
// without or one it only uses when the build declares it, whether what came back is enough to carry
// on, and whether the module reports a value or an error are the caller's decisions, made by reading
// the results. The reader finds numbers and says which ones it could not.
#define FF_DEX_STATIC_INT_UNRESOLVED (-1)
// One `<classDescriptor>.<fieldName>` lookup, and where its value is written. `classDescriptor` is
// the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
typedef struct FFDexStaticIntRequest {
const char* classDescriptor;
const char* fieldName;
int32_t* result;
} FFDexStaticIntRequest;
// Resolves the int value of every request out of the dex entries of `jarPath`, walking `classes.dex`,
// `classes2.dex`, ... in order until one of them defines the class.
//
// Asking for several fields at once is not a convenience. A lookup is dominated by the type table,
// which lists every type the dex references -- 8428 descriptors in `framework.jar`'s `classes.dex`,
// each one read from a different part of a 51 MB mapping -- and a single pass answers every request
// whose class is named in it. Measured over three devices' own `framework.jar`, four fields of two
// classes cost one pass rather than four.
//
// Every request is written a code or FF_DEX_STATIC_INT_UNRESOLVED. A request is never allowed to
// take the others down with it: the classes a caller asks for are looked for independently, and one
// the jar does not declare leaves its own result at the sentinel while the rest are answered.
//
// Not finding a code is not a failure. A jar that is readable answers the call, whether it declares
// every field asked for, some of them, or none -- the results say which, and what that means for the
// module is the caller's to decide. The return value is therefore reserved for the one thing the
// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
// or does not hold a dex that can be read. nullptr means the lookup ran.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
// ---------------------------------------------------------------------------------------------
// Reading a Parcelable's wire layout
// ---------------------------------------------------------------------------------------------
//
// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
// that order is the only thing that says where each one is. It is not in the class's declaration: a
// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
// declaration order a reader would want is not recorded anywhere but the method body.
//
// Recovering it therefore means walking the bytecode. The compiler emits
//
// iget v0, p0, LDisplayInfo;->logicalWidth:I
// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
//
// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
// easy to get wrong in a way that still produces a plausible-looking list:
//
// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
// The table below is ART's `Instruction::SizeInCodeUnits`.
// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
// first of them rather than trying to step over it.
//
// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
// again inside the loop that writes the elements. A class cannot declare two instance fields under
// one name, so a repeat is always that reload, and the first occurrence is where the field is
// written. The reader de-duplicates by name for that reason.
//
// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
// does not. Measured over three releases' own `framework.jar`, that is four names per class.
//
// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
// ones a parser written against one release reads out of the middle of another. Reading the sequence
// from the build that is running makes every one of them optional by construction.
// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
// than truncated: half a name would silently match nothing, which is indistinguishable from the field
// not being there.
#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
typedef struct FFDexParcelField {
char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
} FFDexParcelField;
// One class, and the two things that can be asked about it: the write sequence of one of its methods,
// and which instance fields it declares. Either half may be left out, and both are answered out of
// the same `class_data_item` -- which is why they are one request rather than two calls.
//
// Asking for them together is not a convenience. Both halves are read from a walk that is dominated
// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a
// different part of a 51 MB mapping -- and the fields a class declares are listed in the very
// `class_data_item` the sequence is read out of. A second call would map the jar again, open every
// entry again and make that pass again to reach a list the first call already had in hand. Measured
// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup.
typedef struct FFDexParcelRequest {
const char* classDescriptor;
// The write sequence. Left out by a request that only wants the instance fields, in which case
// `fields`, `capacity` and `count` are not read.
const char* methodName;
FFDexParcelField* fields;
uint32_t capacity;
uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
// The instance fields. This is the question a Parcelable whose layout is not field-driven needs.
// `DisplayCutout` is one: its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors
// the compiler synthesised, so there is no `iget` to walk and no class name on the wire either.
// What does say how long its body is, is which fields the class carries -- Android 11 declares
// three, Android 13 adds `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each
// one adds a known run of writes behind the three both have. A declaration is a field set rather
// than an order, which is exactly what this answers.
const char* const* instanceFieldNames;
bool* instanceFieldResults;
uint32_t instanceFieldCount;
} FFDexParcelRequest;
// Resolves every request out of the dex entries of `jarPath`, walking `classes.dex`, `classes2.dex`,
// ... in order until one of them defines the class -- the same walk, and the same one pass over the
// type table, that ffDexStaticInts makes.
//
// Every request is written its answers before anything can fail: a count of 0 for the sequence, and
// `false` for every instance field. A class the jar does not declare, a method it does not declare,
// and a sequence longer than the caller's `capacity` all leave the count at 0, and none of them holds
// back the others: they are independent questions about the same pass. A count of 0 is not a failure
// but an answer -- this build does not write that class, so a caller reading it has nothing to walk.
// The return value is reserved for the jar itself, as in ffDexStaticInts.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);
+1 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "fastfetch.h"
// The one package name the shell UID owns. android.os.Process.SHELL_UID is a single app UID that
// owns exactly one package, so a process that cannot derive a package of its own -- a static build
+100
View File
@@ -0,0 +1,100 @@
#include "common/android/zip.h"
#include "common/debug.h"
#include <string.h>
static uint16_t zipU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t zipU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result) {
const size_t entryLength = strlen(entry);
if (archiveSize < FF_ZIP_EOCD_SIZE) {
return "The archive is too small to be a zip";
}
// The record is the last thing in the file, so it is searched for backwards -- over at most the
// 64 KiB a trailing comment can take, not over the whole archive.
const size_t newest = archiveSize - FF_ZIP_EOCD_SIZE;
const size_t oldest = archiveSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
? archiveSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
: 0;
size_t eocd = SIZE_MAX;
for (size_t i = newest;; --i) {
if (memcmp(archive + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
eocd = i;
break;
}
if (i == oldest) {
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
// before it stops would wrap and read the whole file backwards, which is the very scan
// this is here to avoid.
break;
}
}
if (eocd == SIZE_MAX) {
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte archive",
newest - oldest + FF_ZIP_EOCD_SIZE, archiveSize);
return "The archive has no zip directory";
}
const uint32_t directorySize = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
const uint32_t directoryOffset = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
if (directoryOffset > archiveSize || directorySize > archiveSize - directoryOffset) {
return "The zip directory is out of range";
}
// The directory is walked to its end rather than for the entry count the record carries: that
// count is a 16-bit field which a 65535-entry archive pins at 0xFFFF and moves into a zip64
// record, while the two bounds above describe the directory itself on every archive this small.
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
if (memcmp(archive + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
return "A zip directory entry is malformed";
}
const uint16_t nameLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
const uint16_t extraLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
const uint16_t commentLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
if (entrySize > directoryEnd - offset) {
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
entrySize, (size_t) (directoryEnd - offset));
return "A zip directory entry runs past the directory";
}
if (nameLength == entryLength && memcmp(archive + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
const uint32_t compressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
const uint32_t uncompressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
const uint32_t localOffset = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressedSize == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
FF_DEBUG("The zip entry \"%s\" is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
entry, compressedSize, uncompressedSize, localOffset);
return "The zip entry is stored as zip64, which is not supported";
}
if (localOffset > archiveSize || FF_ZIP_LOCAL_HEADER_SIZE > archiveSize - localOffset) {
return "A zip local header is out of range";
}
const uint16_t localNameLength = zipU16(archive + localOffset + FF_ZIP_OFF_NAME_LENGTH);
const uint16_t localExtraLength = zipU16(archive + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
if (headerSize > archiveSize - localOffset || compressedSize > archiveSize - localOffset - headerSize) {
return "The zip entry bounds are out of range";
}
result->method = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_METHOD);
result->uncompressedSize = uncompressedSize;
result->payload = archive + localOffset + headerSize;
result->payloadSize = compressedSize;
return nullptr;
}
offset += entrySize;
}
FF_DEBUG("No entry in the %zu byte archive is named \"%s\"", archiveSize, entry);
return "The archive has no entry with that name";
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "fastfetch.h"
// Locating an entry inside a zip, which is what both the jar reader and the app label reader need:
// one looks for `classesN.dex`, the other for the resource table of an APK.
//
// The entry is found through the central directory rather than by scanning for its local header,
// and that is not a micro-optimisation. A local header is preceded by everything that came before
// it, so scanning costs a pass over all of it, once per lookup: `framework.jar` is 52 MB and its
// `classes2.dex` starts 9.6 MB in. Measured on the test device, where the sound module resolves
// seven codes, looking the entries up by scanning was the difference between 30 ms and 2 seconds.
//
// The sizes and the compression method are read from the directory rather than from the entry's own
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
// choose that. Only the local header's two length fields are used, because they are what says where
// the payload begins -- the local extra field does not have to match the central one.
// The zip local file header, from APPNOTE.TXT 4.3.7.
#define FF_ZIP_LOCAL_HEADER_SIZE 30
#define FF_ZIP_OFF_NAME_LENGTH 26
#define FF_ZIP_OFF_EXTRA_LENGTH 28
// The central directory.
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
#define FF_ZIP_CENTRAL_SIZE 46
#define FF_ZIP_OFF_CENTRAL_METHOD 10
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
// The end of central directory record, which points at the directory. It is the last thing in the
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
#define FF_ZIP_EOCD_SIZE 22
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
// and the APKs measured so far are orders of magnitude below that, and carrying the extra field
// reader for them is not worth it.
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
#define FF_ZIP_METHOD_STORED 0
#define FF_ZIP_METHOD_DEFLATED 8
// One entry, as the archive describes it. `payload` points into the mapping the caller passed in and
// is not a copy: for a STORED entry it is the file's own bytes, and for a DEFLATED one it is the raw
// deflate stream, which the caller inflates with `uncompressedSize` as the bound.
typedef struct FFAndroidZipEntry {
const uint8_t* payload;
size_t payloadSize;
uint32_t uncompressedSize;
uint16_t method;
} FFAndroidZipEntry;
// Resolves `entry` through the archive's central directory, writing where its payload is into
// `result`. Returns nullptr on success, a static message otherwise -- an archive that is not a zip,
// a directory that is out of range, an entry that is not there and an entry stored as zip64 are all
// reported rather than answered with a pointer into nothing.
[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result);
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>fastfetch</string>
<string>@BUNDLE_ID@</string>
<key>CFBundleName</key>
<string>@PROJECT_NAME@</string>
<key>CFBundleShortVersionString</key>
+32
View File
@@ -1,36 +1,47 @@
#include "cf_helpers.h"
#include "common/debug.h"
const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result) {
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt64Type, result)) {
FF_DEBUG("CFNumberGetValue(kCFNumberSInt64Type) failed, the number is of type 0x%08lx",
(unsigned long) CFNumberGetType((CFNumberRef) cf));
return "Number type is not SInt64";
}
return nullptr;
} else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
if (CFDataGetLength((CFDataRef) cf) != sizeof(int64_t)) {
FF_DEBUG("CFData holds %ld bytes, int64_t needs %zu",
(long) CFDataGetLength((CFDataRef) cf), sizeof(int64_t));
return "Data length is not sizeof(int64_t)";
}
CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(int64_t)), (uint8_t*) result);
return nullptr;
}
FF_DEBUG("Expected a CFNumber or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is neither 'CFNumber' nor 'CFData'";
}
const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) {
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt32Type, result)) {
FF_DEBUG("CFNumberGetValue(kCFNumberSInt32Type) failed, the number is of type 0x%08lx",
(unsigned long) CFNumberGetType((CFNumberRef) cf));
return "Number type is not SInt32";
}
return nullptr;
} else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
if (CFDataGetLength((CFDataRef) cf) != sizeof(*result)) {
FF_DEBUG("CFData holds %ld bytes, int32_t needs %zu",
(long) CFDataGetLength((CFDataRef) cf), sizeof(*result));
return "Data length is not sizeof(int32_t)";
}
CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(*result)), (uint8_t*) result);
return nullptr;
}
FF_DEBUG("Expected a CFNumber or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is neither 'CFNumber' nor 'CFData'";
}
@@ -38,16 +49,20 @@ const char* ffCfNumGetDouble(CFTypeRef cf, double* result) {
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberDoubleType, result) &&
!CFNumberGetValue((CFNumberRef) cf, kCFNumberFloatType, result)) {
FF_DEBUG("CFNumber is of type 0x%08lx, neither Double nor Float",
(unsigned long) CFNumberGetType((CFNumberRef) cf));
return "Number type is not Double or Float";
}
return nullptr;
}
FF_DEBUG("Expected a CFNumber, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is neither 'CFNumber'";
}
const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result) {
if (CFGetTypeID(cf) != CFDateGetTypeID()) {
FF_DEBUG("Expected a CFDate, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is not 'CFDate'";
}
@@ -76,6 +91,7 @@ const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
}
ffStrbufEnsureFixedLengthFree(result, (uint32_t) CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8));
if (!CFStringGetCString(cfStr, result->chars, result->allocated, kCFStringEncodingUTF8)) {
FF_DEBUG("CFStringGetCString() failed for a %u character string", (unsigned) length);
return "CFStringGetCString() failed";
}
// CFStringGetCString ensures the buffer is NUL terminated
@@ -93,6 +109,7 @@ const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
result->length = (uint32_t) strnlen(result->chars, length);
result->chars[result->length] = '\0';
} else {
FF_DEBUG("Expected a CFString or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is neither 'CFString' nor 'CFData'";
}
@@ -116,6 +133,7 @@ const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result) {
result->length = length;
result->chars[result->length] = '\0';
} else {
FF_DEBUG("Expected a CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is not 'CFData'";
}
@@ -125,6 +143,7 @@ const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result) {
const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
@@ -134,6 +153,7 @@ const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* r
const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
@@ -143,10 +163,12 @@ const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstr
const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result) {
CFBooleanRef cf = (CFBooleanRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
if (CFGetTypeID(cf) != CFBooleanGetTypeID()) {
FF_DEBUG("Expected a CFBoolean, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is not 'CFBoolean'";
}
@@ -157,6 +179,7 @@ const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result)
const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
@@ -166,6 +189,7 @@ const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result) {
const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
@@ -175,6 +199,7 @@ const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* res
const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
@@ -184,16 +209,20 @@ const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* res
const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
if (CFGetTypeID(cf) != CFDataGetTypeID()) {
FF_DEBUG("Expected a CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
return "TypeID is not 'CFData'";
}
CFIndex trueLength = CFDataGetLength((CFDataRef) cf);
if (trueLength < offset + size) {
FF_DEBUG("The CFData holds %ld bytes, the caller asked for [%u, %u)",
(long) trueLength, offset, offset + size);
return "Data length is less than offset + size";
}
@@ -208,6 +237,8 @@ const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offs
const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result) {
CFDictionaryRef cf = (CFDictionaryRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr || CFGetTypeID(cf) != CFDictionaryGetTypeID()) {
FF_DEBUG("Expected a CFDictionary, got %s",
cf == nullptr ? "no value at all" : "a value of another type");
return "TypeID is not 'CFDictionary'";
}
@@ -218,6 +249,7 @@ const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryR
const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == nullptr) {
FF_DEBUG("The dictionary has no value for the key");
return "CFDictionaryGetValue() failed";
}
+28 -2
View File
@@ -1,5 +1,6 @@
#include "smc_temps.h"
#include "common/apple/cf_helpers.h"
#include "common/debug.h"
#include "common/strutil.h"
#include <stdint.h>
@@ -83,7 +84,10 @@ static void smcUltostr(char* str, uint32_t val) {
static const char* smcCall(io_connect_t conn, uint32_t selector, SmcKeyData_t* inputStructure, SmcKeyData_t* outputStructure) {
size_t size = sizeof(SmcKeyData_t);
if (IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size) != kIOReturnSuccess) {
const IOReturn callStatus = IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size);
if (callStatus != kIOReturnSuccess) {
FF_DEBUG("IOConnectCallStructMethod(selector=%u, command=%u) failed: IOReturn 0x%08x",
selector, (uint32_t) (unsigned char) inputStructure->data8, (unsigned) callStatus);
return "IOConnectCallStructMethod(conn) failed";
}
return nullptr;
@@ -136,10 +140,13 @@ static const char* smcReadSmcVal(io_connect_t conn, const UInt32Char_t key, SmcV
static const char* smcOpen(io_connect_t* conn) {
FF_IOOBJECT_AUTO_RELEASE io_object_t device = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMC"));
if (!device) {
FF_DEBUG("IOKit has no AppleSMC service");
return "No SMC device found";
}
if (IOServiceOpen(device, mach_task_self(), 0, conn) != kIOReturnSuccess) {
const IOReturn openStatus = IOServiceOpen(device, mach_task_self(), 0, conn);
if (openStatus != kIOReturnSuccess) {
FF_DEBUG("IOServiceOpen(AppleSMC) failed: IOReturn 0x%08x", (unsigned) openStatus);
return "IOServiceOpen() failed";
}
@@ -153,6 +160,7 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
return error;
}
if (val.dataSize == 0) {
FF_DEBUG("The SMC key %s reported a data size of 0", key);
return "Empty SMC result";
}
@@ -173,9 +181,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
*value = (double) ntohll(*(uint64_t*) (val.bytes));
break;
default:
FF_DEBUG("SMC key %s: unsigned type %s has an unsupported size of %u bytes",
key, val.dataType, val.dataSize);
return "Unsupported SMC unsigned integer data size";
}
} else {
FF_DEBUG("SMC key %s: unsupported unsigned type %s", key, val.dataType);
return "Unsupported SMC unsigned data type";
}
break;
@@ -204,9 +215,13 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
} else if (ffStrEquals(val.dataType, "fpe2")) {
*value = ntohs(*(uint16_t*) (val.bytes)) / 4.0;
} else {
FF_DEBUG("SMC key %s: unsupported fixed point type %s of %u bytes",
key, val.dataType, val.dataSize);
return "Unsupported SMC floating point data type";
}
} else {
FF_DEBUG("SMC key %s: unsupported floating point type %s of %u bytes",
key, val.dataType, val.dataSize);
return "Unsupported SMC floating point data type";
}
break;
@@ -227,6 +242,8 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
*value = (double) ntohll(*(int64_t*) (val.bytes));
break;
default:
FF_DEBUG("SMC key %s: signed type %s has an unsupported size of %u bytes",
key, val.dataType, val.dataSize);
return "Unsupported SMC signed integer data size";
}
} else if (val.dataType[1] == 'p' && val.dataSize == 2) // signed fixed point types
@@ -252,9 +269,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
} else if (ffStrEquals(val.dataType, "spf0")) {
*value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1.0;
} else {
FF_DEBUG("SMC key %s: unsupported signed fixed point type %s of %u bytes",
key, val.dataType, val.dataSize);
return "Unsupported SMC signed integer data type";
}
} else {
FF_DEBUG("SMC key %s: unsupported signed type %s", key, val.dataType);
return "Unsupported SMC signed data type";
}
break;
@@ -263,11 +283,15 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
if (ffStrEquals(val.dataType, "{pwm") && val.dataSize == 2) {
*value = (double) ntohs(*(uint16_t*) (val.bytes)) * 100 / 65536.0;
} else {
FF_DEBUG("SMC key %s: unsupported special type %s of %u bytes", key, val.dataType, val.dataSize);
return "Unsupported SMC special data type";
}
break;
default:
FF_DEBUG("SMC key %s: unknown data type '%c%c' (%s) of %u bytes",
key, val.dataType[0] ? val.dataType[0] : '?', val.dataType[1] ? val.dataType[1] : '?',
val.dataType, val.dataSize);
return "Unsupported SMC data type";
}
return nullptr;
@@ -300,6 +324,7 @@ const char* ffDetectSmcSpecificTemp(const char* sensor, double* result) {
}
if (!detectTemp(conn, sensor, result)) {
FF_DEBUG("The SMC sensor %s could not be read, or read a value outside 10..120", sensor);
return "Could not read SMC temperature";
}
@@ -490,6 +515,7 @@ const char* ffDetectSmcTemps(enum FFTempType type, double* result) {
break;
}
FF_DEBUG("None of the SMC sensors for temp type %d reported a usable reading", (int) type);
if (count == 0) {
return "No temperatures detected";
}
+12 -28
View File
@@ -1,38 +1,22 @@
#pragma once
#include "fastfetch.h"
#include "common/time.h"
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline const char* ffFindFileName(const char* file) {
const char* lastSlash = __builtin_strrchr(file, '/');
#ifdef _WIN32
if (lastSlash == nullptr) {
lastSlash = __builtin_strrchr(file, '\\');
}
#endif
if (lastSlash != nullptr) {
return lastSlash + 1;
}
return file;
}
#ifndef NDEBUG
#define FF_DEBUG_PRINT(file_, line_, format_, ...) \
do { \
if (instance.config.display.debugMode) \
fprintf(stderr, "[%s%4d, %s] " format_ "\n", ffFindFileName(file_), line_, ffTimeToTimeStr(ffTimeGetNow()), ##__VA_ARGS__); \
} while (0)
#else
#define FF_DEBUG_PRINT(file_, line_, format_, ...) \
do { \
} while (0)
#endif
#include "fastfetch.h"
#include "common/time.h"
#define FF_DEBUG(format, ...) FF_DEBUG_PRINT(__FILE__, __LINE__, format, ##__VA_ARGS__)
[[gnu::nonnull(1, 3), gnu::format(printf, 3, 4)]]
void ffDebugPrint(const char* file, int line, const char* format, ...);
#if _WIN32
#define FF_DEBUG(format, ...) ffDebugPrint(__FILE__, __LINE__, format, ##__VA_ARGS__)
#if _WIN32
const char* ffDebugWin32Error(DWORD errorCode);
const char* ffDebugNtStatus(NTSTATUS status);
const char* ffDebugConfigRet(unsigned long /*CONFIGRET*/ ret);
const char* ffDebugHResult(HRESULT hr);
#endif
#else
#define FF_DEBUG(format, ...) ((void) 0)
#endif
+12 -5
View File
@@ -64,7 +64,11 @@ static void getExePath(FFPlatform* platform) {
exePathLen--; // remove terminating NUL
}
#elif defined(__OpenBSD__)
// OpenBSD doesn't have a reliable way to get the executable path.
#if FF_HAVE_GETEXECPATH
// https://man.openbsd.org/getexecpath.3
size_t exePathLen = getexecpath(exePath, sizeof(exePath)) == 0 ? strlen(exePath) : 0;
#else
// OpenBSD (before 8.0) doesn't have a reliable way to get the executable path.
// Current implementation uses argv[0], which can be easily spoofed.
// See #2195
size_t exePathLen = 0;
@@ -165,6 +169,7 @@ static void getExePath(FFPlatform* platform) {
}
}
}
#endif
#elif defined(__sun)
ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
if (exePathLen > 0) {
@@ -299,10 +304,7 @@ static void getHostName(FFPlatform* platform, const struct utsname* uts) {
}
static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
const char* shell = getenv("SHELL");
if (!ffStrSet(shell) && pwd) {
shell = pwd->pw_shell;
}
const char* shell = pwd ? pwd->pw_shell : getenv("SHELL");
ffStrbufAppendS(&platform->userShell, shell);
}
@@ -336,7 +338,12 @@ static void getCwd(FFPlatform* platform) {
void ffPlatformInitImpl(FFPlatform* platform) {
platform->pid = (uint32_t) getpid();
platform->uid = getuid();
#if !__ANDROID__
struct passwd* pwd = getpwuid(platform->uid);
#else
// On Android, /etc/passwd is empty, and getpwuid() will return `/data` for pw_dir
struct passwd* pwd = nullptr;
#endif
struct utsname uts;
if (uname(&uts) < 0) {
+42 -42
View File
@@ -2,13 +2,13 @@
#include "common/io.h"
#include "common/library.h"
#include "common/strutil.h"
#include "common/windows/folders.h"
#include "common/windows/unicode.h"
#include "common/windows/registry.h"
#include "common/windows/nt.h"
#include <stdalign.h>
#include <windows.h>
#include <shlobj.h>
#include <sddl.h>
#define SECURITY_WIN32 1 // For secext.h
@@ -43,43 +43,27 @@ static void getExePath(FFPlatform* platform) {
ffStrbufReplaceAllC(&platform->exePath, '\\', '/');
}
static void getHomeDir(FFPlatform* platform) {
PWSTR pPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, nullptr, &pPath))) {
ffStrbufSetWS(&platform->homeDir, pPath);
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
} else {
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE"));
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
}
CoTaskMemFree(pPath);
}
static void getCacheDir(FFPlatform* platform) {
PWSTR pPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &pPath))) {
ffStrbufSetWS(&platform->cacheDir, pPath);
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
} else {
if (!ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &platform->cacheDir)) {
ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
ffStrbufAppendS(&platform->cacheDir, "AppData/Local/");
return;
}
CoTaskMemFree(pPath);
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
}
static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) {
PWSTR pPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, nullptr, &pPath))) {
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath);
CoTaskMemFree(pPath);
ffStrbufReplaceAllC(&buffer, '\\', '/');
ffStrbufEnsureEndsWithC(&buffer, '/');
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
}
static void platformPathAddKnownFolder(FFlist* dirs, FFKnownFolder folder) {
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
if (!ffGetKnownFolderPath(folder, &buffer)) {
return;
}
ffStrbufReplaceAllC(&buffer, '\\', '/');
ffStrbufEnsureEndsWithC(&buffer, '/');
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
}
}
@@ -112,9 +96,9 @@ static void getConfigDirs(FFPlatform* platform) {
}
ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData);
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData);
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData);
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
ffPlatformPathAddHome(&platform->configDirs, platform, "");
}
@@ -126,9 +110,9 @@ static void getDataDirs(FFPlatform* platform) {
platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share");
}
ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData);
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData);
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData);
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
ffPlatformPathAddHome(&platform->dataDirs, platform, "");
}
@@ -156,9 +140,25 @@ static void getUserName(FFPlatform* platform) {
TOKEN_USER* tokenUser = (TOKEN_USER*) buf;
UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) };
if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) {
ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer);
uint32_t sidLength = sidString.Length / sizeof(wchar_t);
ffStrbufSetNWS(&platform->sid, sidLength, sidString.Buffer);
// The SID is already in hand, so the profile directory costs one registry read rather
// than a second NtQueryInformationToken(). ffGetProfilePath() needs a null-terminated
// string, which RtlConvertSidToUnicodeString() does not produce.
if (sidLength < ARRAY_SIZE(buffer)) {
buffer[sidLength] = L'\0';
ffGetProfilePath(buffer, &platform->homeDir);
}
}
}
if (platform->homeDir.length == 0) {
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); // Last resort; ffGetProfilePath() covers the normal case
}
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
}
static void getHostName(FFPlatform* platform) {
@@ -280,12 +280,12 @@ void ffPlatformInitImpl(FFPlatform* platform) {
platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess;
getExePath(platform);
getCwd(platform);
getHomeDir(platform);
getUserName(platform);
getCacheDir(platform);
getConfigDirs(platform);
getDataDirs(platform);
getUserName(platform);
getHostName(platform);
getUserShell(platform);
+11
View File
@@ -1,7 +1,9 @@
#include "common/binary.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/strutil.h"
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
@@ -108,6 +110,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
if (is_64) {
const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
if (!header) {
FF_DEBUG("Failed to read the 64-bit mach header at offset %lld", (long long) offset);
return "read mach header failed";
}
@@ -116,6 +119,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
} else {
const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
if (!header) {
FF_DEBUG("Failed to read the mach header at offset %lld", (long long) offset);
return "read mach header failed";
}
@@ -200,6 +204,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
if (!headerRaw) {
FF_DEBUG("Failed to read the fat header");
return "read fat header failed";
}
@@ -251,6 +256,7 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
}
}
FF_DEBUG("No supported Mach-O slice was found in the fat binary");
return "Unsupported fat header";
}
@@ -265,11 +271,13 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(\"%s\") failed: %s", machoFile, strerror(errno));
return "File could not be opened";
}
struct stat st;
if (fstat(fd, &st) != 0 || st.st_size <= 0) {
FF_DEBUG("fstat(\"%s\") failed or the file is empty", machoFile);
return "Failed to stat file";
}
@@ -278,12 +286,14 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
.length = (size_t) st.st_size,
};
if (mapping.data == MAP_FAILED) {
FF_DEBUG("mmap(\"%s\", %lld bytes) failed: %s", machoFile, (long long) st.st_size, strerror(errno));
return "mmap failed";
}
// Read the magic number to determine the type of binary
const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
if (!magic) {
FF_DEBUG("Failed to read the magic number");
return "read magic number failed";
}
@@ -291,6 +301,7 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
// MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
// However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
FF_DEBUG("Unsupported magic number 0x%08x", (unsigned) *magic);
return "Unsupported format or big endian mach-o file";
}
+7
View File
@@ -1,4 +1,5 @@
#include "common/binary.h"
#include "common/debug.h"
#if defined(FF_HAVE_ELF) || defined(__sun) || (defined(__FreeBSD__) && !defined(__DragonFly__)) || defined(__OpenBSD__) || defined(__NetBSD__)
@@ -43,6 +44,7 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
FF_LIBRARY_LOAD_MESSAGE(libelf, "libelf" FF_LIBRARY_EXTENSION, 1);
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_version)
if (elfData.ffelf_version(EV_CURRENT) == EV_NONE) {
FF_DEBUG("elf_version() failed");
return "elf_version() failed";
}
@@ -60,23 +62,27 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
}
if (elfData.ffelf_end == nullptr) {
FF_DEBUG("load libelf failed");
return "load libelf failed";
}
// Open the ELF file
FF_AUTO_CLOSE_FD int fd = open(elfFile, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(%s) failed: %s", elfFile, strerror(errno));
return "open() failed";
}
Elf* elf = elfData.ffelf_begin(fd, ELF_C_READ, nullptr);
if (elf == nullptr) {
FF_DEBUG("elf_begin() failed");
return "elf_begin() failed";
}
// Get the section header string table index
size_t shstrndx = 0;
if (elfData.ffelf_getshdrstrndx(elf, &shstrndx) < 0) {
FF_DEBUG("elf_getshdrstrndx() failed");
elfData.ffelf_end(elf);
return "elf_getshdrstrndx() failed";
}
@@ -141,6 +147,7 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
*/
const char* ffBinaryExtractStrings(const char* file, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
FF_UNUSED(file, cb, userdata, minLength);
FF_DEBUG("Fastfetch was built without libelf support");
return "Fastfetch was built without libelf support";
}
+9 -2
View File
@@ -1,4 +1,5 @@
#include "common/binary.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/strutil.h"
#include "common/windows/nt.h"
@@ -17,22 +18,28 @@
const char* ffBinaryExtractStrings(const char* peFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileA(peFile, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
FF_DEBUG("CreateFileA() failed: %s", ffDebugWin32Error(GetLastError()));
return "CreateFileA() failed";
}
FF_AUTO_CLOSE_FD HANDLE hSection = nullptr;
if (!NT_SUCCESS(NtCreateSection(&hSection, SECTION_MAP_READ, nullptr, nullptr, PAGE_READONLY, SEC_COMMIT, hFile))) {
NTSTATUS status = NtCreateSection(&hSection, SECTION_MAP_READ, nullptr, nullptr, PAGE_READONLY, SEC_COMMIT, hFile);
if (!NT_SUCCESS(status)) {
FF_DEBUG("NtCreateSection() failed: %s", ffDebugNtStatus(status));
return "NtCreateSection() failed";
}
PVOID base = nullptr;
SIZE_T viewSize = 0;
if (!NT_SUCCESS(NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, nullptr, &viewSize, ViewUnmap, 0, PAGE_READONLY))) {
status = NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, nullptr, &viewSize, ViewUnmap, 0, PAGE_READONLY);
if (!NT_SUCCESS(status)) {
FF_DEBUG("NtMapViewOfSection() failed: %s", ffDebugNtStatus(status));
return "NtMapViewOfSection() failed";
}
PIMAGE_NT_HEADERS ntHeaders = RtlImageNtHeader(base);
if (!ntHeaders) {
FF_DEBUG("RtlImageNtHeader() failed");
NtUnmapViewOfSection(NtCurrentProcess(), base);
return "RtlImageNtHeader() failed";
}
+3
View File
@@ -2,6 +2,7 @@
#ifdef FF_HAVE_DBUS
#include "common/debug.h"
#include "common/thread.h"
#include "common/strutil.h"
@@ -39,11 +40,13 @@ static const FFDBusLibrary* loadLib(void) {
const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data) {
data->lib = loadLib();
if (data->lib == nullptr) {
FF_DEBUG("libdbus-1 could not be loaded");
return "Failed to load DBus library";
}
data->connection = data->lib->ffdbus_bus_get(busType, nullptr);
if (data->connection == nullptr) {
FF_DEBUG("dbus_bus_get(busType=%d) returned no connection", (int) busType);
return "Failed to connect to DBus";
}
+108
View File
@@ -0,0 +1,108 @@
#include <errno.h>
#ifndef NDEBUG
#include "common/debug.h"
#include "common/thread.h"
[[gnu::nonnull(1), gnu::pure, gnu::always_inline, nodiscard]]
static inline const char* ffFindFileName(const char* file) {
const char* lastSlash = __builtin_strrchr(file, '/');
#ifdef _WIN32
if (lastSlash == nullptr) {
lastSlash = __builtin_strrchr(file, '\\');
}
#endif
if (lastSlash != nullptr) {
return lastSlash + 1;
}
return file;
}
#if __GNUC__ && !__clang__
[[gnu::optimize("O2")]]
#endif
void ffDebugPrint(const char* file, int line, const char* format, ...) {
if (!instance.config.display.debugMode) {
return;
}
static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER;
// Saved before the lock and restored after the unlock: FF_DEBUG() is called from error paths
// that go on to read `errno`, so nothing this function does -- neither the lock nor the
// formatting -- may be allowed to leave its own value behind.
int errno_ = errno;
ffThreadMutexLock(&debugMutex);
static char errmsg_[1024]; // Don't increase stack size
va_list args;
va_start(args, format);
vsnprintf(errmsg_, sizeof(errmsg_), format, args);
va_end(args);
fprintf(stderr, "[%s%4d, %s] %s\n", ffFindFileName(file), line, ffTimeToTimeStr(ffTimeGetNow()), errmsg_);
ffThreadMutexUnlock(&debugMutex);
errno = errno_;
}
#if _WIN32
#include "common/windows/nt.h"
#include <windows.h>
#include <cfgmgr32.h>
const char* ffDebugWin32Error(DWORD errorCode) {
static char buffer[512];
wchar_t bufferW[256];
ULONG len = FormatMessageW(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
(DWORD) errorCode,
0,
bufferW,
ARRAY_SIZE(bufferW),
nullptr);
if (len == 0) {
snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode);
} else {
// Remove trailing newline
while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) {
--len;
}
if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) {
snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode);
} else {
snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode);
}
}
return buffer;
}
const char* ffDebugConfigRet(CONFIGRET ret) {
return ffDebugWin32Error(CM_MapCrToWin32Err(ret, ERROR_INTERNAL_ERROR));
}
const char* ffDebugNtStatus(NTSTATUS status) {
return ffDebugWin32Error(RtlNtStatusToDosError(status));
}
static inline DWORD HRESULTToWin32Error(HRESULT hr) {
if (SUCCEEDED(hr)) {
return ERROR_SUCCESS;
}
if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
return HRESULT_CODE(hr);
}
return ERROR_INTERNAL_ERROR;
}
const char* ffDebugHResult(HRESULT hr) {
return ffDebugWin32Error(HRESULTToWin32Error(hr));
}
#endif
#endif
-60
View File
@@ -1,60 +0,0 @@
#include "common/debug.h"
#include "common/windows/nt.h"
#include <windows.h>
#include <cfgmgr32.h>
const char* ffDebugWin32Error(DWORD errorCode) {
static char buffer[512];
wchar_t bufferW[256];
ULONG len = FormatMessageW(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
(DWORD) errorCode,
0,
bufferW,
ARRAY_SIZE(bufferW),
nullptr);
if (len == 0) {
snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode);
} else {
// Remove trailing newline
while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) {
--len;
}
if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) {
snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode);
} else {
snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode);
}
}
return buffer;
}
const char* ffDebugConfigRet(CONFIGRET ret) {
return ffDebugWin32Error(CM_MapCrToWin32Err(ret, ERROR_INTERNAL_ERROR));
}
const char* ffDebugNtStatus(NTSTATUS status) {
return ffDebugWin32Error(RtlNtStatusToDosError(status));
}
static inline DWORD HRESULTToWin32Error(HRESULT hr) {
if (SUCCEEDED(hr)) {
return ERROR_SUCCESS;
}
if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
return HRESULT_CODE(hr);
}
return ERROR_INTERNAL_ERROR;
}
const char* ffDebugHResult(HRESULT hr) {
return ffDebugWin32Error(HRESULTToWin32Error(hr));
}
-183
View File
@@ -280,183 +280,6 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
}
#endif
#if FF_HAVE_QUICKJS
#include <quickjs.h>
struct FFQuickJSData {
FF_LIBRARY_SYMBOL(JS_NewRuntime)
FF_LIBRARY_SYMBOL(JS_NewContext)
FF_LIBRARY_SYMBOL(JS_FreeRuntime)
FF_LIBRARY_SYMBOL(JS_EvalThis)
FF_LIBRARY_SYMBOL(JS_GetException)
FF_LIBRARY_SYMBOL(JS_ToCStringLen2)
FF_LIBRARY_SYMBOL(JS_FreeCString)
FF_LIBRARY_SYMBOL(JS_NewStringLen)
FF_LIBRARY_SYMBOL(JS_NewArray)
FF_LIBRARY_SYMBOL(JS_NewBigUint64)
FF_LIBRARY_SYMBOL(JS_SetPropertyUint32)
FF_LIBRARY_SYMBOL(JS_NewObject)
FF_LIBRARY_SYMBOL(JS_SetPropertyStr)
FF_LIBRARY_SYMBOL(JS_FreeValue)
JSRuntime* rt;
JSContext* ctx;
bool inited;
} qjsData;
static const char* loadQuickJSState(void) {
if (qjsData.inited) {
if (qjsData.ctx == nullptr) {
return "QuickJS is not available";
}
return nullptr;
}
qjsData.inited = true;
#ifdef _WIN32
FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs-0" FF_LIBRARY_EXTENSION, 0)
#else
FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs" FF_LIBRARY_EXTENSION, 0)
#endif
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewRuntime)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewContext)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeRuntime)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_EvalThis)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_GetException)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_ToCStringLen2)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeCString)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewStringLen)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewArray)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyUint32)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewObject)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyStr)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeValue)
qjsData.rt = qjsData.ffJS_NewRuntime();
if (qjsData.rt == nullptr) {
return "JS_NewRuntime() failed";
}
qjsData.ctx = qjsData.ffJS_NewContext(qjsData.rt);
if (qjsData.ctx == nullptr) {
qjsData.ffJS_FreeRuntime(qjsData.rt);
qjsData.rt = nullptr;
return "JS_NewContext() failed";
}
libqjs = nullptr; // don't close quickjs
return nullptr;
}
static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) {
const char* err = loadQuickJSState();
if (err) {
ffStrbufAppendF(buffer, "Qjs init error: %s", err);
return false;
}
JSContext* ctx = qjsData.ctx;
JSValue argsObj = qjsData.ffJS_NewObject(ctx);
FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate();
for (uint32_t i = 0; i < numArgs; ++i) {
const FFformatarg* arg = &arguments[i];
JSValue value;
switch (arg->type) {
case FF_ARG_TYPE_INT:
value = JS_NewInt32(ctx, *(int32_t*) arg->value);
break;
case FF_ARG_TYPE_UINT:
value = JS_NewUint32(ctx, *(uint32_t*) arg->value);
break;
case FF_ARG_TYPE_UINT64: {
uint64_t val = *(uint64_t*) arg->value;
if (val <= INT32_MAX) {
value = JS_NewInt32(ctx, (int32_t) val);
} else {
value = JS_NewFloat64(ctx, (double) val);
}
break;
}
case FF_ARG_TYPE_UINT16:
value = JS_NewUint32(ctx, *(uint16_t*) arg->value);
break;
case FF_ARG_TYPE_UINT8:
value = JS_NewUint32(ctx, *(uint8_t*) arg->value);
break;
case FF_ARG_TYPE_FLOAT:
value = JS_NewFloat64(ctx, *(float*) arg->value);
break;
case FF_ARG_TYPE_DOUBLE:
value = JS_NewFloat64(ctx, *(double*) arg->value);
break;
case FF_ARG_TYPE_BOOL:
value = JS_NewBool(ctx, *(bool*) arg->value);
break;
case FF_ARG_TYPE_STRING:
value = qjsData.ffJS_NewStringLen(ctx, (const char*) arg->value, strlen((const char*) arg->value));
break;
case FF_ARG_TYPE_STRBUF: {
const FFstrbuf* sb = (const FFstrbuf*) arg->value;
value = qjsData.ffJS_NewStringLen(ctx, sb->chars, sb->length);
break;
}
case FF_ARG_TYPE_LIST: {
const FFlist* list = (const FFlist*) arg->value;
JSValue arr = qjsData.ffJS_NewArray(ctx);
for (uint32_t li = 0; li < list->length; ++li) {
const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li);
JSValue itemValue = qjsData.ffJS_NewStringLen(ctx, item->chars, item->length);
qjsData.ffJS_SetPropertyUint32(ctx, arr, li, itemValue);
}
value = arr;
break;
}
default:
value = JS_UNDEFINED;
break;
}
if (arg->name && arg->name[0]) {
normalizeArgName(&argNameBuf, arg->name);
} else {
ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1);
}
qjsData.ffJS_SetPropertyStr(ctx, argsObj, argNameBuf.chars, value);
}
JSValue result = qjsData.ffJS_EvalThis(ctx, argsObj, script, scriptLen, "", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_STRICT);
qjsData.ffJS_FreeValue(ctx, argsObj);
bool ret = false;
if (JS_IsException(result)) {
JSValue exc = qjsData.ffJS_GetException(ctx);
const char* message = qjsData.ffJS_ToCStringLen2(ctx, nullptr, exc, false);
qjsData.ffJS_FreeValue(ctx, exc);
ffStrbufAppendF(buffer, "Qjs runtime error: %s", message ?: "unknown");
if (message) {
qjsData.ffJS_FreeCString(ctx, message);
}
} else if (JS_IsUndefined(result)) {
ffStrbufAppendS(buffer, "Qjs result error: undefined result");
} else {
size_t len;
const char* res = qjsData.ffJS_ToCStringLen2(ctx, &len, result, false);
if (res) {
ffStrbufAppendNS(buffer, (uint32_t) len, res);
qjsData.ffJS_FreeCString(ctx, res);
}
ret = true;
}
qjsData.ffJS_FreeValue(ctx, result);
return ret;
}
#endif
// Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none.
// A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
static uint32_t ansiEscapeLength(const char* str) {
@@ -813,12 +636,6 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3
}
bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
#if FF_HAVE_QUICKJS
if (ffStrbufStartsWithS(formatstr, "qjs:")) {
return parseQuickJSString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments);
}
#endif
#if FF_HAVE_LUA
if (ffStrbufStartsWithS(formatstr, "lua:")) {
// If outputFormat starts with "lua:", treat the rest as a Lua script
-12
View File
@@ -178,9 +178,6 @@ void ffDestroyInstance(void) {
#if FF_HAVE_LUA
#include <lua.h>
#endif
#if FF_HAVE_QUICKJS
#include <quickjs.h>
#endif
// Must be in a file compiled with the libfastfetch target, because the FF_HAVE* macros are not defined for the executable targets
void ffListFeatures(void) {
@@ -248,12 +245,6 @@ void ffListFeatures(void) {
#if FF_HAVE_FREETYPE
"freetype\n"
#endif
#if FF_HAVE_PULSE
"libpulse\n"
#endif
#if FF_HAVE_DDCUTIL
"libddcutil\n"
#endif
#if FF_HAVE_ELF || __sun || (__FreeBSD__ && !__DragonFly__) || __OpenBSD__ || __NetBSD__
"libelf\n"
#endif
@@ -295,9 +286,6 @@ void ffListFeatures(void) {
#endif
#if FF_HAVE_LUA
LUA_VERSION "\n"
#endif
#if FF_HAVE_QUICKJS
"QuickJS " FF_STR(QJS_VERSION_MAJOR) "." FF_STR(QJS_VERSION_MINOR) "." FF_STR(QJS_VERSION_PATCH) QJS_VERSION_SUFFIX "\n"
#endif
"",
stdout);
+23 -4
View File
@@ -1,5 +1,6 @@
#include "common/io.h"
#include "fastfetch.h"
#include "common/debug.h"
#include "common/strutil.h"
#include "common/time.h"
@@ -16,7 +17,7 @@
#if FF_HAVE_WORDEXP
#include <wordexp.h>
#else
#elif FF_HAVE_GLOB
#include <glob.h>
#endif
@@ -41,9 +42,11 @@ bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
createSubfolders(fileName);
fd = open(fileName, openFlagsModes, openFlagsRights);
if (fd == -1) {
FF_DEBUG("open(%s) failed after creating the parent folders: %s", fileName, strerror(errno));
return false;
}
} else {
FF_DEBUG("open(%s) failed: %s", fileName, strerror(errno));
return false;
}
}
@@ -109,7 +112,7 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
wordfree(&exp);
#else
#elif FF_HAVE_GLOB
glob_t gb;
if (glob(in, GLOB_NOSORT
@@ -131,6 +134,10 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
globfree(&gb);
#else
FF_UNUSED(in, out)
#endif
return result;
@@ -146,16 +153,19 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
if (ftty < 0) {
ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
if (ftty < 0) {
FF_DEBUG("open(/dev/tty) failed: %s", strerror(errno));
return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
}
if (tcgetattr(ftty, &oldTerm) == -1) {
FF_DEBUG("tcgetattr(/dev/tty) failed: %s", strerror(errno));
return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
}
struct termios newTerm = oldTerm;
newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
FF_DEBUG("tcsetattr(/dev/tty) failed: %s", strerror(errno));
return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
}
atexit(restoreTerm);
@@ -165,7 +175,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
// Give the terminal some time to respond
#ifndef __APPLE__
if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) {
const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS);
if (pollResult <= 0) {
FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s",
pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno));
return "poll(/dev/tty) timeout or failed";
}
#else
@@ -175,7 +188,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
fd_set rd;
FD_ZERO(&rd);
FD_SET(ftty, &rd);
if (select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) {
const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 });
if (selectResult <= 0) {
FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s",
selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno));
return "select(/dev/tty) timeout or failed";
}
}
@@ -191,6 +207,8 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
if (nRead <= 0) {
FF_DEBUG("read(/dev/tty) returned %zd after %zu bytes: %s",
nRead, bytesRead, nRead == 0 ? "the terminal closed the connection" : strerror(errno));
va_end(args);
return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
}
@@ -204,6 +222,7 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
va_end(cargs);
if (ret <= 0) {
FF_DEBUG("vsscanf(\"%s\") matched %d of %d parameters", buffer, ret, nParams);
va_end(args);
return "vsscanf(buffer, format, args) failed";
}
+7 -2
View File
@@ -441,7 +441,9 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
}
while (true) {
if (NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }) != STATUS_WAIT_0) {
NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 });
if (status != STATUS_WAIT_0) {
FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status));
SetConsoleMode(hInput, inputMode);
return "NtWaitForSingleObject() failed or timeout";
}
@@ -471,7 +473,9 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
while (true) {
DWORD bytes = 0;
if (!ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr) || bytes == 0) {
BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr);
if (!readOk || bytes == 0) {
FF_DEBUG("ReadFile() %s, bytes=%lu: %s", readOk ? "returned no data" : "failed", bytes, readOk ? "no error" : ffDebugWin32Error(GetLastError()));
va_end(args);
return "ReadFile() failed";
}
@@ -490,6 +494,7 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
if (ret <= 0) {
va_end(args);
FF_DEBUG("vsscanf(buffer, format, args) failed: %s", strerror(errno));
return "vsscanf(buffer, format, args) failed";
}
if (ret >= nParams) {
+14
View File
@@ -1,5 +1,6 @@
#include "fastfetch.h"
#include "common/color.h"
#include "common/debug.h"
#include "common/jsonconfig.h"
#include "common/printing.h"
#include "common/io.h"
@@ -87,6 +88,7 @@ const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair p
}
}
FF_DEBUG("The enum value %d is not one of the accepted integers", intVal);
return "Invalid enum integer";
} else if (yyjson_is_str(val)) {
const char* strVal = yyjson_get_str(val);
@@ -97,8 +99,11 @@ const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair p
}
}
FF_DEBUG("The enum string \"%s\" is not one of the accepted names", strVal);
return "Invalid enum string";
} else {
FF_DEBUG("The enum value is of JSON type %s, not a string or an integer",
yyjson_get_type_desc(val));
return "Invalid enum value type; must be a string or integer";
}
}
@@ -273,6 +278,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
assert(root);
if (!yyjson_is_obj(root)) {
FF_DEBUG("The config root is of JSON type %s", yyjson_get_type_desc(root));
return "Invalid JSON config format. Root value must be an object";
}
@@ -281,6 +287,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
return nullptr;
}
if (!yyjson_is_arr(modules)) {
FF_DEBUG("'modules' is of JSON type %s", yyjson_get_type_desc(modules));
return "Property 'modules' must be an array of strings or objects";
}
@@ -303,6 +310,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
yyjson_val* conditions = yyjson_obj_get(module, "condition");
if (conditions) {
if (!yyjson_is_obj(conditions)) {
FF_DEBUG("'condition' is of JSON type %s", yyjson_get_type_desc(conditions));
return "Property 'condition' must be an object";
}
@@ -329,6 +337,8 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
if (!unsafe_yyjson_is_bool(previousSucceeded)) {
FF_DEBUG("'condition.succeeded' is of JSON type %s",
yyjson_get_type_desc(previousSucceeded));
return "Property 'succeeded' in 'condition' must be a boolean";
}
gatedOnSucceeded = true;
@@ -355,6 +365,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
if (gatedOnSucceeded && ffStrEqualsIgnCase(yyjson_get_str(yyjson_obj_get(module, "type")), ffCommandModuleInfo.name)) {
yyjson_val* parallel = yyjson_obj_get(module, "parallel");
if (parallel == nullptr || yyjson_get_bool(parallel)) {
FF_DEBUG("The command module at index %zu is gated on 'condition.succeeded' and runs in parallel", idx);
return "Module \"command\" cannot be combined with \"condition.succeeded\" while it runs in "
"parallel: whether it is printed is only known in the print pass, so it cannot be "
"prepared, and the prepared results are matched to modules by position. Set "
@@ -371,12 +382,14 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
type = yyjson_get_str(yyjson_obj_get(module, "type"));
if (!type) {
FF_DEBUG("The module object at index %zu has no \"type\" key, or it is not a string", idx);
return "module object must contain a \"type\" key ( case sensitive )";
}
if (yyjson_obj_size(module) == 1) { // contains only Property type
module = nullptr;
}
} else {
FF_DEBUG("The modules entry at index %zu is of JSON type %s", idx, yyjson_get_type_desc(module));
return "modules must be an array of strings or objects";
}
@@ -417,6 +430,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
return nullptr;
}
[[gnu::cold]]
void ffPrintJsonConfig(FFdata* data, bool prepare) {
yyjson_mut_doc* jsonDoc = data->resultDoc;
const char* error = printJsonConfig(data, prepare);
+3
View File
@@ -1,6 +1,7 @@
#if FF_HAVE_LUA
#include "common/lua.h"
#include "common/debug.h"
#include "common/mallocHelper.h"
struct FFLuaData luaData;
@@ -141,6 +142,7 @@ static int yyjsonEncode(lua_State* L) {
const char* ffLuaLoadState(void) {
if (luaData.inited) {
if (luaData.L == nullptr) {
FF_DEBUG("The Lua state was already initialised and is gone");
return "Lua library is not available";
}
return nullptr;
@@ -212,6 +214,7 @@ const char* ffLuaLoadState(void) {
lua_State* L = ffluaL_newstate();
if (L == nullptr) {
FF_DEBUG("luaL_newstate() could not allocate an interpreter");
return "luaL_newstate() failed";
}
#if LUA_VERSION_NUM >= 505
-190
View File
@@ -1,197 +1,7 @@
#include "fastfetch.h"
#include "common/library.h"
#include "common/networking.h"
#include "common/strutil.h"
#include "common/debug.h"
#ifdef FF_HAVE_ZLIB
#include <zlib.h>
struct FFZlibLibrary {
FF_LIBRARY_SYMBOL(inflateInit2_)
FF_LIBRARY_SYMBOL(inflate)
FF_LIBRARY_SYMBOL(inflateEnd)
bool inited;
} zlibData;
const char* ffNetworkingLoadZlibLibrary(void) {
if (!zlibData.inited) {
zlibData.inited = true;
FF_LIBRARY_LOAD_MESSAGE(zlib,
#ifdef _WIN32
"zlib1"
#else
"libz"
#endif
FF_LIBRARY_EXTENSION,
2)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd)
zlib = nullptr; // don't auto dlclose
}
return zlibData.ffinflateEnd == nullptr ? "Failed to load libz" : nullptr;
}
// Try to pre-read gzip header to determine uncompressed size
static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) {
// gzip file format: http://www.zlib.org/rfc-gzip.html
if (dataSize < 10 || ((const uint8_t*) data)[0] != 0x1f || ((const uint8_t*) data)[1] != 0x8b) {
return 0;
}
// Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB
if (dataSize > 18) {
// Get ISIZE value from the end of file (little endian)
const uint8_t* tail = (const uint8_t*) data + dataSize - 4;
uint32_t uncompressedSize = (uint32_t) tail[0] | ((uint32_t) tail[1] << 8u) | ((uint32_t) tail[2] << 16u) | ((uint32_t) tail[3] << 24u);
// For valid gzip files, this value is the length of the uncompressed data modulo 2^32
if (uncompressedSize > 0) {
FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize);
// Add some margin to the estimated size for safety
return uncompressedSize + 64;
}
}
// If unable to get size from trailer or size is 0, use estimated value
// Typically, text data compression ratio is between 3-5x, we use the larger value
uint32_t estimatedSize = dataSize * 5;
FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize);
return estimatedSize;
}
// Decompress gzip content
bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) {
// `headerEnd` itself is non-null per the `nonnull(2)` contract; what it points to is not expressible as an attribute
assert(*headerEnd == '\r');
// Calculate header size
uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars);
*headerEnd = '\0'; // Replace delimiter with null character for easier processing
// Ensure Content-Encoding is in response headers, not in response body
bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != nullptr;
*headerEnd = '\r'; // Restore delimiter
if (!hasGzip) {
FF_DEBUG("No gzip compressed content detected, skipping decompression");
return true;
}
FF_DEBUG("Gzip compressed content detected, preparing for decompression");
const char* bodyStart = headerEnd + 4; // Skip delimiter
if (buffer->length <= headerSize + 4) {
// No content to decompress
FF_DEBUG("Compressed content size is 0, skipping decompression");
return true;
}
// Calculate compressed content size
uint32_t compressedSize = buffer->length - headerSize - 4;
// Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b)
if (compressedSize < 2 || (uint8_t) bodyStart[0] != 0x1f || (uint8_t) bodyStart[1] != 0x8b) {
FF_DEBUG("Content is not valid gzip format, skipping decompression");
return false;
}
// Predict uncompressed size
uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize);
// Create decompression buffer with estimated size
FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5);
FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated);
// Initialize decompression
z_stream zs = {
.zalloc = Z_NULL,
.zfree = Z_NULL,
.opaque = Z_NULL,
.avail_in = (uInt) compressedSize,
.next_in = (Bytef*) bodyStart,
.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer),
.next_out = (Bytef*) decompressedBuffer.chars,
};
// Initialize decompression engine
if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) {
FF_DEBUG("Failed to initialize decompression engine");
return false;
}
uInt availableOut = zs.avail_out;
// Perform decompression
int result = zlibData.ffinflate(&zs, Z_FINISH);
// If output buffer is insufficient, try to extend buffer
while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) {
FF_DEBUG("Output buffer insufficient, trying to extend");
// Save already decompressed data amount
uint32_t alreadyDecompressed = (uint32_t) (availableOut - zs.avail_out);
decompressedBuffer.length += alreadyDecompressed;
decompressedBuffer.chars[decompressedBuffer.length] = '\0';
ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2);
// Set output parameters to point to new buffer
zs.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer);
zs.next_out = (Bytef*) (decompressedBuffer.chars + decompressedBuffer.length);
availableOut = zs.avail_out;
// Decompress again
result = zlibData.ffinflate(&zs, Z_FINISH);
}
// Check for decompression errors before using result
if (result != Z_STREAM_END) {
FF_DEBUG("Decompression failed with zlib error: %d", result);
zlibData.ffinflateEnd(&zs);
return false;
}
zlibData.ffinflateEnd(&zs);
// Calculate decompressed size (from the last inflate call)
uint32_t decompressedSize = (uint32_t) (availableOut - zs.avail_out);
decompressedBuffer.length += decompressedSize;
decompressedBuffer.chars[decompressedBuffer.length] = '\0';
FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length);
// Modify Content-Length header and remove Content-Encoding header
// Use decompressedBuffer.length (total) not decompressedSize (last chunk only)
FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedBuffer.length + 64);
char* line = nullptr;
size_t len = 0;
while (ffStrbufGetline(&line, &len, buffer)) {
if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) {
continue;
} else if (ffStrStartsWithIgnCase(line, "Content-Length:")) {
ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedBuffer.length);
continue;
} else if (line[0] == '\r') {
ffStrbufAppendS(&newBuffer, "\r\n");
ffStrbufGetlineRestore(&line, &len, buffer);
break;
}
ffStrbufAppendS(&newBuffer, line); // Including the trailing \r
ffStrbufAppendC(&newBuffer, '\n');
}
ffStrbufAppend(&newBuffer, &decompressedBuffer);
ffStrbufDestroy(buffer);
ffStrbufInitMove(buffer, &newBuffer);
return true;
}
#endif // FF_HAVE_ZLIB
const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen) {
uint32_t nameLen = (uint32_t) strlen(name);
uint32_t pos = 0;
+2 -39
View File
@@ -146,6 +146,7 @@ static const char* tryNonThreadingFastPath(FFNetworkingState* state) {
#endif
#else
FF_UNUSED(state);
FF_DEBUG("TFO support is not available");
return "TFO support is not available";
#endif
}
@@ -213,12 +214,6 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos
}
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection
// If compression needs to be enabled
if (state->compression) {
FF_DEBUG("Enabling HTTP content compression");
ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
}
ffStrbufAppendS(&state->command, headers);
ffStrbufAppendS(&state->command, "\r\n");
@@ -330,26 +325,6 @@ error:
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
if (state->compression) {
FF_DEBUG("Compression enabled, checking if zlib is available");
#ifdef FF_HAVE_ZLIB
const char* zlibError = ffNetworkingLoadZlibLibrary();
// Only enable compression if zlib library is successfully loaded
if (zlibError == nullptr) {
FF_DEBUG("Successfully loaded zlib library, compression enabled");
} else {
FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
state->compression = false;
}
#else
FF_DEBUG("zlib not supported at build time, compression disabled");
state->compression = false;
#endif
} else {
FF_DEBUG("Compression disabled");
}
const char* initResult = initNetworkingState(state, host, port, path, headers);
if (initResult != nullptr) {
FF_DEBUG("Initialization failed: %s", initResult);
@@ -593,6 +568,7 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
}
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
FF_DEBUG("Failed to decode chunked response");
return "Failed to decode chunked response";
}
@@ -610,18 +586,5 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "Content length mismatch";
}
// If compression was used, try to decompress
#ifdef FF_HAVE_ZLIB
if (state->compression) {
FF_DEBUG("Content received, checking if compressed");
if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
FF_DEBUG("Decompression failed or invalid compression format");
return "Failed to decompress or invalid format";
} else {
FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
}
}
#endif
return nullptr;
}
+16 -49
View File
@@ -14,8 +14,9 @@ static LPFN_CONNECTEX ConnectEx;
static const char* initWsaData(WSADATA* wsaData) {
FF_DEBUG("Initializing WinSock");
if (WSAStartup(MAKEWORD(2, 2), wsaData) != 0) {
FF_DEBUG("WSAStartup() failed");
int ret = WSAStartup(MAKEWORD(2, 2), wsaData);
if (ret != 0) {
FF_DEBUG("WSAStartup() failed: %s", ffDebugWin32Error((DWORD) ret));
return "WSAStartup() failed";
}
@@ -28,7 +29,7 @@ static const char* initWsaData(WSADATA* wsaData) {
// Dummy socket needed for WSAIoctl
SOCKET sockfd = WSASocketW(AF_INET, SOCK_STREAM, 0, nullptr, 0, 0);
if (sockfd == INVALID_SOCKET) {
FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed");
FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
WSACleanup();
return "WSASocketW(AF_INET, SOCK_STREAM) failed";
}
@@ -36,7 +37,7 @@ static const char* initWsaData(WSADATA* wsaData) {
DWORD dwBytes;
GUID guid = WSAID_CONNECTEX;
if (WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), &dwBytes, nullptr, nullptr) != 0) {
FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed");
FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
closesocket(sockfd);
WSACleanup();
return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed";
@@ -51,24 +52,6 @@ static const char* initWsaData(WSADATA* wsaData) {
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
if (state->compression) {
#ifdef FF_HAVE_ZLIB
const char* zlibError = ffNetworkingLoadZlibLibrary();
// Only enable compression if zlib library is successfully loaded
if (zlibError == nullptr) {
FF_DEBUG("Successfully loaded zlib library, compression enabled");
} else {
FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
state->compression = false;
}
#else
FF_DEBUG("zlib not supported at build time, compression disabled");
state->compression = false;
#endif
} else {
FF_DEBUG("Compression disabled");
}
static WSADATA wsaData;
if (wsaData.wVersion == 0) {
const char* error = initWsaData(&wsaData);
@@ -90,8 +73,9 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
};
wchar_t hostW[256];
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), nullptr, host, (ULONG) strlen(host) + 1))) {
FF_DEBUG("Failed to convert host to wide string: %s", host);
NTSTATUS status = RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), nullptr, host, (ULONG) strlen(host) + 1);
if (!NT_SUCCESS(status)) {
FF_DEBUG("Failed to convert host to wide string: %s: %s", host, ffDebugNtStatus(status));
return "Failed to convert host to wide string";
}
@@ -99,14 +83,15 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
_itow(port, portW, 10);
FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
if (GetAddrInfoW(hostW, portW, &hints, &addr) != 0) {
FF_DEBUG("GetAddrInfoW() failed");
int ret = GetAddrInfoW(hostW, portW, &hints, &addr);
if (ret != 0) {
FF_DEBUG("GetAddrInfoW() failed: %s", ffDebugWin32Error((DWORD) ret));
return "GetAddrInfoW() failed";
}
state->sockfd = WSASocketW(addr->ai_family, addr->ai_socktype, addr->ai_protocol, nullptr, 0, 0);
if (state->sockfd == INVALID_SOCKET) {
FF_DEBUG("WSASocketW() failed");
FF_DEBUG("WSASocketW() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
FreeAddrInfoW(addr);
return "WSASocketW() failed";
}
@@ -149,8 +134,9 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
// Initialize overlapped structure with WSA event for asynchronous I/O
state->overlapped = (OVERLAPPED) {};
if (!NT_SUCCESS(NtCreateEvent(&state->overlapped.hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE))) {
FF_DEBUG("NtCreateEvent() failed");
NTSTATUS eventStatus = NtCreateEvent(&state->overlapped.hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE);
if (!NT_SUCCESS(eventStatus)) {
FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(eventStatus));
closesocket(state->sockfd);
FreeAddrInfoW(addr);
state->sockfd = INVALID_SOCKET;
@@ -176,13 +162,6 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
ffStrbufAppendF(&state->command, ":%u", port);
}
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure
// Add compression support if enabled
if (state->compression) {
FF_DEBUG("Enabling HTTP content compression");
ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
}
ffStrbufAppendS(&state->command, headers);
ffStrbufAppendS(&state->command, "\r\n");
@@ -417,6 +396,7 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
}
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
FF_DEBUG("Failed to decode chunked response");
return "Failed to decode chunked response";
}
@@ -436,18 +416,5 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "Content length mismatch";
}
// If compression was used, try to decompress
#ifdef FF_HAVE_ZLIB
if (state->compression) {
FF_DEBUG("Content received, checking if compressed");
if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
FF_DEBUG("Decompression failed or invalid compression format");
return "Failed to decompress or invalid format";
} else {
FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
}
}
#endif
return nullptr;
}
+4
View File
@@ -1,4 +1,5 @@
#include "common/path.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/arrutil.h"
@@ -6,6 +7,7 @@
const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
char* path = getenv("PATH");
if (!path) {
FF_DEBUG("The PATH environment variable is not set");
return "$PATH not set";
}
@@ -50,6 +52,7 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
return nullptr;
}
ffStrbufClear(result);
FF_DEBUG("%s is not executable in any of the %s entries", name, "PATH");
return "Executable not found";
}
#else
@@ -62,6 +65,7 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
char buffer[MAX_PATH + 1];
DWORD length = SearchPathA(nullptr, name, ".exe", sizeof(buffer), buffer, nullptr);
if (length == 0) {
FF_DEBUG("SearchPathA(%s) failed: %s", name, ffDebugWin32Error(GetLastError()));
ffStrbufClear(result);
return "Executable not found";
}
+11 -2
View File
@@ -1,4 +1,5 @@
#include "fastfetch.h"
#include "common/debug.h"
#include "common/percent.h"
#include "common/color.h"
#include "common/option.h"
@@ -27,6 +28,8 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
yyjson_arr_foreach (jsonVal, idx, max, item) {
const char* flag = yyjson_get_str(item);
if (!flag) {
FF_DEBUG("A percent.type array entry is of JSON type %s, not a string",
yyjson_get_type_desc(item));
return "Error: percent.type: invalid flag string";
}
if (ffStrEqualsIgnCase(flag, "num")) {
@@ -40,6 +43,7 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
} else if (ffStrEqualsIgnCase(flag, "bar-monochrome")) {
flags |= FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT;
} else {
FF_DEBUG("Unknown percent.type flag \"%s\"", flag);
return "Error: percent.type: unknown flag string";
}
}
@@ -48,6 +52,7 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
return nullptr;
}
FF_DEBUG("percent.type is of JSON type %s", yyjson_get_type_desc(jsonVal));
return "Error: usage: percent.type must be a number or an array of strings";
}
@@ -83,8 +88,11 @@ void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConf
FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5);
assert(blocksPercent <= options->barWidth);
uint8_t blocksPercent = 0;
if (percent > 0) {
double blocks = percent / 100.0 * options->barWidth + 0.5;
blocksPercent = (uint8_t) (blocks >= options->barWidth ? options->barWidth : blocks);
}
bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
@@ -285,6 +293,7 @@ bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageMo
return true;
}
[[gnu::cold]]
void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config) {
yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, module, "percent");
yyjson_mut_obj_add_uint(doc, percent, "green", config.green);
+42 -16
View File
@@ -1,4 +1,5 @@
#include "fastfetch.h"
#include "common/debug.h"
#include "common/processing.h"
#include "common/io.h"
#include "common/strutil.h"
@@ -42,28 +43,39 @@ extern char** environ;
enum { FF_PIPE_BUFSIZ = 8192 };
static inline int ffPipe2(int* fds, int flags) {
static inline int ffPipe2(int* readEnd, int* writeEnd, int flags) {
int fds[2];
#ifndef FF_HAVE_PIPE2
if (pipe(fds) == -1) {
return -1;
}
fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | flags);
fcntl(fds[1], F_SETFL, fcntl(fds[1], F_GETFL) | flags);
return 0;
#else
return pipe2(fds, flags);
if (pipe2(fds, flags) == -1) {
return -1;
}
#endif
*readEnd = fds[0];
*writeEnd = fds[1];
return 0;
}
// Not thread-safe
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
int pipes[2];
if (ffPipe2(pipes, O_CLOEXEC) == -1) {
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
FF_AUTO_CLOSE_FD int pipeRead = -1;
FF_AUTO_CLOSE_FD int pipeWrite = -1;
if (ffPipe2(&pipeRead, &pipeWrite, O_CLOEXEC) == -1) {
FF_DEBUG("pipe() failed: %s", strerror(errno));
return "pipe() failed";
}
pid_t childPid = -1;
int nullFile = ffGetNullFD();
// Each stream is wired to the pipe when its bit is set and to the null device otherwise, so an
// unselected stream can neither mix into the captured text nor fill the pipe and block the child.
int stdoutFd = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? pipeWrite : nullFile;
int stderrFd = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? pipeWrite : nullFile;
#if !(__ANDROID__ || __OpenBSD__)
@@ -75,8 +87,8 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_init(&file_actions);
posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
posix_spawn_file_actions_adddup2(&file_actions, stdoutFd, STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&file_actions, stderrFd, STDERR_FILENO);
if (ffIsValidNativeFD(stdinFd)) {
posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO);
}
@@ -123,8 +135,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
posix_spawn_file_actions_destroy(&file_actions);
if (ret != 0) {
close(pipes[0]);
close(pipes[1]);
FF_DEBUG("posix_spawnp() failed: %s", strerror(ret));
if (ret == ENOENT) {
return "command not found";
}
@@ -136,15 +147,14 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
// https://github.com/termux/termux-packages/issues/25369
childPid = fork();
if (childPid == -1) {
close(pipes[0]);
close(pipes[1]);
FF_DEBUG("fork() failed: %s", strerror(errno));
return "fork() failed";
}
if (childPid == 0) {
// Child process
dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
dup2(stdoutFd, STDOUT_FILENO);
dup2(stderrFd, STDERR_FILENO);
if (ffIsValidNativeFD(stdinFd)) {
dup2(stdinFd, STDIN_FILENO);
}
@@ -155,9 +165,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
#endif
close(pipes[1]);
outHandle->pid = childPid;
outHandle->pipeRead = pipes[0];
outHandle->pipeRead = pipeRead;
pipeRead = -1; // ownership moved into outHandle, which ffProcessReadOutput() lets go
return nullptr;
}
@@ -177,6 +187,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
struct pollfd pollfd = { childPipeFd, POLLIN, 0 };
int pollret = poll(&pollfd, 1, timeout);
if (pollret == 0) {
FF_DEBUG("poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)");
kill(childPid, SIGTERM);
waitpid(childPid, nullptr, 0);
return "poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)";
@@ -199,6 +210,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
return "child process exited abnormally";
}
if (WEXITSTATUS(stat_loc) == 127) {
FF_DEBUG("command not found");
return "command not found";
}
// We only handle 127 as an error. See `getTerminalVersionUrxvt` in `terminalshell.c`
@@ -210,6 +222,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
}
}
FF_DEBUG("read(childPipeFd, str, FF_PIPE_BUFSIZ) failed: %s", strerror(errno));
return "read(childPipeFd, str, FF_PIPE_BUFSIZ) failed";
}
@@ -475,6 +488,7 @@ void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, cons
const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, int32_t* tty) {
if (pid <= 0) {
FF_DEBUG("Invalid pid");
return "Invalid pid";
}
@@ -489,6 +503,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
char buf[PROC_FILE_BUFFSIZ];
ssize_t nRead = ffReadFileData(procFilePath, sizeof(buf) - 1, buf);
if (nRead <= 8) {
FF_DEBUG("ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed");
return "ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed";
}
buf[nRead] = '\0'; // pid (comm) state ppid pgrp session tty
@@ -499,16 +514,19 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
// comm in `/proc/pid/stat` is not encoded, and may contain ' ', ')' or even `\n`
const char* start = memchr(buf, '(', (size_t) nRead);
if (!start) {
FF_DEBUG("memchr(stat, '(') failed");
return "memchr(stat, '(') failed";
}
start++;
const char* end = memrchr(start, ')', (size_t) nRead - (size_t) (start - buf));
if (!end) {
FF_DEBUG("memrchr(stat, ')') failed");
return "memrchr(stat, ')') failed";
}
ffStrbufSetNS(name, (uint32_t) (end - start), start);
ffStrbufTrimRightSpace(name);
if (name->chars[0] == '\0') {
FF_DEBUG("process name is empty");
return "process name is empty";
}
pState = end + 2; // skip ") "
@@ -520,6 +538,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
{
int ppid_, tty_;
if (sscanf(pState + 2, "%d %*d %*d %d", &ppid_, &tty_) < 2) {
FF_DEBUG("sscanf(stat) failed");
return "sscanf(stat) failed";
}
@@ -536,6 +555,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/comm", (int) pid);
ssize_t nRead = ffReadFileBuffer(procFilePath, name);
if (nRead <= 0) {
FF_DEBUG("ffReadFileBuffer(/proc/pid/comm, name) failed");
return "ffReadFileBuffer(/proc/pid/comm, name) failed";
}
ffStrbufTrimRightSpace(name);
@@ -548,6 +568,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
size_t size = sizeof(proc);
if (sysctl(
(int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, nullptr, 0)) {
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
return "sysctl(KERN_PROC_PID) failed";
}
@@ -574,6 +595,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
size_t size = sizeof(proc);
if (sysctl(
(int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, nullptr, 0)) {
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
return "sysctl(KERN_PROC_PID) failed";
}
@@ -600,6 +622,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
size_t size = sizeof(proc);
if (sysctl(
(int[]) { CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, sizeof(proc), 1 }, 6, &proc, &size, nullptr, 0) != 0) {
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
return "sysctl(KERN_PROC_PID) failed";
}
@@ -625,6 +648,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
snprintf(path, sizeof(path), "/proc/%d/psinfo", (int) pid);
psinfo_t proc;
if (ffReadFileData(path, sizeof(proc), &proc) != sizeof(proc)) {
FF_DEBUG("ffReadFileData(psinfo) failed");
return "ffReadFileData(psinfo) failed";
}
@@ -650,6 +674,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
*tty = (int) proc.p_tdev;
}
} else {
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
return "sysctl(KERN_PROC_PID) failed";
}
@@ -667,6 +692,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
#else
FF_DEBUG("Unsupported platform");
return "Unsupported platform";
#endif
+36 -14
View File
@@ -1,4 +1,5 @@
#include "fastfetch.h"
#include "common/debug.h"
#include "common/mallocHelper.h"
#include "common/processing.h"
#include "common/io.h"
@@ -28,7 +29,7 @@ static void argvToCmdline(char* const argv[], FFstrbuf* result) {
}
// Add quotes around string if whitespace chars are present (with slash duplicating at the end of string)
if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) {
if (ffStrbufContainAnyC(&temp, " \t")) {
uint32_t pos = temp.length;
uint32_t cnt = 0;
while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
@@ -83,7 +84,7 @@ static wchar_t* createChildEnvironment(void) {
return result;
}
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
const int32_t timeout = instance.config.general.processingTimeout;
wchar_t pipeName[32];
@@ -100,6 +101,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
0,
nullptr);
if (hChildPipeRead == INVALID_HANDLE_VALUE) {
FF_DEBUG("CreateNamedPipeW() failed: %s", ffDebugWin32Error(GetLastError()));
return "CreateNamedPipeW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
}
@@ -116,6 +118,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
0,
nullptr);
if (hChildPipeWrite == INVALID_HANDLE_VALUE) {
FF_DEBUG("CreateFileW() failed: %s", ffDebugWin32Error(GetLastError()));
return "CreateFileW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
}
@@ -124,13 +127,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
.cb = sizeof(siStartInfo),
.dwFlags = STARTF_USESTDHANDLES,
};
if (useStdErr) {
siStartInfo.hStdOutput = ffGetNullFD();
siStartInfo.hStdError = hChildPipeWrite;
} else {
siStartInfo.hStdOutput = hChildPipeWrite;
siStartInfo.hStdError = ffGetNullFD();
}
HANDLE nullFile = ffGetNullFD();
// A stream wired to the pipe is captured; the other goes to the null device so it cannot mix
// into the captured text or fill the pipe and block the child. Both may share the pipe handle.
siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile;
siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile;
if (ffIsValidNativeFD(stdinFd)) {
siStartInfo.hStdInput = stdinFd;
}
@@ -139,9 +141,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
{
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
argvToCmdline(argv, &buf);
FF_DEBUG("Command line: %s", buf.chars);
uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
cmdline = malloc(cmdlineBytes);
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1))) {
NTSTATUS status = RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1);
if (!NT_SUCCESS(status)) {
FF_DEBUG("RtlUTF8ToUnicodeN() failed: %s", ffDebugNtStatus(status));
return "RtlUTF8ToUnicodeN() failed";
}
}
@@ -162,9 +167,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
NtClose(hChildPipeWrite);
if (!success) {
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
FF_DEBUG("Command not found: %s (%s)", argv[0], ffDebugWin32Error(error));
return "command not found";
}
FF_DEBUG("CreateProcessW(%s) failed: %s", argv[0], ffDebugWin32Error(error));
return "CreateProcessW() failed";
}
@@ -197,7 +205,9 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
handle->pid = INVALID_HANDLE_VALUE;
handle->pipeRead = INVALID_HANDLE_VALUE;
if (timeout >= 0 && !NT_SUCCESS(NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE))) {
NTSTATUS createStatus = STATUS_SUCCESS;
if (timeout >= 0 && !NT_SUCCESS(createStatus = NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE))) {
FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(createStatus));
return "NtCreateEvent() failed";
}
@@ -216,7 +226,8 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
nullptr,
nullptr);
if (status == STATUS_PENDING) {
switch (NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 })) {
NTSTATUS waitStatus = NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 });
switch (waitStatus) {
case STATUS_WAIT_0:
status = iosb.Status;
break;
@@ -227,6 +238,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
}
default:
FF_DEBUG("NtWaitForSingleObject(hReadEvent) failed: %s", ffDebugNtStatus(waitStatus));
terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
return "NtWaitForSingleObject(hReadEvent) failed";
}
@@ -237,6 +249,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
}
if (!NT_SUCCESS(status)) {
FF_DEBUG("NtReadFile(hChildPipeRead) failed: %s", ffDebugNtStatus(status));
terminateChildProcess(hProcess, hChildPipeRead, nullptr, &iosb);
return "NtReadFile(hChildPipeRead) failed";
}
@@ -248,12 +261,21 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
exit: {
PROCESS_BASIC_INFORMATION info = {};
ULONG size;
if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) {
NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size);
if (NT_SUCCESS(status)) {
assert(size == sizeof(info));
if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
if (info.ExitStatus == 9009) {
// Note: CMD swallows 9009 errorlevel and returns 1 by default.
// One must use `& call exit %^ERRORLEVEL%` to propagate the correct error code.
FF_DEBUG("Child process exited with error code 9009 (command not found)");
return "command not found";
}
FF_DEBUG("Child process exited with an error: %x %s", (unsigned) info.ExitStatus, ffDebugNtStatus(info.ExitStatus));
return "Child process exited with an error";
}
} else {
FF_DEBUG("NtQueryInformationProcess(ProcessBasicInformation) failed: %s", ffDebugNtStatus(status));
return "NtQueryInformationProcess(ProcessBasicInformation) failed";
}
}
+7
View File
@@ -1,17 +1,22 @@
#include "common/sysctl.h"
#include "common/debug.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#ifdef __OpenBSD__
const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result) {
size_t neededLength;
if (sysctl((int[]) { mib1, mib2 }, 2, nullptr, &neededLength, nullptr, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
FF_DEBUG("sysctl({%d, %d}) length query failed: %s", mib1, mib2, strerror(errno));
return "sysctl() length query failed";
}
ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
if (sysctl((int[]) { mib1, mib2 }, 2, result->chars + result->length, &neededLength, nullptr, 0) != 0) {
FF_DEBUG("sysctl({%d, %d}) failed to retrieve %zu bytes: %s", mib1, mib2, neededLength, strerror(errno));
return "sysctl() failed to retrieve string data";
}
@@ -62,12 +67,14 @@ int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue) {
const char* ffSysctlGetString(const char* propName, FFstrbuf* result) {
size_t neededLength;
if (sysctlbyname(propName, nullptr, &neededLength, nullptr, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
FF_DEBUG("sysctlbyname(%s) length query failed: %s", propName, strerror(errno));
return "sysctlbyname() failed";
}
ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
if (sysctlbyname(propName, result->chars + result->length, &neededLength, nullptr, 0) != 0) {
FF_DEBUG("sysctlbyname(%s) failed to retrieve %zu bytes: %s", propName, neededLength, strerror(errno));
return "sysctlbyname() failed to retrieve string data";
}
+1
View File
@@ -154,6 +154,7 @@ bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, F
return true;
}
[[gnu::cold]]
void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config) {
if (!temp) {
yyjson_mut_obj_add_bool(doc, module, "temp", false);
+81
View File
@@ -0,0 +1,81 @@
#include "common/zlib.h"
#include "common/debug.h"
#if FF_HAVE_ZLIB && !FF_DISABLE_DLOPEN
struct FFZlibData zlibData;
// Opens the library and leaves the handle in zlibData, so the groups that follow resolve against the
// same mapping instead of opening their own.
static const char* openZlib(void) {
#ifdef _WIN32
FF_LIBRARY_LOAD_MESSAGE(zlib, "zlib1" FF_LIBRARY_EXTENSION, 2)
#else
FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2)
#endif
zlibData.handle = zlib;
zlib = nullptr; // the handle is kept in zlibData, so the cleanup must not close it
return nullptr;
}
const char* ffZlibLoad(FFZlibGroup group) {
FF_DEBUG("Loading zlib group: %x", group);
// Opened once, by whichever group is asked for first; the later ones reuse that mapping. A
// failed open is retried by the next group rather than cached, so a system without libz pays at
// most one attempt per group -- not one per call.
if (zlibData.handle == nullptr) {
if (zlibData.attempted == UINT8_MAX) {
return "Previous attempt to open zlib failed";
}
const char* error = openZlib();
if (error != nullptr) {
static_assert(sizeof(zlibData.attempted) == 1, "zlibData.attempted must be 1 byte");
zlibData.attempted = UINT8_MAX;
return error;
}
}
if (group & FF_ZLIB_COMPRESS) {
if (zlibData.attempted & FF_ZLIB_COMPRESS) {
if (!zlibData.ffcompress2) {
return "Previous attempt to load compress failed";
}
} else {
zlibData.attempted |= FF_ZLIB_COMPRESS;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compressBound)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compress2)
}
}
if (group & FF_ZLIB_INFLATE) {
if (zlibData.attempted & FF_ZLIB_INFLATE) {
if (!zlibData.ffinflateEnd) {
return "Previous attempt to load inflate failed";
}
} else {
zlibData.attempted |= FF_ZLIB_INFLATE;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateInit2_)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflate)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateEnd)
}
}
if (group & FF_ZLIB_GZFILE) {
if (zlibData.attempted & FF_ZLIB_GZFILE) {
if (!zlibData.ffgzclose) {
return "Previous attempt to load gzfile failed";
}
} else {
zlibData.attempted |= FF_ZLIB_GZFILE;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzopen)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzread)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzclose)
}
}
return nullptr;
}
#endif
+1
View File
@@ -13,6 +13,7 @@
#define LUALIB_API LUA_API
#define LUAMOD_API LUA_API
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
+1 -7
View File
@@ -25,7 +25,6 @@ typedef struct FFNetworkingState {
FFstrbuf command;
uint32_t timeout;
bool ipv6;
bool compression; // if true, HTTP content compression will be enabled if supported
bool tfo; // if true, TCP Fast Open will be attempted first, and fallback to traditional connection if it fails
} FFNetworkingState;
@@ -62,7 +61,7 @@ typedef struct FFNetworkingState {
// `Content-Length` header in place of `Transfer-Encoding`.
// `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller
// passed in no longer describes the buffer afterwards. Callers must use the new one for anything
// that addresses the body, such as ffNetworkingDecompressGzip().
// that addresses the body.
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd);
// Result of parsing a `Transfer-Encoding` header value
@@ -78,8 +77,3 @@ typedef enum FFNetworkingTransferEncoding {
// client cannot decode. Only a lone `chunked` is accepted; everything else is reported
// as unsupported so that the caller fails the response instead of returning garbage.
[[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen);
#ifdef FF_HAVE_ZLIB
const char* ffNetworkingLoadZlibLibrary(void);
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd);
#endif
+18 -17
View File
@@ -21,23 +21,24 @@ typedef struct FFModuleFormatArgList {
typedef struct FFModuleDisplayName {
const char* en; // English
const char* ar; // Arabic
const char* cs; // Czech
const char* de; // German
const char* es; // Spanish
const char* fr; // French
const char* gl; // Galician
const char* he; // Hebrew
const char* id; // Indonesian
const char* it; // Italian
const char* ja; // Japanese
const char* ko; // Korean
const char* pl; // Polish
const char* pt; // (Brazilian) Portuguese
const char* ru; // Russian
const char* tr; // Turkish
const char* uk; // Ukrainian
const char* vi; // Vietnamese
const char* ar; // Arabic
const char* cs; // Czech
const char* de; // German
const char* es; // Spanish
const char* fi; // Finnish
const char* fr; // French
const char* gl; // Galician
const char* he; // Hebrew
const char* id; // Indonesian
const char* it; // Italian
const char* ja; // Japanese
const char* ko; // Korean
const char* pl; // Polish
const char* pt; // (Brazilian) Portuguese
const char* ru; // Russian
const char* tr; // Turkish
const char* uk; // Ukrainian
const char* vi; // Vietnamese
const char* zh_CN; // Simplified Chinese
const char* zh_TW; // Traditional Chinese
} FFModuleDisplayName;
+14 -3
View File
@@ -17,18 +17,29 @@ typedef struct FFProcessHandle {
#endif
} FFProcessHandle;
// Which of the child's two output streams are wired to the pipe that `ffProcessReadOutput()` drains.
// The stream that is not selected is pointed at the null device, so it can neither interleave with
// the captured text nor fill the pipe and block the child. This is a bitfield rather than a value
// because `BOTH` is a combination: each backend sets up the child's two stdio slots independently,
// testing one bit each.
typedef enum FFProcessOutputType: uint8_t {
FF_PROCESS_OUTPUT_STDOUT_BIT = 1 << 0,
FF_PROCESS_OUTPUT_STDERR_BIT = 1 << 1,
FF_PROCESS_OUTPUT_BOTH = FF_PROCESS_OUTPUT_STDOUT_BIT | FF_PROCESS_OUTPUT_STDERR_BIT,
} FFProcessOutputType;
// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from
// our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do.
// Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the
// service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then
// only reports `Failure calling service <name>: Failed transaction`.
#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1)
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle);
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle);
const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally
static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) {
FFProcessHandle handle;
const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle);
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, FF_PROCESS_INHERIT_STDIN, &handle);
if (error) {
return error;
}
@@ -42,7 +53,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar
static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) {
FFProcessHandle handle;
const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle);
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDERR_BIT, FF_PROCESS_INHERIT_STDIN, &handle);
if (error) {
return error;
}
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#include "common/FFstrbuf.h"
#include "fastfetch.h"
// Not `pure`: it trims the trailing spaces of `value` before reporting whether it is set
[[gnu::nonnull(1), nodiscard]] bool ffIsSmbiosValueSet(FFstrbuf* value);
+32 -8
View File
@@ -1,10 +1,34 @@
#!/bin/sh
# Convert logo.svg to logo.ico
rsvg-convert -w 16 -h 16 logo.svg > logo16.png
rsvg-convert -w 32 -h 32 logo.svg > logo32.png
rsvg-convert -w 48 -h 48 logo.svg > logo48.png
rsvg-convert -w 64 -h 64 logo.svg > logo64.png
rsvg-convert -w 128 -h 128 logo.svg > logo128.png
rsvg-convert -w 256 -h 256 logo.svg > logo256.png
convert logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png logo.ico
# Convert logo.svg to logo.ico.
#
# Every frame is stored as PNG. ImageMagick's ICO writer only uses PNG for frames that are
# 256x256 or larger and falls back to an uncompressed DIB for everything smaller, which
# inflates the icon from ~19 KB to ~108 KB (and to ~370 KB on ImageMagick 7, which also
# stores 256x256 as a DIB). Packing the frames here keeps the icon small and predictable.
set -e
cd "$(dirname "$0")"
for size in 16 32 48 64 128 256; do
rsvg-convert -w "$size" -h "$size" ../../logo/logo.svg > "logo$size.png"
done
python3 - <<'PY'
import struct
sizes = [16, 32, 48, 64, 128, 256]
frames = [open(f"logo{s}.png", "rb").read() for s in sizes]
header = struct.pack("<HHH", 0, 1, len(sizes))
offset = 6 + 16 * len(sizes)
entries = b""
for size, frame in zip(sizes, frames):
dim = 0 if size == 256 else size # 0 means 256 in the ICO directory
entries += struct.pack("<BBBBHHII", dim, dim, 0, 0, 1, 32, len(frame), offset)
offset += len(frame)
with open("logo.ico", "wb") as f:
f.write(header + entries + b"".join(frames))
PY
rm logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png
+8
View File
@@ -1,4 +1,5 @@
#include "com.h"
#include "common/debug.h"
#include <stdlib.h>
@@ -14,15 +15,19 @@ static const char* doInitCom() {
if (FAILED(res)) {
switch (res) {
case E_INVALIDARG:
FF_DEBUG("RoInitialize() failed: invalid argument: 0x%08lX (%s)", res, ffDebugHResult(res));
return "RoInitialize() failed: invalid argument";
case E_OUTOFMEMORY:
FF_DEBUG("RoInitialize() failed: out of memory: 0x%08lX (%s)", res, ffDebugHResult(res));
return "RoInitialize() failed: out of memory";
case E_UNEXPECTED:
FF_DEBUG("RoInitialize() failed: unexpected error: 0x%08lX (%s)", res, ffDebugHResult(res));
return "RoInitialize() failed: unexpected error";
case RPC_E_CHANGED_MODE:
// COM was already initialized with a different concurrency model
return nullptr;
default:
FF_DEBUG("RoInitialize() failed: unknown error: 0x%08lX (%s)", res, ffDebugHResult(res));
return "RoInitialize() failed: unknown error";
}
}
@@ -42,13 +47,16 @@ static const char* doInitCom() {
if (FAILED(res)) {
switch (res) {
case E_INVALIDARG:
FF_DEBUG("CoInitializeEx() failed: invalid argument: 0x%08lX (%s)", res, ffDebugHResult(res));
return "CoInitializeEx() failed: invalid argument";
case E_OUTOFMEMORY:
FF_DEBUG("CoInitializeEx() failed: out of memory: 0x%08lX (%s)", res, ffDebugHResult(res));
return "CoInitializeEx() failed: out of memory";
case RPC_E_CHANGED_MODE:
// COM was already initialized with a different concurrency model
return nullptr;
default:
FF_DEBUG("CoInitializeEx() failed: unknown error: 0x%08lX (%s)", res, ffDebugHResult(res));
return "CoInitializeEx() failed: unknown error";
}
}
+237
View File
@@ -0,0 +1,237 @@
#include "folders.h"
#include "common/debug.h"
#include "common/mallocHelper.h"
#include "common/windows/nt.h"
#include "common/windows/registry.h"
#include "common/windows/unicode.h"
#include <ntstatus.h>
// Where the shell keeps the values SHGetKnownFolderPath() would compute.
//
// `User Shell Folders` is the store folder redirection writes to -- HKCU for the user's own
// folders, HKLM for the machine-wide ones -- and it holds the *unexpanded* form, which is why the
// environment expansion below is not optional. `ProfileList\<SID>\ProfileImagePath` is what the
// logon session recorded as the profile directory, and is read by ffGetProfilePath().
#define FF_FOLDERS_HKCU_SHELL_FOLDERS L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
#define FF_FOLDERS_HKLM_SHELL_FOLDERS L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
#define FF_FOLDERS_HKLM_CURRENT_VERSION L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion"
#define FF_FOLDERS_HKLM_PROFILE_LIST L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
// The environment fallbacks. Two of the registry values above are self-referential -- `Common
// AppData` is literally `%ProgramData%` -- so these are the same answer, not a different one.
#define FF_FOLDERS_ENV_LOCAL_APP_DATA L"%LOCALAPPDATA%"
#define FF_FOLDERS_ENV_ROAMING_APP_DATA L"%APPDATA%"
#define FF_FOLDERS_ENV_PROGRAM_DATA L"%ProgramData%"
#define FF_FOLDERS_ENV_PROGRAM_FILES L"%ProgramFiles%"
// Every folder is resolved once and then kept. ffGetKnownFolderPath() is called about a dozen times
// per process -- getCacheDir, getConfigDirs and getDataDirs all ask for the same folders -- and
// each answer is a pure function of the registry and this process' environment. `states` separates
// "not looked up yet" from "looked up and not there", which the path itself cannot: an empty
// FFstrbuf is a valid answer.
//
// No lock guards this: the only thread fastfetch ever creates is the networking module's
// asynchronous connect, and no module that resolves a known folder runs on it.
typedef enum FFFolderState : uint8_t {
FF_FOLDER_UNRESOLVED,
FF_FOLDER_FOUND,
FF_FOLDER_MISSING,
} FFFolderState;
static FFstrbuf folderPaths[FF_KNOWN_FOLDER_COUNT];
static FFFolderState folderStates[FF_KNOWN_FOLDER_COUNT];
// Expands `%NAME%` placeholders from this process' environment block.
//
// The work is done in UTF-16 so that a user name outside the ANSI code page survives: getenv()
// goes through the CRT's narrow copy of the environment and would mangle it.
//
// RtlExpandEnvironmentStrings() substitutes nothing and still reports success for a variable it
// cannot find (measured), so a leftover '%' is treated as a failure. A directory name that
// legitimately contains one is a false negative, and the only thing that costs is the caller's
// fallback.
static bool expandEnv(const wchar_t* source, uint32_t sourceLen, FFstrbuf* result) {
// Asking for the required length is done with a null destination, which the call reports as
// STATUS_BUFFER_TOO_SMALL -- a *failure* status that still fills in `size`. The status is
// therefore deliberately not checked here; a call that writes nothing leaves `size` at zero.
SIZE_T size = 0; // in characters, including the null terminator
(void) RtlExpandEnvironmentStrings(nullptr, source, sourceLen, nullptr, 0, &size);
if (size == 0) {
return false;
}
FF_AUTO_FREE wchar_t* expanded = (wchar_t*) malloc(size * sizeof(wchar_t));
SIZE_T written = 0;
if (!NT_SUCCESS(RtlExpandEnvironmentStrings(nullptr, source, sourceLen, expanded, size, &written)) || written == 0) {
return false;
}
if (wcschr(expanded, L'%')) {
return false;
}
ffStrbufSetNWS(result, (uint32_t) (written - 1), expanded);
return true;
}
static bool expandEnvZ(const wchar_t* source, FFstrbuf* result) {
return expandEnv(source, (uint32_t) wcslen(source), result);
}
// `raw` is the payload of a REG_SZ / REG_EXPAND_SZ value: UTF-16, usually null terminated.
static bool expandBuffer(const FFArgBuffer* raw, FFstrbuf* result) {
uint32_t length = raw->length / sizeof(wchar_t);
if (length == 0) {
return false;
}
const wchar_t* valueW = (const wchar_t*) raw->data;
if (valueW[length - 1] == L'\0') {
--length;
}
return length > 0 && expandEnv(valueW, length, result);
}
// FF_ARG_TYPE_BUFFER rather than FF_ARG_TYPE_STRBUF, because the value has to stay UTF-16 for
// expandEnv(); the strbuf flavour converts it to UTF-8 on the way out. The buffer flavour does not
// report the registry type either, so REG_SZ and REG_EXPAND_SZ are not told apart -- expanding a
// string without placeholders is a no-op, which is what makes that acceptable.
static bool readExpandedValue(HANDLE hKey, const wchar_t* valueNameW, FFstrbuf* result) {
FFArgBuffer raw = {};
if (!ffRegReadData(hKey, valueNameW, &raw, nullptr)) {
return false;
}
FF_AUTO_FREE void* data = raw.data;
return expandBuffer(&raw, result);
}
static bool readExpandedValueFrom(HKEY hRootKey, const wchar_t* subKeyW, const wchar_t* valueNameW, FFstrbuf* result) {
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
if (!ffRegOpenKeyForRead(hRootKey, subKeyW, &hKey, nullptr)) {
return false;
}
return readExpandedValue(hKey, valueNameW, result);
}
// The SID comes from the caller rather than from a token query here: FFPlatform_windows.c already
// resolves it for `platform->sid`, and the two are the same SID by definition.
bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result) {
assert(sidW);
FF_AUTO_CLOSE_FD HANDLE hProfileList = nullptr;
if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_PROFILE_LIST, &hProfileList, nullptr)) {
FF_DEBUG("Cannot open %ls", FF_FOLDERS_HKLM_PROFILE_LIST);
return false;
}
FF_AUTO_CLOSE_FD HANDLE hProfile = nullptr;
if (!ffRegOpenSubkeyForRead(hProfileList, sidW, &hProfile, nullptr)) {
FF_DEBUG("No profile is registered for %ls", sidW);
return false;
}
return readExpandedValue(hProfile, L"ProfileImagePath", result);
}
// `Local AppData` and `AppData` share a key, so they are read together.
//
// The names have to be exact, spaces and all: `Local AppData` is `Local`, one space, `AppData` --
// there is no `Local App Data`, and asking for it fails the whole batch below.
static const struct {
FFKnownFolder folder;
const wchar_t* valueNameW;
const wchar_t* envW;
} shellFolderEntries[] = {
{ FF_KNOWN_FOLDER_LOCAL_APP_DATA, L"Local AppData", FF_FOLDERS_ENV_LOCAL_APP_DATA },
{ FF_KNOWN_FOLDER_ROAMING_APP_DATA, L"AppData", FF_FOLDERS_ENV_ROAMING_APP_DATA },
};
// One open plus one NtQueryMultipleValueKey resolves both -- and whichever of the two is asked for
// first caches the pair, which is what the callers want anyway.
//
// NtQueryMultipleValueKey is all-or-nothing: one absent name fails the batch, which would push
// *both* folders to the environment fallback. That is survivable, because these values are
// themselves just `%LOCALAPPDATA%` and `%APPDATA%` expanded -- but it is also invisible, and an
// invisible fallback is how a wrong value name went unnoticed here. So a failed batch is retried
// one value at a time, and the extra queries are only paid when the batch has already failed.
static void resolveUserShellFolders(void) {
FFArgBuffer buffers[ARRAY_SIZE(shellFolderEntries)] = {};
FFRegValueArg args[ARRAY_SIZE(shellFolderEntries)];
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
args[i] = (FFRegValueArg) { .type = FF_ARG_TYPE_BUFFER, .value = &buffers[i], .name = shellFolderEntries[i].valueNameW };
}
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, FF_FOLDERS_HKCU_SHELL_FOLDERS, &hKey, nullptr)) {
if (ffRegReadValues(hKey, ARRAY_SIZE(shellFolderEntries), args, nullptr)) {
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
if (expandBuffer(&buffers[i], &folderPaths[shellFolderEntries[i].folder])) {
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
}
}
} else {
FF_DEBUG("Batched read of %ls failed; retrying one value at a time", FF_FOLDERS_HKCU_SHELL_FOLDERS);
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
if (readExpandedValue(hKey, shellFolderEntries[i].valueNameW, &folderPaths[shellFolderEntries[i].folder])) {
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
}
}
}
}
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
// Every buffer has to be freed even when the read failed part way through, which is why the
// free is not tied to the success path above.
free(buffers[i].data);
FFKnownFolder folder = shellFolderEntries[i].folder;
if (folderStates[folder] == FF_FOLDER_UNRESOLVED &&
expandEnvZ(shellFolderEntries[i].envW, &folderPaths[folder])) {
folderStates[folder] = FF_FOLDER_FOUND;
}
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
folderStates[folder] = FF_FOLDER_MISSING;
}
}
}
static bool resolveFolder(FFKnownFolder folder) {
switch (folder) {
case FF_KNOWN_FOLDER_LOCAL_APP_DATA:
case FF_KNOWN_FOLDER_ROAMING_APP_DATA:
resolveUserShellFolders(); // Resolves both of them
return folderStates[folder] == FF_FOLDER_FOUND;
case FF_KNOWN_FOLDER_PROGRAM_DATA:
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_SHELL_FOLDERS, L"Common AppData", &folderPaths[folder]) ||
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_DATA, &folderPaths[folder]);
case FF_KNOWN_FOLDER_PROGRAM_FILES:
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_CURRENT_VERSION, L"ProgramFilesDir", &folderPaths[folder]) ||
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_FILES, &folderPaths[folder]);
default:
assert(false);
return false;
}
}
bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result) {
assert(result);
assert(folder < FF_KNOWN_FOLDER_COUNT);
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
folderStates[folder] = resolveFolder(folder) ? FF_FOLDER_FOUND : FF_FOLDER_MISSING;
}
if (folderStates[folder] != FF_FOLDER_FOUND) {
return false;
}
ffStrbufSet(result, &folderPaths[folder]);
return true;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "fastfetch.h"
#include "common/io.h"
#include <wchar.h> // ffGetProfilePath() takes a SID as a wide string
// The known folders fastfetch needs. They are spelled out instead of taking a REFKNOWNFOLDERID so
// that this header does not have to drag in <shlobj.h> -- the call sites include it for reasons of
// their own, and a header that only works when <windows.h> came first is a trap.
typedef enum FFKnownFolder : uint8_t {
FF_KNOWN_FOLDER_LOCAL_APP_DATA,
FF_KNOWN_FOLDER_ROAMING_APP_DATA,
FF_KNOWN_FOLDER_PROGRAM_DATA,
FF_KNOWN_FOLDER_PROGRAM_FILES,
FF_KNOWN_FOLDER_COUNT,
} FFKnownFolder;
// Resolves a known folder without initializing the shell.
//
// The first SHGetKnownFolderPath() call in a process costs ~2.4 ms (measured on Windows 10 22H2):
// the shell's known-folder machinery is built on demand, and whichever folder is asked for first
// pays for it. Every later call is free, so a process that needs a handful of folders pays that
// price once and can never amortize it. Each folder below is instead one registry value or one
// environment variable away -- ~0.03 ms for all of them together -- so the shell is never touched.
//
// The answer is cached: these five folders are asked for about a dozen times per process, and each
// answer is a pure function of the registry and this process' environment. That is safe without a
// lock, because nothing that resolves a known folder ever runs on the one thread fastfetch creates.
//
// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the
// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes,
// exactly like SHGetKnownFolderPath().
bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result);
// Resolves the profile directory recorded for `sidW`, a SID string as produced by
// RtlConvertSidToUnicodeString().
//
// There is no FF_KNOWN_FOLDER_PROFILE because the caller already has the SID:
// FFPlatform_windows.c resolves it once for `platform->sid`, and deriving it a second time here
// would mean a second NtQueryInformationToken(). The answer is deliberately not cached -- this is
// called once per process, and from one place.
//
// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the
// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes,
// exactly like SHGetKnownFolderPath().
bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 19 KiB

-1
View File
@@ -1 +0,0 @@
../../logo/logo.svg
+24 -3
View File
@@ -171,9 +171,6 @@ NTSTATUS NTAPI NtQuerySystemEnvironmentValueEx(
_Out_opt_ PULONG Attributes // EFI_VARIABLE_*
);
NTSTATUS NTAPI RtlGUIDFromString(IN PCUNICODE_STRING GuidString, OUT GUID* Guid);
NTSTATUS NTAPI RtlStringFromGUIDEx(IN const GUID* Guid, OUT PUNICODE_STRING GuidString, IN BOOLEAN AllocateGuidString);
typedef struct _SYSTEM_SECUREBOOT_INFORMATION {
BOOLEAN SecureBootEnabled;
BOOLEAN SecureBootCapable;
@@ -722,6 +719,10 @@ static inline bool ffIsSystemBasicProcessInfoAvailable() { // Includes HandleCou
return SharedUserData->NtBuildNumber >= 26200; // MSDN says it was added in 26100.4770; ntdoc says 25H2
}
static inline bool ffIsFileFsFullSizeInformationExAvailable() {
return SharedUserData->NtBuildNumber >= 17763; // Windows 10 October 2018 Update (1809, REDSTONE5)
}
NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
@@ -1422,3 +1423,23 @@ NTSYSAPI NTSTATUS NTAPI NtTerminateProcess(
NTSYSAPI NTSTATUS NTAPI RtlAcquirePebLock(VOID);
NTSYSAPI NTSTATUS NTAPI RtlReleasePebLock(VOID);
typedef struct _FILE_FS_FULL_SIZE_INFORMATION_EX {
ULONGLONG ActualTotalAllocationUnits;
ULONGLONG ActualAvailableAllocationUnits;
ULONGLONG ActualPoolUnavailableAllocationUnits;
ULONGLONG CallerTotalAllocationUnits;
ULONGLONG CallerAvailableAllocationUnits;
ULONGLONG CallerPoolUnavailableAllocationUnits;
ULONGLONG UsedAllocationUnits;
ULONGLONG TotalReservedAllocationUnits;
ULONGLONG VolumeStorageReserveAllocationUnits;
ULONGLONG AvailableCommittedAllocationUnits;
ULONGLONG PoolAvailableAllocationUnits;
ULONG SectorsPerAllocationUnit;
ULONG BytesPerSector;
} FILE_FS_FULL_SIZE_INFORMATION_EX, *PFILE_FS_FULL_SIZE_INFORMATION_EX;
enum {
FileFsFullSizeInformationEx = 14
};
+111
View File
@@ -0,0 +1,111 @@
#pragma once
#include "fastfetch.h"
#if FF_HAVE_ZLIB
// Which set of entry points a caller needs. Resolving is per group rather than all at once so that a
// symbol nobody uses cannot make a caller fail: libz is not one library across the platforms
// fastfetch builds on, and the gz family in particular is not guaranteed to be present.
typedef enum FFZlibGroup : uint8_t {
FF_ZLIB_COMPRESS = 1 << 0, // compressBound, compress2
FF_ZLIB_INFLATE = 1 << 1, // inflateInit2_, inflate, inflateEnd
FF_ZLIB_GZFILE = 1 << 2, // gzopen, gzread, gzclose
} FFZlibGroup;
#if FF_DISABLE_DLOPEN
#include <zlib.h>
static inline const char* ffZlibLoad([[maybe_unused]] FFZlibGroup group) {
// the library is linked in: nothing to open, nothing to resolve, nothing to fail
return nullptr;
}
#else
// Hack. <zlib.h> declares everything with an extern linkage macro, which prevents us from
// implementing the functions ourselves. zlib spells it ZEXTERN/ZEXPORT and zlib-ng's
// zlib-compat header spells it Z_EXTERN/Z_EXPORT; both set theirs under `#ifndef`, so
// defining all four before the include is what makes the definitions at the bottom of this
// header legal -- they inherit the static inline linkage from the declarations zlib.h emits
// here. This header must therefore be the first thing in a translation unit to pull in
// <zlib.h>; if it is not, the definitions below stop being static and clang reports
// `static declaration follows non-static declaration` right there.
#undef ZEXTERN
#undef ZEXPORT
#undef Z_EXTERN
#undef Z_EXPORT
#define ZEXTERN static inline
#define ZEXPORT
#define Z_EXTERN static inline
#define Z_EXPORT
// Redeclaring everything as `static inline` above means <zlib.h> now defines entry points
// that a given translation unit may not call at all. Silenced for the include only: the
// pragma is popped right after, so the rest of the file -- and the rest of the translation
// unit that includes this header -- still reports unused functions.
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
#pragma GCC diagnostic ignored "-Wunused-function"
#include <zlib.h>
#include "common/library.h"
extern struct FFZlibData {
// Kept open rather than closed once opened: every group resolves against this one mapping, and
// re-opening per group would pay the cold load again.
void* handle;
FF_LIBRARY_SYMBOL(compressBound)
FF_LIBRARY_SYMBOL(compress2)
FF_LIBRARY_SYMBOL(inflateInit2_)
FF_LIBRARY_SYMBOL(inflate)
FF_LIBRARY_SYMBOL(inflateEnd)
FF_LIBRARY_SYMBOL(gzopen)
FF_LIBRARY_SYMBOL(gzread)
FF_LIBRARY_SYMBOL(gzclose)
FFZlibGroup attempted;
} zlibData;
[[gnu::always_inline]] static inline uLong(compressBound)(uLong sourceLen) {
return zlibData.ffcompressBound(sourceLen);
}
[[gnu::always_inline]] static inline int(compress2)(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen, int level) {
return zlibData.ffcompress2(dest, destLen, source, sourceLen, level);
}
[[gnu::always_inline]] static inline int(inflateInit2_)(z_streamp strm, int windowBits, const char* version, int stream_size) {
return zlibData.ffinflateInit2_(strm, windowBits, version, stream_size);
}
[[gnu::always_inline]] static inline int(inflate)(z_streamp strm, int flush) {
return zlibData.ffinflate(strm, flush);
}
[[gnu::always_inline]] static inline int(inflateEnd)(z_streamp strm) {
return zlibData.ffinflateEnd(strm);
}
[[gnu::always_inline]] static inline gzFile(gzopen)(const char* path, const char* mode) {
return zlibData.ffgzopen(path, mode);
}
[[gnu::always_inline]] static inline int(gzread)(gzFile file, voidp buf, unsigned len) {
return zlibData.ffgzread(file, buf, len);
}
[[gnu::always_inline]] static inline int(gzclose)(gzFile file) {
return zlibData.ffgzclose(file);
}
// Resolves the entry points of `group` and returns nullptr, or an error message. Call it before using
// that group's functions: until it has succeeded, the forwarders above are null pointers. The library
// itself is opened once, by whichever group is asked for first, and stays open.
const char* ffZlibLoad(FFZlibGroup group);
#endif
#endif
+456 -215
View File
@@ -2,35 +2,119 @@
#include "battery.h"
#include "common/android/api.h"
#include "common/android/binder.h"
#include "common/android/dex.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/processing.h"
#include "common/properties.h"
#include "common/settings.h"
#include "common/strutil.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <string.h>
// Android exposes the battery through BatteryManager, which the NDK does not wrap and whose sysfs
// backing is unreadable from an app UID. What does work without any permission is
// android.os.IBatteryPropertiesRegistrar in system_server, reachable either over /dev/binder (see
// common/android/binder.h) or through a `dumpsys battery` subprocess. The binder route is the cheap
// one: ~0.3 ms for both properties, and no child process.
// backing is unreadable from an app UID -- /sys/class/power_supply is closed to untrusted_app
// outright, directory and all. What does work without any permission is
// android.os.IBatteryPropertiesRegistrar in system_server, over /dev/binder (see
// common/android/binder.h).
//
// The registrar only answers properties that do not need BATTERY_STATS, and that rules out
// everything the module would otherwise like to show. Manufacturer, model name, technology, serial
// number, manufacture date, temperature and cycle count all sit behind that permission.
// `dumpsys battery` is the other route, tried first for shell and root because it is the richer of
// the two: a flat `key: value` list read by name, so it carries what neither the registrar nor a
// property has -- the technology and the critical capacity level -- and neither a vendor that prints
// extra keys in the middle nor a release that appends a field can break it. The binder reply is
// positional and breaks silently instead. What it costs is a fork/exec, which is why it is only
// reached where it can succeed; see the UID gate in ffDetectBattery.
//
// An app UID gets none of that, and not because Android withholds it. `ACTION_BATTERY_CHANGED` is a
// sticky broadcast, and a sticky broadcast is read without a permission: it carries the technology,
// and a cycle count since Android 14 -- the device's own BatteryManager declares `EXTRA_CYCLE_COUNT`,
// spelled "android.os.extra.CYCLE_COUNT". What is out of reach is the call that fetches it. A sticky
// broadcast is read by `registerReceiver`, the NDK does not wrap it, and a native binary has no Java
// side to make that call from, so every route to it is a Java process. termux-api is one, and it is
// not the answer: on the device this was written on it hung on six of six runs, each killed at 30
// seconds having written nothing, after exec'ing `am broadcast` and waiting on a socket that has no
// timeout of its own.
//
// What is left within reach is a pair of system properties BatteryService mirrors out of every update
// it processes: `debug.tracing.plug_type` and `debug.tracing.battery_status`. A system property is
// world readable, which is the whole point of them here -- the charger bits used to need `dumpsys
// battery`, and so the shell UID, while the status used to cost a second transaction. Both are now
// free, to any UID; see the two defines below.
//
// The temperature comes from neither service. The kernel publishes it as a thermal zone, and
// /sys/class/thermal is readable from an app UID -- unlike /sys/class/power_supply, which is closed
// outright, directory and all -- so it is read there whenever `temp` asks for it. The dump has a
// `temperature` line of its own and that one wins where the dump runs; see `detectTemperature` below
// for why the zone is looked up by name rather than addressed by number.
//
// The registrar answers nothing else. `BatteryProperty` has no temperature, no technology and no
// cycle count, and manufacturer and model name are not battery data at all. Of the properties that
// do exist, ids 7 to 12 -- the ones Android 15 added -- are behind BATTERY_STATS, and ids 1 to 6 are
// not. The one reading the module wants that is not a property either, how long the charge will
// last, comes from a different service; see the `batterystats` block below.
// The transaction code and the reply layout are both positional, and neither is negotiated with the
// service, so each only stays correct as long as AOSP keeps the order it already has. The code has
// not: `IBatteryPropertiesRegistrar` still declared registerListener / unregisterListener /
// getProperty up to Android 9, and Android 10 dropped the two listener methods, which moved
// `getProperty` from the third position to the first. The layout has, because `BatteryProperty` has
// only ever grown at the end -- API 35 appended [string8 mValueString] to it -- so the field read
// below is still the first one written.
// The reply layout is positional and is not negotiated with the service, so it only stays correct as
// long as AOSP keeps the order it already has. It has, because `BatteryProperty` has only ever grown
// at the end -- API 35 appended [string8 mValueString] to it -- so the field read below is still the
// first one written.
//
// The transaction code is positional for the same reason, and it has *not* stayed: the interface
// still declared registerListener / unregisterListener / getProperty up to Android 9, and Android 10
// dropped the two listener methods, which moved `getProperty` from the third position to the first.
// The number is therefore read out of the device's own jar rather than guessed from the API level,
// because a vendor fork is free to insert a method ahead of it -- and a wrong code reaches a
// different method, or none. See common/android/dex.h.
//
// Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the
// end of the reply, so a drift shows up as a wrong capacity, not as a failed call.
#define FF_BATTERY_ANDROID_SERVICE "batteryproperties"
#define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar"
// IBatteryPropertiesRegistrar has always been part of framework.jar -- it is not one of the
// frameworks that moved into an APEX -- so there is one path and no fallback. The method this module
// calls is named by the constant the dex carries for it, which is what the lookup turns into whatever
// number this build uses.
#define FF_BATTERY_ANDROID_JAR "/system/framework/framework.jar"
#define FF_BATTERY_ANDROID_STUB "Landroid/os/IBatteryPropertiesRegistrar$Stub;"
#define FF_BATTERY_ANDROID_GET_PROPERTY "TRANSACTION_getProperty"
// How long the charge will last is not a property of the pack: it is a forecast BatteryStatsService
// makes from the discharge history, so it lives on a second service. That service is `batterystats`,
// whose interface is com.android.internal.app.IBatteryStats, and it is also in framework.jar --
// com.android.internal.app is not one of the namespaces that moved into an APEX -- so the same read
// resolves this code as the one above.
//
// The interface has 120 methods and the dex cannot say which of them an app UID may call: neither
// IBatteryStats nor its $Stub carries an `_enforcePermission` wrapper, so the wall is inside
// BatteryStatsService, which lives in services.jar and is not available to read. It was therefore
// measured, as uid 10478 on an Android 16 device. The three this module needs answer with no
// permission at all -- and so do `getAllWakeLocks` (117) and `getAllWakeLocksEx` (118), which is why
// the claim is not "only three of 120". Every read-only statistic getter tried (`getAwakeTimeBattery`,
// `getAwakeTimePlugged`, and the cellular / wifi / gps / bluetooth ones) came back with
// "Access denied, requires: android.permission.BATTERY_STATS", or an `anyOf` of that and
// UPDATE_DEVICE_STATS.
//
// The code is read out of the jar for the same reason as `getProperty`, and the numbers make the
// point better than the argument does: this method is transaction 19 on that Android 16 device, 22
// on a Mi 10 (Android 14) and 20 on a Mi 9 (Android 10). A hardcoded 19 would reach a different
// method on two of the three.
#define FF_BATTERY_ANDROID_STATS_SERVICE "batterystats"
#define FF_BATTERY_ANDROID_STATS_DESCRIPTOR "com.android.internal.app.IBatteryStats"
#define FF_BATTERY_ANDROID_STATS_STUB "Lcom/android/internal/app/IBatteryStats$Stub;"
#define FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING "TRANSACTION_computeBatteryTimeRemaining"
// The temperature is a third source again, and not a service this time: the kernel publishes it as a
// thermal zone, and the zone Android's own thermal HAL names `battery` is the one to read. The name
// is what is matched -- not the number, and not a prefix -- for the reasons spelled out in
// detectTemperature below.
#define FF_BATTERY_ANDROID_THERMAL_DIR "/sys/class/thermal/"
#define FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX "thermal_zone"
#define FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE "battery"
// BatteryManager.BATTERY_PROPERTY_*
typedef enum FFBatteryAndroidProperty : uint32_t {
FF_BATTERY_ANDROID_PROPERTY_CAPACITY = 4,
@@ -38,19 +122,49 @@ typedef enum FFBatteryAndroidProperty : uint32_t {
} FFBatteryAndroidProperty;
// BatteryManager.BATTERY_STATUS_*, which the health HAL's `BatteryStatus` mirrors value for value.
// Only the three the module has something to say about are carried: NOT_CHARGING (4) and FULL (5)
// both mean a charger is attached, which the powered bits already say.
// The module has something to say about only three of the five: NOT_CHARGING (4) and FULL (5) both
// mean a charger is attached, and it answers nothing about the charger to put that on, while the dump
// route maps UNKNOWN (1) as well, because there it is a different statement from the service not
// answering at all. The two are named anyway, because the property route validates the whole range
// before it trusts a value it did not get from the service.
typedef enum FFBatteryAndroidStatus : uint32_t {
FF_BATTERY_ANDROID_STATUS_UNKNOWN = 1,
FF_BATTERY_ANDROID_STATUS_CHARGING = 2,
FF_BATTERY_ANDROID_STATUS_DISCHARGING = 3,
FF_BATTERY_ANDROID_STATUS_NOT_CHARGING = 4,
FF_BATTERY_ANDROID_STATUS_FULL = 5,
} FFBatteryAndroidStatus;
// The pair of system properties that stand in for two of the readings above. Both are published by
// `BatteryService.processValuesLocked` -- the two strings sit next to each other in the constant pool
// of classes.dex in this device's services.jar, which is what a pair of `SystemProperties.set` calls
// looks like after the fact -- and neither is a `BatteryProperty`, so nothing about the registrar's
// positional reply applies to them.
//
// debug.tracing.plug_type -- BatteryManager.BATTERY_PLUGGED_*: 1 AC, 2 USB, 4 wireless,
// 8 dock, and 0 when nothing is attached. The dock bit has no
// `FF_BATTERY_STATUS_*` counterpart and is masked off below.
// debug.tracing.battery_status -- BatteryManager.BATTERY_STATUS_*, the value
// `getProperty(BATTERY_PROPERTY_STATUS)` answers with as well:
// 1 UNKNOWN, 2 CHARGING, 3 DISCHARGING, 4 NOT_CHARGING, 5 FULL.
#define FF_BATTERY_ANDROID_PROP_PLUG_TYPE "debug.tracing.plug_type"
#define FF_BATTERY_ANDROID_PROP_BATTERY_STATUS "debug.tracing.battery_status"
// The encoding of plug_type, read out of the device's own framework.jar rather than assumed: the
// values are bits of one field and not a sequence, so `value != 0` is not a test for "wired", and a
// device reporting the dock would be told it is on neither AC nor USB nor wireless.
typedef enum FFBatteryAndroidPlugged : uint32_t {
FF_BATTERY_ANDROID_PLUGGED_AC = 1,
FF_BATTERY_ANDROID_PLUGGED_USB = 2,
FF_BATTERY_ANDROID_PLUGGED_WIRELESS = 4,
} FFBatteryAndroidPlugged;
// BatteryCapacityLevel.BATTERY_CAPACITY_LEVEL_CRITICAL, the level the framework itself shuts the
// device down on. This is the health HAL's numbering, not the one the older
// `BatteryManager.BATTERY_CAPACITY_LEVEL_*` constants used, which had CRITICAL at 4. The rest of the
// enum -- -1 UNSUPPORTED, 0 UNKNOWN, 2 LOW, 3 NORMAL, 4 HIGH, 5 FULL -- only restates the
// percentage, so only this value is carried.
// enum is skipped: -1 (UNSUPPORTED) and 0 (UNKNOWN) are what a HAL that does not implement the
// property reports, and 2 to 5 only restate the percentage. Only the dump carries this -- it is not a
// `BatteryProperty`, so the binder route never sees it.
typedef enum FFBatteryAndroidCapacityLevel : int32_t {
FF_BATTERY_ANDROID_CAPACITY_LEVEL_CRITICAL = 1,
} FFBatteryAndroidCapacityLevel;
@@ -62,6 +176,11 @@ typedef enum FFBatteryAndroidCapacityLevel : int32_t {
#define FF_BATTERY_ANDROID_VALUE_OFFSET 12
#define FF_BATTERY_ANDROID_VALUE_UNSET 0x8000000000000000ULL // Long.MIN_VALUE, i.e. never filled in
// A method that answers with a bare `long` writes [int32 exception][int64 value] and nothing else:
// there is no out parameter, so no non-null marker sits in front of the value the way it does in a
// `getProperty` reply. Twelve bytes in all on the device.
#define FF_BATTERY_ANDROID_LONG_VALUE_OFFSET 4
// One `getProperty` reply is [exception code][return value][value present][int64 mValueLong], and it
// measures 24 bytes on the device: API 35 appended [string8 mValueString] to `BatteryProperty`, and a
// reply carries a word for it even when it is empty. A dump of the first 32 covers every word.
@@ -103,15 +222,8 @@ static void debugDumpReply(const uint8_t* data, size_t size) {
#define FF_BATTERY_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
#endif
static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t property, uint64_t* value) {
// `getProperty` is the third method declared in `IBatteryPropertiesRegistrar` up to Android 9 and
// the first one from Android 10 on. Written as an `if` because clang rejects `__builtin_available`
// -- which is what FF_ANDROID_API_AT_LEAST expands to -- in any other position.
uint32_t transaction = 3u;
if (FF_ANDROID_API_AT_LEAST(29)) {
transaction = 1u;
}
FF_DEBUG("Property %u goes out as transaction %u (3 up to Android 9, 1 from Android 10)", property, transaction);
static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t transaction, uint32_t property, uint64_t* value) {
FF_DEBUG("Property %u goes out as transaction %u", property, transaction);
uint8_t parcelBuffer[128];
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
@@ -164,9 +276,277 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope
return nullptr;
}
static const char* parseBinder(FFlist* results) {
// Reads either of the two properties above as a number. Both are written by `Integer.toString`, so a
// buffer whose text is not a number is the property saying nothing rather than a 0 -- 0 is a plug type,
// and it is not a status.
static bool readPropertyUInt(const char* name, uint32_t* value) {
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
if (!ffSettingsGetAndroidProperty(name, &buffer)) {
FF_DEBUG("The \"%s\" property is not set, so nothing is read from it", name);
return false;
}
// `ffStrbufToUInt` answers the default for text that does not start with a digit, which is why the
// default is a value neither property can hold rather than a 0.
const uint64_t parsed = ffStrbufToUInt(&buffer, UINT64_MAX);
if (parsed == UINT64_MAX) {
FF_DEBUG("The \"%s\" property reads \"%s\", which is not a number", name, buffer.chars);
return false;
}
*value = (uint32_t) parsed;
return true;
}
// The charger type. This is what the property route has and the registrar does not: `getProperty`
// answers nothing about the charger, so the three bits used to come from `dumpsys battery`, i.e. from
// the shell UID and root alone. A device that does not publish the property leaves the bits clear,
// which is also what an unplugged charger looks like -- the module cannot tell those apart, and neither
// can the framework, whose sticky broadcast carries this same value as `EXTRA_PLUGGED`.
static void fillPlugged(FFBatteryResult* battery) {
uint32_t plugged = 0;
if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_PLUG_TYPE, &plugged)) {
return;
}
// A mask rather than a comparison: the three are bits of one field, and it is the bits that have a
// `FF_BATTERY_STATUS_*` counterpart. BATTERY_PLUGGED_DOCK (8) is the one that does not, so a device
// on a dock reports no powered bit at all -- and `plugged != 0` would have called that "wired".
if (plugged & FF_BATTERY_ANDROID_PLUGGED_AC) {
battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
}
if (plugged & FF_BATTERY_ANDROID_PLUGGED_USB) {
battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
}
if (plugged & FF_BATTERY_ANDROID_PLUGGED_WIRELESS) {
battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
}
FF_DEBUG("The \"%s\" property is %u, which becomes status bits 0x%x (1 AC, 2 USB, 4 wireless; 0 is "
"what both an unplugged charger and a device that does not publish the property look like)",
FF_BATTERY_ANDROID_PROP_PLUG_TYPE, plugged, (unsigned) battery->status);
}
// The status, read the cheap way first. The property carries the same `mHealthInfo.batteryStatus` the
// transaction would fetch, so the two can only differ inside the window between the health HAL reporting
// a value and the service storing it, while what the property costs is a read of a shared page instead
// of a round trip through /dev/binder -- a round trip measured at about 0.04 ms on the device this was
// written on, and what it saves is a positional reply that has to be decoded as well.
//
// It stays a preference rather than a replacement: a property that is not set (an early boot, or a
// build whose BatteryService does not publish it) or that does not carry one of the five values sends
// the caller to the service, which is the one answer that cannot be stale.
static bool detectStatusFromProperty(uint64_t* status) {
uint32_t value = 0;
if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, &value)) {
return false;
}
if (value < FF_BATTERY_ANDROID_STATUS_UNKNOWN || value > FF_BATTERY_ANDROID_STATUS_FULL) {
FF_DEBUG("The \"%s\" property is %u, which is not a battery status, so the service is asked",
FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, value);
return false;
}
*status = value;
return true;
}
// Calls a method that takes no argument and answers with one `long`, which is how
// IBatteryStats.computeBatteryTimeRemaining() is declared. `transaction` comes out of the jar, and the
// sentinel the reader leaves for a code it could not find is caught here, so that a build which does
// not declare the method is a message rather than a transaction that means something else.
//
// A negative answer is not an error: it is how the interface spells "no estimate", so it is passed
// back as it came and the caller decides what that is worth. The unit is milliseconds.
static const char* getTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, int64_t* milliseconds) {
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
FF_DEBUG("computeBatteryTimeRemaining is unresolved in the jar (transaction %d)", (int) transaction);
return "The battery stats service does not declare computeBatteryTimeRemaining";
}
uint8_t parcelBuffer[128];
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_STATS_DESCRIPTOR);
uint8_t replyBuffer[128];
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
if (error != nullptr) {
FF_DEBUG("computeBatteryTimeRemaining could not be transacted: %s", error);
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
FF_DEBUG("computeBatteryTimeRemaining came back as a status reply of %d instead of a parcel",
(int) ffBinderReadI32(reply.data, reply.size, 0));
return "The battery stats service rejected the request";
}
// The exception word is read first and the value only once it is known to be a value: a denied
// call answers [exception -1][string16 message], so the eight bytes at the value's offset are
// then the length and the first characters of the message rather than a number.
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
if (exception != 0) {
FF_DEBUG("computeBatteryTimeRemaining is transaction %d, reply %zu bytes, exception %d",
transaction, reply.size, exception);
return "The battery stats service raised an exception";
}
*milliseconds = (int64_t) ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_LONG_VALUE_OFFSET);
FF_DEBUG("computeBatteryTimeRemaining is transaction %d: reply is %zu bytes, value %" PRId64 " ms",
transaction, reply.size, *milliseconds);
return nullptr;
}
// Writes the time estimate onto the battery just reported. It is not one of the registrar's
// properties -- it is a forecast BatteryStatsService makes -- so it comes from a second service and
// is filled in here, after the capacity and the status are already in place.
//
// `binder` and `handle` are the caller's, already open on `batterystats`. Nothing here can fail the
// module: an estimate that is not available is what `FFBatteryResult` already spells with -1.
static void fillTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, FFlist* results) {
if (results->length == 0) {
FF_DEBUG("No time remaining is reported: no battery was reported to put it on");
return;
}
int64_t milliseconds = 0;
const char* error = getTimeRemaining(binder, handle, transaction, &milliseconds);
if (error != nullptr) {
FF_DEBUG("The time remaining could not be computed, so none is reported: %s", error);
return;
}
if (milliseconds <= 0) {
FF_DEBUG("The battery stats service answered %" PRId64 " ms, which is how it says it has no estimate",
milliseconds);
return;
}
// The interface answers in milliseconds and `FFBatteryResult` carries seconds, which is what the
// other platform modules fill in. An answer under a second truncates to 0, which the module reads
// as "unknown" as well, so there is nothing to guard against.
FF_LIST_GET(FFBatteryResult, *results, 0)->timeRemaining = (int32_t) (milliseconds / 1000);
}
// The battery temperature is neither a registrar property nor part of the health HAL: the kernel
// publishes it as a thermal zone, and the zone Android's thermal HAL names `battery` is the one to
// read. It is read from /sys/class/thermal, which an app UID may open -- the reason the module does
// not simply read /sys/class/power_supply/battery/temp is that that directory is closed to it.
//
// The zone is *found*, not addressed. Its number is a property of the probe order rather than of the
// hardware -- the battery is `thermal_zone67` on the device this was written on, and nothing promises
// the next one -- so the directory is walked and each entry is matched on the `type` it reports.
//
// That match is an exact string, and has to stay one. A prefix would be cheaper and wrong: the same
// directory holds hardware trip points (`cpu-hw-trip-0` reports 95000, a perfectly believable 95
// degrees), current and battery levels (`pmih010x-ibat-lvl0`, 0 or a small integer) and raw registers
// (`vbat`, in millivolts). None of those are a temperature, and every one of them would be printed as
// one. Reading a different zone is not a smaller error than reading none.
//
// A device that names its battery zone something else therefore reports no temperature rather than a
// wrong one, which is what FF_BATTERY_TEMP_UNSET already means.
static double detectTemperature(void) {
FF_AUTO_CLOSE_DIR DIR* dirp = opendir(FF_BATTERY_ANDROID_THERMAL_DIR);
if (dirp == nullptr) {
FF_DEBUG("Opening \"%s\" failed, so no temperature is reported: %s", FF_BATTERY_ANDROID_THERMAL_DIR, strerror(errno));
return FF_BATTERY_TEMP_UNSET;
}
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
struct dirent* entry;
while ((entry = readdir(dirp)) != nullptr) {
if (!ffStrStartsWith(entry->d_name, FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX)) {
continue;
}
FF_AUTO_CLOSE_FD int zone = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (zone < 0) {
continue;
}
// The type file ends with a newline, which the comparison below would otherwise never survive.
if (!ffReadFileBufferRelative(zone, "type", &buffer)) {
continue;
}
ffStrbufTrimRightSpace(&buffer);
if (!ffStrbufEqualS(&buffer, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE)) {
continue;
}
if (!ffReadFileBufferRelative(zone, "temp", &buffer)) {
FF_DEBUG("The battery zone \"%s\" has no readable temperature", entry->d_name);
return FF_BATTERY_TEMP_UNSET;
}
// sysfs thermal publishes millidegrees Celsius. That is the one thing every zone here agrees
// on, even the ones whose reading is not a temperature at all.
const double value = ffStrbufToDouble(&buffer, FF_BATTERY_TEMP_UNSET);
if (value == FF_BATTERY_TEMP_UNSET) {
FF_DEBUG("The battery zone \"%s\" reports \"%s\", which is not a number", entry->d_name, buffer.chars);
return FF_BATTERY_TEMP_UNSET;
}
FF_DEBUG("The battery zone is \"%s\", reporting %.2f degrees", entry->d_name, value / 1000.);
return value / 1000.;
}
FF_DEBUG("No zone in \"%s\" is named \"%s\", so no temperature is reported",
FF_BATTERY_ANDROID_THERMAL_DIR, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE);
return FF_BATTERY_TEMP_UNSET;
}
// Temperature is opt-in on every platform, and on Android that is not only about the display: the
// walk above touches a directory with a hundred-odd entries on the device this was written on, and
// none of that is paid for unless `temp` asks for it.
static void fillTemperature(FFBatteryOptions* options, FFlist* results) {
if (!options->temp) {
FF_DEBUG("Temperature is not enabled, so the thermal zones are not walked");
return;
}
if (results->length == 0) {
return;
}
const double temperature = detectTemperature();
if (temperature != FF_BATTERY_TEMP_UNSET) {
FF_LIST_GET(FFBatteryResult, *results, 0)->temperature = temperature;
}
}
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
// Resolved before the binder is opened, so that a jar which does not declare the method says so
// rather than the module failing later with a message about the transport. Both codes are asked
// for in one call, which is cheaper than two: the reader walks the jar's dex entries in order and
// stops once every request has been answered, so a second request only costs the walk to the
// entry that defines the second class. That walk is not free here, because the two classes are
// not in the same entry -- `IBatteryPropertiesRegistrar$Stub` is in classes3.dex and
// `IBatteryStats$Stub` in classes5.dex -- and on the device this was measured on it added 1.3 ms
// to a module that had been 3.3 ms, while the service lookup and the transaction behind it stayed
// inside the noise. The reader reports a code it cannot find by leaving the sentinel in the
// result and says nothing more -- what a missing code means for this module is decided here, not
// there.
//
// The two are not equally important. `getProperty` is what the module is made of, so a build
// without it has nothing to report; `computeBatteryTimeRemaining` only adds the time estimate,
// and a build without it still has the capacity and the status, so its sentinel is carried on to
// where the estimate is asked for instead of failing the route here.
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
int32_t timeRemainingTransaction = FF_DEX_STATIC_INT_UNRESOLVED;
const FFDexStaticIntRequest requests[] = {
{ FF_BATTERY_ANDROID_STUB, FF_BATTERY_ANDROID_GET_PROPERTY, &transaction },
{ FF_BATTERY_ANDROID_STATS_STUB, FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING, &timeRemainingTransaction },
};
const char* error = ffDexStaticInts(FF_BATTERY_ANDROID_JAR, requests, ARRAY_SIZE(requests));
if (error != nullptr) {
FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_BATTERY_ANDROID_JAR, error);
return error;
}
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
FF_DEBUG("getProperty is unresolved in the jar (transaction %d)", (int) transaction);
return "The battery service does not declare getProperty";
}
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
const char* error = ffBinderOpen(&binder);
error = ffBinderOpen(&binder);
if (error != nullptr) {
FF_DEBUG("Opening /dev/binder failed: %s", error);
return error;
@@ -179,20 +559,30 @@ static const char* parseBinder(FFlist* results) {
FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error);
return error;
}
FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, service.handle);
FF_DEBUG("The \"%s\" service is handle %u, getProperty is transaction %d", FF_BATTERY_ANDROID_SERVICE, service.handle, transaction);
uint64_t capacity = 0;
error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
if (error != nullptr) {
FF_DEBUG("The capacity is what failed, so no battery is reported at all");
return error;
}
// The status is asked of the property first, because that is where the second transaction on the
// service is saved. The service is still asked when the property has nothing to say, and a failure
// there is still fatal: the capacity and the status are what this route is made of, and a battery
// whose status is unknown is not a battery this route can describe.
uint64_t status = 0;
error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
if (error != nullptr) {
FF_DEBUG("The status is what failed, so no battery is reported at all");
return error;
if (detectStatusFromProperty(&status)) {
FF_DEBUG("The status came from the \"%s\" property and is %" PRIu64 ", so the service is not asked "
"for it",
FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, status);
} else {
error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
if (error != nullptr) {
FF_DEBUG("The status is what failed, so no battery is reported at all");
return error;
}
}
FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
@@ -203,187 +593,38 @@ static const char* parseBinder(FFlist* results) {
} else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) {
battery->status |= FF_BATTERY_STATUS_DISCHARGING;
}
// The charger type is the one reading this route has that the registrar does not, and it is a
// property as well: the three bits used to come from `dumpsys battery`, and so from the shell UID
// and root alone.
fillPlugged(battery);
// The temperature comes from outside this route entirely, so it is asked for here rather than
// inside it: nothing about the binder session above has any bearing on it, and a device without a
// zone by that name simply leaves the field at the sentinel.
fillTemperature(options, results);
// The time estimate is the one part of this route that is allowed to be missing: a jar without
// the code, a service that is not running and a service that answers -1 all leave the field at
// the -1 the module starts it with, which is how `FFBatteryResult` spells "unknown". It is asked
// for last for that reason -- a failure here cannot cost the capacity and the status that have
// already been read.
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle stats = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_STATS_SERVICE, FF_BINDER_SM_GET_SERVICE, &stats.handle);
if (error != nullptr) {
FF_DEBUG("Looking up the \"%s\" service failed, so no time remaining is reported: %s",
FF_BATTERY_ANDROID_STATS_SERVICE, error);
} else {
fillTimeRemaining(&binder, stats.handle, timeRemainingTransaction, results);
}
// `BatteryStatus` has five values and only two of them say something the powered bits do not, so
// the trace spells the mapping out: anything that is not 2 or 3 leaves the bits clear, which is
// what UNKNOWN (1), NOT_CHARGING (4) and FULL (5) all come to.
// the trace spells the mapping out: anything that is not 2 or 3 leaves the charging / discharging
// bits clear, which is what UNKNOWN (1), NOT_CHARGING (4) and FULL (5) all come to -- that a
// charger is attached is stated by the bits the plug_type property fills in, not by this one. The
// temperature is not repeated here: it was traced where it was read, and it is the field of the
// three that can be missing without anything being wrong.
FF_DEBUG("Reporting one battery: capacity %.0f%%, status %" PRIu64 " -> bits 0x%x "
"(2 charging, 3 discharging; 1/4/5 leave them clear). The registrar answers no "
"temperature and no technology, so both stay unset",
battery->capacity, status, (unsigned) battery->status);
"(2 charging, 3 discharging; 1/4/5 leave them clear), %d s remaining (-1 when there is no "
"estimate). The technology stays unset: it rides the sticky broadcast, which this route "
"does not read",
battery->capacity, status, (unsigned) battery->status, battery->timeRemaining);
return nullptr;
}
// `dumpsys battery` prints what `BatteryService.dumpInternal()` prints: the whole of the health HAL's
// `HealthInfo`, plus the timestamps the service keeps for its own broadcast rate limiter. It is a flat
// `key: value` list read by name, which is what makes it the sturdier of the two routes -- a vendor
// that prints extra keys (some vendors do: `engine`, `soc decimal`, `adapter power`, `board temp status`,
// `low bat status`, `reverse wl chg status`, `reverse wl chg exception`, `chg shut vbat`,
// `last mode flag` and `last mode keep time`, in the middle of the list) only adds keys nobody asks
// for, and a release that appends a field costs nothing. The binder reply is positional and breaks
// silently instead.
//
// What each key carries, per `android.hardware.health.HealthInfo`:
//
// AC / USB / Wireless / Dock powered -- `charger*Online`, the `plugged` bits of the
// ACTION_BATTERY_CHANGED broadcast. Only the first three have a `FF_BATTERY_STATUS_*`
// counterpart; `Dock powered` (`BATTERY_PLUGGED_DOCK`) has none and is not read.
// Max charging current / Max charging voltage -- `maxChargingCurrentMicroamps` (µA) and
// `maxChargingVoltageMicrovolts` (µV): what the charger offers, not what the battery is drawing.
// Charge counter -- `batteryChargeCounterUah` (µAh), what is left in the pack. The unit is not
// honoured everywhere: MIUI reports the same number as its own dump and three orders of
// magnitude below what the registrar answers for the same battery.
// status -- `batteryStatus`, i.e. `BatteryStatus`: 1 UNKNOWN, 2 CHARGING, 3 DISCHARGING,
// 4 NOT_CHARGING (a charger is attached and the battery is deliberately not taking current),
// 5 FULL.
// health -- `batteryHealth`, i.e. `BatteryHealth`: 1 UNKNOWN, 2 GOOD, 3 OVERHEAT, 4 DEAD,
// 5 OVER_VOLTAGE, 6 UNSPECIFIED_FAILURE, 7 COLD, and, in newer releases, 8 FAIR, 11 NOT_AVAILABLE
// and 12 INCONSISTENT.
// present -- `batteryPresent`, whether the pack is in the device at all.
// level / scale -- `batteryLevel`, the remaining capacity in percent, and `BATTERY_SCALE`, the value
// 100% is expressed in. The scale has been 100 on every release, but it is printed rather than
// assumed, so it is read.
// voltage -- `batteryVoltageMillivolts`. The unit in the name is the recent one: the HAL 1.0 field
// was `batteryVoltage` in microvolts, and the AIDL notes that implementations had always filled it
// in millivolts, so the rename only wrote down the de-facto unit.
// temperature -- `batteryTemperatureTenthsCelsius`, tenths of a degree Celsius.
// technology -- `batteryTechnology`, e.g. "Li-poly". The registrar answers no such property, so
// together with the temperature this is what the route is here for.
// Charging state -- `chargingState`, i.e. `BatteryChargingState`: 0 INVALID, 1 NORMAL, 2 TOO_COLD,
// 3 TOO_HOT, 4 LONG_LIFE, 5 ADAPTIVE.
// Charging policy -- `chargingPolicy`, i.e. `BatteryChargingPolicy`: 0 INVALID, 1 DEFAULT,
// 2 LONG_LIFE, 3 ADAPTIVE. 0 in either means the HAL did not report it.
// Capacity level -- `batteryCapacityLevel`, i.e. `BatteryCapacityLevel`: -1 UNSUPPORTED,
// 0 UNKNOWN, 1 CRITICAL, 2 LOW, 3 NORMAL, 4 HIGH, 5 FULL.
//
// Three keys are not battery readings at all and are skipped: the two `Time when the latest updated
// value of the ... was sent via battery changed broadcast` lines and `The last voltage value sent via
// the battery changed broadcast` are what `BatteryService` keeps to rate limit the broadcast.
//
// `FFBatteryResult` has a place for the status enum, the level pair, the temperature and the
// technology, and for the three powered booleans and the capacity level in the form of status bits.
// `voltage`, `health`, `Charge counter` and the two maximums are read by nothing -- there is no field
// to put them in -- and are described above so that the next reader does not have to go back to the
// service to find out what they are.
static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) {
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
if (ffProcessAppendStdOut(&buf, (char*[]) {
"/system/bin/dumpsys",
"battery",
nullptr,
}) != nullptr ||
buf.length == 0) {
return "Executing `/system/bin/dumpsys battery` failed";
}
if (!ffStrbufStartsWithS(&buf, "Current Battery Service state:\n")) {
// A refusal, which ffDetectBattery has already ruled out by looking at the UID: an app UID gets
// `Can't find service: battery` on Android 16, and the permission denial some other
// release would print lands here too. Both exit 0, so this is the only place that notices.
return "Invalid `/system/bin/dumpsys battery` result";
}
const char* start = buf.chars + strlen("Current Battery Service state:\n");
FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
if (!ffParsePropLines(start, "present: ", &temp) || !ffStrbufEqualS(&temp, "true")) {
return nullptr;
}
ffStrbufClear(&temp);
FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
initResult(battery);
if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
}
ffStrbufClear(&temp);
if (ffParsePropLines(start, "USB powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
}
ffStrbufClear(&temp);
if (ffParsePropLines(start, "Wireless powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
}
ffStrbufClear(&temp);
// NOT_CHARGING and FULL both mean a charger is attached, which the powered bits already say, and
// neither is charging or discharging in the sense `FF_BATTERY_STATUS` means. UNKNOWN is mapped
// because it is a different statement from the service not answering at all.
if (ffParsePropLines(start, "status: ", &temp)) {
switch (ffStrbufToUInt(&temp, 0)) {
case FF_BATTERY_ANDROID_STATUS_CHARGING:
battery->status |= FF_BATTERY_STATUS_CHARGING;
break;
case FF_BATTERY_ANDROID_STATUS_DISCHARGING:
battery->status |= FF_BATTERY_STATUS_DISCHARGING;
break;
case FF_BATTERY_ANDROID_STATUS_UNKNOWN:
battery->status |= FF_BATTERY_STATUS_UNKNOWN;
break;
default:
break;
}
}
ffStrbufClear(&temp);
// CRITICAL is the one capacity level that says something the percentage does not. -1 (UNSUPPORTED)
// and 0 (UNKNOWN) are what a HAL that does not implement the property reports, and 2 to 5 only
// restate the level.
if (ffParsePropLines(start, "Capacity level: ", &temp) &&
ffStrbufToSInt(&temp, -1) == FF_BATTERY_ANDROID_CAPACITY_LEVEL_CRITICAL) {
battery->status |= FF_BATTERY_STATUS_CRITICAL;
}
ffStrbufClear(&temp);
{
double level = 0, scale = 0;
if (ffParsePropLines(start, "level: ", &temp)) {
level = ffStrbufToDouble(&temp, -DBL_MAX);
}
ffStrbufClear(&temp);
if (ffParsePropLines(start, "scale: ", &temp)) {
scale = ffStrbufToDouble(&temp, -DBL_MAX);
}
ffStrbufClear(&temp);
if (level > 0 && scale > 0) {
battery->capacity = level * 100 / scale;
}
}
if (options->temp) {
if (ffParsePropLines(start, "temperature: ", &temp)) {
battery->temperature = ffStrbufToDouble(&temp, FF_BATTERY_TEMP_UNSET);
if (battery->temperature != FF_BATTERY_TEMP_UNSET) {
battery->temperature /= 10.0; // Android returns temperature in tenths of a degree
}
}
ffStrbufClear(&temp);
}
ffParsePropLines(start, "technology: ", &battery->technology);
return nullptr;
}
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
// `dumpsys battery` needs android.permission.DUMP, which only the shell UID and root hold: an app
// UID is answered with `Can't find service: battery` on stdout and a zero exit status, so forking
// it there costs a child process and cannot succeed. It is also the richer of the two routes --
// the registrar answers neither temperature nor technology, and the dump is what spells out the
// status enum -- so it is tried first wherever it is allowed to run, and the binder route covers
// everything else at ~0.3 ms. termux-api was dropped: it returns nothing on this device and can
// hang for minutes, the same reason its camera path was removed.
if (ffAndroidIsRootOrShell(instance.state.platform.uid)) {
const char* error = parseDumpsys(options, results);
if (error == nullptr) {
return nullptr;
}
FF_DEBUG("`/system/bin/dumpsys battery` failed: %s. Using the binder route instead", error);
}
return parseBinder(results);
}
+2
View File
@@ -1,5 +1,6 @@
#include "fastfetch.h"
#include "battery.h"
#include "common/debug.h"
#include "common/apple/cf_helpers.h"
#include "common/apple/smc_temps.h"
@@ -9,6 +10,7 @@
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("AppleSmartBattery"), &iterator) != kIOReturnSuccess) {
FF_DEBUG("IOServiceGetMatchingServices() failed");
return "IOServiceGetMatchingServices() failed";
}
+5
View File
@@ -1,4 +1,5 @@
#include "fastfetch.h"
#include "common/debug.h"
#include "common/sysctl.h"
#include "common/io.h"
#include "battery.h"
@@ -6,6 +7,8 @@
#include <dev/acpica/acpiio.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist* results) {
@@ -14,6 +17,7 @@ const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist*
int units = ffSysctlGetInt("hw.acpi.battery.units", -100);
if (units < 0) {
FF_DEBUG("sysctlbyname(\"hw.acpi.battery.units\") failed: %s", strerror(errno));
return "sysctlbyname(\"hw.acpi.battery.units\") failed";
}
@@ -23,6 +27,7 @@ const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist*
FF_AUTO_CLOSE_FD int acpifd = open("/dev/acpi", O_RDONLY | O_CLOEXEC);
if (acpifd < 0) {
FF_DEBUG("open(\"/dev/acpi\", O_RDONLY | O_CLOEXEC) failed: %s", strerror(errno));
return "open(\"/dev/acpi\", O_RDONLY | O_CLOEXEC) failed";
}
+6
View File
@@ -1,23 +1,28 @@
#include "fastfetch.h"
#include "battery.h"
#include "common/debug.h"
#include "common/io.h"
#include <private/device/power_managment.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
const char* parseBattery(int dfd, const char* battId, FFlist* results) {
FF_AUTO_CLOSE_FD int fd = openat(dfd, battId, O_RDWR);
if (fd < 0) {
FF_DEBUG("openat(%s) failed: %s", battId, strerror(errno));
return "openat() failed";
}
acpi_battery_info basic = {};
if (ioctl(fd, GET_BATTERY_INFO, &basic, sizeof(basic)) != 0) {
FF_DEBUG("ioctl(GET_BATTERY_INFO) failed");
return "ioctl(GET_BATTERY_INFO) failed";
}
acpi_extended_battery_info extended = {};
if (ioctl(fd, GET_EXTENDED_BATTERY_INFO, &extended, sizeof(extended)) != 0) {
FF_DEBUG("ioctl(GET_EXTENDED_BATTERY_INFO) failed");
return "ioctl(GET_EXTENDED_BATTERY_INFO) failed";
}
@@ -56,6 +61,7 @@ const char* parseBattery(int dfd, const char* battId, FFlist* results) {
const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist* results) {
FF_AUTO_CLOSE_DIR DIR* dir = opendir("/dev/power/acpi_battery/");
if (!dir) {
FF_DEBUG("opendir(\"/dev/power/acpi_battery/\") failed: %s", strerror(errno));
return "opendir(/dev/power/acpi_battery) failed";
}
+2
View File
@@ -6,6 +6,7 @@
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power
@@ -176,6 +177,7 @@ static bool parseBattery(int dfd, const char* id, FFBatteryOptions* options, FFl
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/power_supply/");
if (dirp == nullptr) {
FF_DEBUG("opendir(\"/sys/class/power_supply/\") failed: %s", strerror(errno));
return "opendir(\"/sys/class/power_supply/\") == nullptr";
}
+3
View File
@@ -1,4 +1,5 @@
#include "battery.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/FFstrbuf.h"
#include "common/strutil.h"
@@ -17,11 +18,13 @@
const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist* results) {
FF_AUTO_CLOSE_FD int fd = open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(_PATH_SYSMON) failed: %s", strerror(errno));
return "open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed";
}
prop_dictionary_t root = nullptr;
if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &root) < 0) {
FF_DEBUG("prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed");
return "prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed";
}
+4
View File
@@ -1,6 +1,8 @@
#include "battery.h"
#include "common/debug.h"
#include "common/io.h"
#include <string.h>
#include <machine/apmvar.h>
#include <sys/ioctl.h>
#include <unistd.h>
@@ -10,12 +12,14 @@ const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist*
FF_AUTO_CLOSE_FD int devfd = open("/dev/apm", O_RDONLY | O_CLOEXEC);
if (devfd < 0) {
FF_DEBUG("open(\"/dev/apm\") failed: %s", strerror(errno));
return "open(dev/apm, O_RDONLY | O_CLOEXEC) failed";
}
struct apm_power_info info = {};
if (ioctl(devfd, APM_IOC_GETPOWER, &info) < 0) {
FF_DEBUG("ioctl(APM_IOC_GETPOWER) failed: %s", strerror(errno));
return "ioctl(APM_IOC_GETPOWER) failed";
}
+3
View File
@@ -1,4 +1,5 @@
#include "battery.h"
#include "common/debug.h"
#include <kstat.h>
@@ -50,6 +51,7 @@ const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist*
// https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/powertop/common/battery.c
[[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open();
if (!kc) {
FF_DEBUG("kstat_open() failed");
return "kstat_open() failed";
}
@@ -75,6 +77,7 @@ const char* ffDetectBattery([[maybe_unused]] FFBatteryOptions* options, FFlist*
if (!getUint32(bif, "bif_last_cap", &lastCapacity) ||
!getUint32(bst, "bst_rem_cap", &remainingCapacity) ||
!getUint32(bst, "bst_state", &state)) {
FF_DEBUG("kstat_data_lookup() failed for bif_last_cap, bst_rem_cap or bst_state");
return "kstat_data_lookup() failed";
}
+4 -3
View File
@@ -1,10 +1,10 @@
#define INITGUID
#include "battery.h"
#include "common/endian.h"
#include "common/debug.h"
#include "common/endian.h"
#include "common/mallocHelper.h"
#include "common/windows/nt.h"
#include "common/windows/unicode.h"
#include "common/windows/wmi.h"
@@ -51,7 +51,7 @@ static FFBatteryWmiEntry* getBatteryEntry(FFlist* entries, FFlist* results, ULON
return entry;
}
static const char* queryWmiAllData(const GUID* guid, const char* guidStr, PWNODE_ALL_DATA* pAllData, ULONG* pBufferSize) {
static const char* queryWmiAllData(const GUID* guid, [[maybe_unused]] const char* guidStr, PWNODE_ALL_DATA* pAllData, ULONG* pBufferSize) {
[[gnu::cleanup(ffCloseWmiBlock)]] HANDLE hBlock = nullptr;
ULONG status = WmiOpenBlock(guid, WMIGUID_QUERY, &hBlock);
if (status != ERROR_SUCCESS) {
@@ -66,6 +66,7 @@ static const char* queryWmiAllData(const GUID* guid, const char* guidStr, PWNODE
}
if (*pBufferSize == 0) {
FF_DEBUG("WMI: WmiQueryAllDataW(nullptr) returned no data");
return "WmiQueryAllDataW(nullptr) returned no data";
}
+4
View File
@@ -1,5 +1,6 @@
#include "bios.h"
#include "common/apple/cf_helpers.h"
#include "common/debug.h"
#include <IOKit/IOKitLib.h>
@@ -10,11 +11,13 @@ const char* ffDetectBios(FFBiosResult* bios) {
// For Intel
FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t deviceRom = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/rom");
if (!deviceRom) {
FF_DEBUG("IODeviceTree:/rom not found");
return "IODeviceTree:/rom not found";
}
FF_CFTYPE_AUTO_RELEASE CFMutableDictionaryRef deviceRomProps = nullptr;
if (IORegistryEntryCreateCFProperties(deviceRom, &deviceRomProps, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) {
FF_DEBUG("IORegistryEntryCreateCFProperties(deviceRom) failed");
return "IORegistryEntryCreateCFProperties(deviceRom) failed";
}
@@ -28,6 +31,7 @@ const char* ffDetectBios(FFBiosResult* bios) {
// For arm64
FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t device = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/");
if (!device) {
FF_DEBUG("IODeviceTree:/ not found");
return "IODeviceTree:/ not found";
}
+3
View File
@@ -1,4 +1,5 @@
#include "bios.h"
#include "common/debug.h"
#include "common/smbios.h"
#ifdef _WIN32
@@ -46,11 +47,13 @@ static_assert(offsetof(FFSmbiosBios, ExtendedBiosRomSize) == 0x18,
const char* ffDetectBios(FFBiosResult* bios) {
const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable();
if (!smbiosTable) {
FF_DEBUG("BIOS: failed to get SMBIOS data");
return "Failed to get SMBIOS data";
}
const FFSmbiosBios* data = (const FFSmbiosBios*) (*smbiosTable)[FF_SMBIOS_TYPE_BIOS];
if (!data) {
FF_DEBUG("BIOS: section is not found in SMBIOS data");
return "BIOS section is not found in SMBIOS data";
}
+2
View File
@@ -1,4 +1,5 @@
#include "bluetooth.h"
#include "common/debug.h"
#define L2CAP_SOCKET_CHECKED
#include <bluetooth.h>
@@ -31,6 +32,7 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF
// struct hostent* ent = bt_gethostent();
if (bt_devenum((void*) enumDev, devices) < 0) {
FF_DEBUG("bt_devenum() failed");
return "bt_devenum() failed";
}
@@ -1,4 +1,5 @@
#include "bluetooth.h"
#include "common/debug.h"
#include "common/percent.h"
#include "common/strutil.h"
@@ -304,11 +305,13 @@ static const char* detectBluetooth(FFBluetoothOptions* options, FFlist* devices,
DBusMessage* managedObjects = ffDBusGetMethodReply(&dbus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", nullptr, nullptr);
if (!managedObjects) {
FF_DEBUG("Failed to call GetManagedObjects");
return "Failed to call GetManagedObjects";
}
DBusMessageIter rootIter;
if (!dbus.lib->ffdbus_message_iter_init(managedObjects, &rootIter)) {
FF_DEBUG("Failed to get root iterator of GetManagedObjects");
dbus.lib->ffdbus_message_unref(managedObjects);
return "Failed to get root iterator of GetManagedObjects";
}
@@ -351,6 +354,7 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF
return detectBluetooth(options, devices, connectedCount);
#else
FF_UNUSED(options, devices);
FF_DEBUG("Fastfetch was compiled without DBus support");
return "Fastfetch was compiled without DBus support";
#endif
}
@@ -1,4 +1,5 @@
#include "bluetooth.h"
#include "common/debug.h"
#include "common/library.h"
#include "common/mallocHelper.h"
#include "common/windows/unicode.h"
@@ -64,6 +65,7 @@ static const char* detectBattery(FFlist* devices) {
// not in the media class at all, so narrowing the filter would lose it.
CONFIGRET status = CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_MEDIA_STRING, CM_GETIDLIST_FILTER_PRESENT);
if (status != CR_SUCCESS) {
FF_DEBUG("CM_Get_Device_ID_List_SizeW failed: %s", ffDebugConfigRet(status));
return "CM_Get_Device_ID_List_SizeW failed";
}
@@ -73,11 +75,13 @@ static const char* detectBattery(FFlist* devices) {
FF_AUTO_FREE wchar_t* idList = (wchar_t*) malloc((size_t) idListLength * sizeof(wchar_t));
if (!idList) {
FF_DEBUG("malloc() failed");
return "malloc() failed";
}
status = CM_Get_Device_ID_ListW(GUID_DEVCLASS_MEDIA_STRING, idList, idListLength, CM_GETIDLIST_FILTER_PRESENT);
if (status != CR_SUCCESS) {
FF_DEBUG("CM_Get_Device_ID_ListW failed: %s", ffDebugConfigRet(status));
return "CM_Get_Device_ID_ListW failed";
}
@@ -137,6 +141,7 @@ static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /*
&btdi);
if (!hFind) {
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
FF_DEBUG("BluetoothFindFirstDevice() failed: %s", ffDebugWin32Error(GetLastError()));
return "BluetoothFindFirstDevice() failed";
}
// "Nothing found" is not an error here: this search only ever sees BR/EDR devices, and a
+14 -4
View File
@@ -1,5 +1,6 @@
extern "C" {
#include "bluetooth.h"
#include "common/debug.h"
#include "common/mallocHelper.h"
#include "common/percent.h"
#include "common/strutil.h"
@@ -280,24 +281,32 @@ extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist*
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>* ledeviceStatics = nullptr;
if (FAILED(ffGetActivationFactory<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>(L"Windows.Devices.Bluetooth.BluetoothLEDevice", &ledeviceStatics)) || !ledeviceStatics) {
HRESULT ledeviceHr = ffGetActivationFactory<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>(L"Windows.Devices.Bluetooth.BluetoothLEDevice", &ledeviceStatics);
if (FAILED(ledeviceHr) || !ledeviceStatics) {
FF_DEBUG("winrt: RoGetActivationFactory(BluetoothLEDevice) failed: %s", ffDebugHResult(ledeviceHr));
return "winrt: RoGetActivationFactory(BluetoothLEDevice) failed";
}
[[gnu::cleanup(ffDeleteHstring)]] HSTRING selector = nullptr;
if (FAILED(ledeviceStatics->GetDeviceSelectorFromPairingState(true, reinterpret_cast<void**>(&selector))) || !selector) {
HRESULT selectorHr = ledeviceStatics->GetDeviceSelectorFromPairingState(true, reinterpret_cast<void**>(&selector));
if (FAILED(selectorHr) || !selector) {
FF_DEBUG("winrt: GetDeviceSelectorFromPairingState() failed: %s", ffDebugHResult(selectorHr));
return "winrt: GetDeviceSelectorFromPairingState() failed";
}
// Without additional properties the map carries only the eight generic ones, and every AEP key
// is absent, so the list is not optional.
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>* properties = nullptr;
if (FAILED(ffWinrtCreateHstringIterable(ffBluetoothLeProperties, (uint32_t) ARRAY_SIZE(ffBluetoothLeProperties), &properties)) || !properties) {
HRESULT propertiesHr = ffWinrtCreateHstringIterable(ffBluetoothLeProperties, (uint32_t) ARRAY_SIZE(ffBluetoothLeProperties), &properties);
if (FAILED(propertiesHr) || !properties) {
FF_DEBUG("winrt: building the additional-properties list failed: %s", ffDebugHResult(propertiesHr));
return "winrt: building the additional-properties list failed";
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>* enumerationStatics = nullptr;
if (FAILED(ffGetActivationFactory<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>(L"Windows.Devices.Enumeration.DeviceInformation", &enumerationStatics)) || !enumerationStatics) {
HRESULT deviceInfoHr = ffGetActivationFactory<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>(L"Windows.Devices.Enumeration.DeviceInformation", &enumerationStatics);
if (FAILED(deviceInfoHr) || !enumerationStatics) {
FF_DEBUG("winrt: RoGetActivationFactory(DeviceInformation) failed: %s", ffDebugHResult(deviceInfoHr));
return "winrt: RoGetActivationFactory(DeviceInformation) failed";
}
@@ -307,6 +316,7 @@ extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist*
},
&found);
if (FAILED(hr) || !found) {
FF_DEBUG("winrt: DeviceInformation::FindAllAsync() failed: %s", ffDebugHResult(hr));
return "winrt: DeviceInformation::FindAllAsync() failed";
}
@@ -1,4 +1,5 @@
#include "bluetoothradio.h"
#include "common/debug.h"
#include "common/strutil.h"
#ifdef FF_HAVE_DBUS
@@ -36,6 +37,7 @@ array [
static const char* detectBluetoothProperty(FFBluetoothRadioResult* device, FFDBusData* dbus, DBusMessageIter* iter) {
if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
FF_DEBUG("Expected dict entry");
return "Expected dict entry";
}
@@ -43,6 +45,7 @@ static const char* detectBluetoothProperty(FFBluetoothRadioResult* device, FFDBu
dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter);
if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
FF_DEBUG("Expected dict entry key to be a string");
return "Expected dict entry key to be a string";
}
@@ -82,17 +85,20 @@ static const char* detectBluetoothRoot(FFBluetoothRadioResult* device, const cha
DBusMessage* properties = ffDBusGetMethodReply(dbus, "org.bluez", objPath, "org.freedesktop.DBus.Properties", "GetAll", "org.bluez.Adapter1", nullptr);
if (!properties) {
FF_DEBUG("Failed to call org.freedesktop.DBus.Properties.GetAll");
return "Failed to call org.freedesktop.DBus.Properties.GetAll";
}
DBusMessageIter rootIter;
if (!dbus->lib->ffdbus_message_iter_init(properties, &rootIter)) {
dbus->lib->ffdbus_message_unref(properties);
FF_DEBUG("Failed to get root iterator of org.freedesktop.DBus.Properties.GetAll");
return "Failed to get root iterator of org.freedesktop.DBus.Properties.GetAll";
}
if (dbus->lib->ffdbus_message_iter_get_arg_type(&rootIter) != DBUS_TYPE_ARRAY) {
dbus->lib->ffdbus_message_unref(properties);
FF_DEBUG("Expected array");
return "Expected array";
}
@@ -110,6 +116,7 @@ static const char* detectBluetoothRoot(FFBluetoothRadioResult* device, const cha
static const char* detectBluetooth(FFlist* devices) {
FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth");
if (dirp == nullptr) {
FF_DEBUG("opendir(\"/sys/class/bluetooth\") failed: %s", strerror(errno));
return "Failed to open /sys/class/bluetooth";
}
@@ -148,6 +155,7 @@ const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */)
return detectBluetooth(devices);
#else
FF_UNUSED(devices)
FF_DEBUG("Fastfetch was compiled without DBus support");
return "Fastfetch was compiled without DBus support";
#endif
}
@@ -1,4 +1,5 @@
#include "bluetoothradio.h"
#include "common/debug.h"
#include "common/library.h"
#include "common/io.h"
#include "common/windows/unicode.h"
@@ -64,16 +65,20 @@ const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */)
&hRadio);
if (!hFind) {
if (GetLastError() == ERROR_NO_MORE_ITEMS) {
FF_DEBUG("No Bluetooth radios found or service disabled");
return "No Bluetooth radios found or service disabled";
} else {
FF_DEBUG("BluetoothFindFirstRadio() failed: %s", ffDebugWin32Error(GetLastError()));
return "BluetoothFindFirstRadio() failed";
}
}
do {
FF_AUTO_CLOSE_FD HANDLE hCurrent = hRadio;
BTH_LOCAL_RADIO_INFO blri;
DWORD returned;
if (!DeviceIoControl(hRadio, IOCTL_BTH_GET_LOCAL_INFO, nullptr, 0, &blri, sizeof(blri), &returned, nullptr)) {
if (!DeviceIoControl(hCurrent, IOCTL_BTH_GET_LOCAL_INFO, nullptr, 0, &blri, sizeof(blri), &returned, nullptr)) {
continue;
}
@@ -87,10 +92,8 @@ const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */)
device->lmpSubversion = blri.radioInfo.lmpSubversion;
ffStrbufInitStatic(&device->vendor, ffBluetoothRadioGetVendor(blri.radioInfo.mfg));
device->enabled = true;
device->connectable = ffBluetoothIsConnectable(hRadio);
device->discoverable = ffBluetoothIsDiscoverable(hRadio);
NtClose(hRadio);
device->connectable = ffBluetoothIsConnectable(hCurrent);
device->discoverable = ffBluetoothIsDiscoverable(hCurrent);
} while (ffBluetoothFindNextRadio(hFind, &hRadio));
ffBluetoothFindRadioClose(hFind);
+2
View File
@@ -1,10 +1,12 @@
#include "board.h"
#include "common/debug.h"
#include "common/apple/cf_helpers.h"
const char* ffDetectBoard(FFBoardResult* result) {
FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t service = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("IOPlatformExpertDevice"));
if (!service) {
FF_DEBUG("No IOPlatformExpertDevice found");
return "No IOPlatformExpertDevice found";
}
+3
View File
@@ -1,4 +1,5 @@
#include "board.h"
#include "common/debug.h"
#include "common/smbios.h"
typedef struct [[gnu::packed]] FFSmbiosBaseboard {
@@ -23,11 +24,13 @@ static_assert(offsetof(FFSmbiosBaseboard, ContainedObjectHandles) == 0x0F,
const char* ffDetectBoard(FFBoardResult* board) {
const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable();
if (!smbiosTable) {
FF_DEBUG("Baseboard information: failed to get SMBIOS data");
return "Failed to get SMBIOS data";
}
const FFSmbiosBaseboard* data = (const FFSmbiosBaseboard*) (*smbiosTable)[FF_SMBIOS_TYPE_BASEBOARD_INFO];
if (!data) {
FF_DEBUG("Baseboard information: section is not found in SMBIOS data");
return "Baseboard information section is not found in SMBIOS data";
}
+7
View File
@@ -1,5 +1,6 @@
#include "bootmgr.h"
#include "common/io.h"
#include "common/debug.h"
#include "common/apple/cf_helpers.h"
#include <IOKit/IOKitLib.h>
@@ -8,15 +9,18 @@ static const char* detectSecureBoot(bool* result) {
#if __aarch64__
FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/chosen");
if (!entryDevice) {
FF_DEBUG("IORegistryEntryFromPath(\"IODeviceTree:/chosen\") failed");
return "IORegistryEntryFromPath() failed";
}
FF_CFTYPE_AUTO_RELEASE CFTypeRef prop = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("secure-boot"), kCFAllocatorDefault, kNilOptions);
if (!prop) {
FF_DEBUG("IORegistryEntryCreateCFProperty(\"secure-boot\") failed");
return "IORegistryEntryCreateCFProperty() failed";
}
if (CFGetTypeID(prop) != CFDataGetTypeID() || CFDataGetLength((CFDataRef) prop) == 0) {
FF_DEBUG("Invalid secure-boot property");
return "Invalid secure-boot property";
}
@@ -24,15 +28,18 @@ static const char* detectSecureBoot(bool* result) {
#else
FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/options");
if (!entryDevice) {
FF_DEBUG("IORegistryEntryFromPath(\"IODeviceTree:/options\") failed");
return "IORegistryEntryFromPath() failed";
}
FF_CFTYPE_AUTO_RELEASE CFTypeRef prop = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("94b73556-2197-4702-82a8-3e1337dafbfb:AppleSecureBootPolicy"), kCFAllocatorDefault, 0);
if (!prop) {
FF_DEBUG("IORegistryEntryCreateCFProperty(AppleSecureBootPolicy) failed");
return "IORegistryEntryCreateCFProperty() failed";
}
if (CFGetTypeID(prop) != CFDataGetTypeID() || CFDataGetLength((CFDataRef) prop) == 0) {
FF_DEBUG("Invalid secure-boot property");
return "Invalid secure-boot property";
}
+4
View File
@@ -1,5 +1,6 @@
#include "bootmgr.h"
#include "efi_helper.h"
#include "common/debug.h"
#include "common/io.h"
#ifdef __OpenBSD__
@@ -22,6 +23,7 @@ typedef uint16_t efi_char;
const char* ffDetectBootmgr(FFBootmgrResult* result) {
FF_AUTO_CLOSE_FD int efifd = open("/dev/efi", O_RDWR | O_CLOEXEC);
if (efifd < 0) {
FF_DEBUG("open(\"/dev/efi\") failed: %s", strerror(errno));
return "open(/dev/efi) failed";
}
@@ -35,6 +37,7 @@ const char* ffDetectBootmgr(FFBootmgrResult* result) {
ioc.name = (efi_char[]) { 'B', 'o', 'o', 't', 'C', 'u', 'r', 'r', 'e', 'n', 't', '\0' };
ioc.namesize = sizeof("BootCurrent") * 2;
if (ioctl(efifd, EFIIOC_VAR_GET, &ioc) < 0 || ioc.datasize != 2) {
FF_DEBUG("ioctl(EFIIOC_VAR_GET, BootCurrent) failed");
return "ioctl(EFIIOC_VAR_GET, BootCurrent) failed";
}
@@ -46,6 +49,7 @@ const char* ffDetectBootmgr(FFBootmgrResult* result) {
ioc.name = (efi_char[]) { 'B', 'o', 'o', 't', hex[0], hex[1], hex[2], hex[3], '\0' };
ioc.namesize = sizeof("Boot####") * 2;
if (ioctl(efifd, EFIIOC_VAR_GET, &ioc) < 0 || ioc.datasize == sizeof(buffer)) {
FF_DEBUG("ioctl(EFIIOC_VAR_GET, Boot####) failed");
return "ioctl(EFIIOC_VAR_GET, Boot####) failed";
}

Some files were not shown because too many files have changed in this diff Show More