diff --git a/.codespellrc b/.codespellrc deleted file mode 100644 index 594f2e5b8f..0000000000 --- a/.codespellrc +++ /dev/null @@ -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 diff --git a/.gitattributes b/.gitattributes index 2a442b7180..e340b8450e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 0000000000..aa746f421b --- /dev/null +++ b/.github/workflows/build-android.yml @@ -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* , 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-*.* diff --git a/.github/workflows/build-benchmark.yml b/.github/workflows/build-benchmark.yml index ebead8bf38..06160b3592 100644 --- a/.github/workflows/build-benchmark.yml +++ b/.github/workflows/build-benchmark.yml @@ -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 diff --git a/.github/workflows/build-dragonfly-amd64.yml b/.github/workflows/build-dragonfly-amd64.yml index 10e4d1b776..00678c7b0c 100644 --- a/.github/workflows/build-dragonfly-amd64.yml +++ b/.github/workflows/build-dragonfly-amd64.yml @@ -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 diff --git a/.github/workflows/build-freebsd-amd64.yml b/.github/workflows/build-freebsd-amd64.yml index c82abfa814..7f5d0d3dba 100644 --- a/.github/workflows/build-freebsd-amd64.yml +++ b/.github/workflows/build-freebsd-amd64.yml @@ -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 diff --git a/.github/workflows/build-linux-armv7l.yml b/.github/workflows/build-linux-armv7l.yml index 16d239c5de..c319d77cac 100644 --- a/.github/workflows/build-linux-armv7l.yml +++ b/.github/workflows/build-linux-armv7l.yml @@ -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 diff --git a/.github/workflows/build-linux-hosts.yml b/.github/workflows/build-linux-hosts.yml index 8c9c3ed8a8..b4c4761e99 100644 --- a/.github/workflows/build-linux-hosts.yml +++ b/.github/workflows/build-linux-hosts.yml @@ -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 diff --git a/.github/workflows/build-linux-i686.yml b/.github/workflows/build-linux-i686.yml index 85f1efb455..51af1c9fc2 100644 --- a/.github/workflows/build-linux-i686.yml +++ b/.github/workflows/build-linux-i686.yml @@ -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 diff --git a/.github/workflows/build-linux-loong64.yml b/.github/workflows/build-linux-loong64.yml index 2a69a2c2de..088d76930c 100644 --- a/.github/workflows/build-linux-loong64.yml +++ b/.github/workflows/build-linux-loong64.yml @@ -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 diff --git a/.github/workflows/build-linux-vms.yml b/.github/workflows/build-linux-vms.yml index 73bfd9c667..2bfcf1359e 100644 --- a/.github/workflows/build-linux-vms.yml +++ b/.github/workflows/build-linux-vms.yml @@ -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 diff --git a/.github/workflows/build-macos-hosts.yml b/.github/workflows/build-macos-hosts.yml index a145ac8dc2..c9360b7fda 100644 --- a/.github/workflows/build-macos-hosts.yml +++ b/.github/workflows/build-macos-hosts.yml @@ -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" diff --git a/.github/workflows/build-musl-amd64.yml b/.github/workflows/build-musl-amd64.yml index 07fad3a7f8..bb980a1985 100644 --- a/.github/workflows/build-musl-amd64.yml +++ b/.github/workflows/build-musl-amd64.yml @@ -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} diff --git a/.github/workflows/build-netbsd-amd64.yml b/.github/workflows/build-netbsd-amd64.yml index 758f1da551..3a8409d678 100644 --- a/.github/workflows/build-netbsd-amd64.yml +++ b/.github/workflows/build-netbsd-amd64.yml @@ -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 diff --git a/.github/workflows/build-no-features-test.yml b/.github/workflows/build-no-features-test.yml index 28e2c483c8..499cdc14e3 100644 --- a/.github/workflows/build-no-features-test.yml +++ b/.github/workflows/build-no-features-test.yml @@ -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 diff --git a/.github/workflows/build-omnios-amd64.yml b/.github/workflows/build-omnios-amd64.yml index 4975cd872b..1a4deead4e 100644 --- a/.github/workflows/build-omnios-amd64.yml +++ b/.github/workflows/build-omnios-amd64.yml @@ -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 diff --git a/.github/workflows/build-openbsd-amd64.yml b/.github/workflows/build-openbsd-amd64.yml index e962cc24b2..529019adea 100644 --- a/.github/workflows/build-openbsd-amd64.yml +++ b/.github/workflows/build-openbsd-amd64.yml @@ -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 diff --git a/.github/workflows/build-solaris-amd64.yml b/.github/workflows/build-solaris-amd64.yml index 6392a06a53..b0e77d72d0 100644 --- a/.github/workflows/build-solaris-amd64.yml +++ b/.github/workflows/build-solaris-amd64.yml @@ -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 diff --git a/.github/workflows/build-spellcheck.yml b/.github/workflows/build-spellcheck.yml deleted file mode 100644 index e57c92218b..0000000000 --- a/.github/workflows/build-spellcheck.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/build-windows-hosts.yml b/.github/workflows/build-windows-hosts.yml index 18f53bb658..ff95bd27bb 100644 --- a/.github/workflows/build-windows-hosts.yml +++ b/.github/workflows/build-windows-hosts.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16f6ce34c2..99e92778b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9cbe6bb9..61d12fc3ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/CMakeLists.txt b/CMakeLists.txt index 2acd937c29..f435131fda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 _* (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: + # 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}" "$" + 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}" "$" + 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() ################## diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c89492b32b..985bb15b26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 | `FFOptions` | `FFCPUOptions` | | Detection results | `FFResult` | `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 # 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 diff --git a/README.md b/README.md index b3e66cee84..160a1975da 100644 --- a/README.md +++ b/README.md @@ -248,50 +248,32 @@ Set the key to a white space. ### Q: How can I display images on Windows? -As of April 2025: - -#### mintty and Wezterm - -mintty (used by Bash on Windows and MSYS2) and Wezterm (nightly build only) support the iTerm image protocol on Windows. - -In `config.jsonc`: -```json -{ - "logo": { - "type": "iterm", - "source": "C:/path/to/image.png", - "width": - } -} -``` - #### Windows Terminal Windows Terminal supports the sixel image protocol only. -* If you installed fastfetch through MSYS2: - 1. Install imagemagick: `pacman -S mingw-w64--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 + "source": "C:/path/to/image.png", // ~ can be used to refer to the Windows user home directory "width": , // Optional "height": // 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 + +#### mintty and Wezterm + +mintty (used by Bash on Windows and MSYS2) and Wezterm (nightly build only) support the iTerm image protocol on Windows. + +In `config.jsonc`: +```json { "logo": { - "type": "raw", // DO NOT USE "auto" - "source": "C:/path/to/image.sixel", - "width": , // Required - "height": // Required + "type": "iterm", + "source": "C:/path/to/image.png", + "width": } } ``` diff --git a/completions/fastfetch.bash b/completions/fastfetch.bash index 681046ddf6..da159f81b9 100644 --- a/completions/fastfetch.bash +++ b/completions/fastfetch.bash @@ -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 ;; diff --git a/debian/control b/debian/control index 7494ef59cc..003c0793ae 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: fastfetch Section: universe/utils Priority: optional Maintainer: Carter Li -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 diff --git a/doc/help.json b/doc/help.json index 471abc6206..1188434d83 100644 --- a/doc/help.json +++ b/doc/help.json @@ -630,6 +630,7 @@ "cs": "Czech", "de": "German", "es": "Spanish", + "fi": "Finnish", "fr": "French", "gl": "Galician", "he": "Hebrew", diff --git a/doc/json_schema.json b/doc/json_schema.json index f65c5dcd91..a7c6eca705 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -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 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" }, diff --git a/src/common/FFstrbuf.h b/src/common/FFstrbuf.h index 879508a072..214f4cdb63 100644 --- a/src/common/FFstrbuf.h +++ b/src/common/FFstrbuf.h @@ -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); diff --git a/src/common/android/applabel.c b/src/common/android/applabel.c new file mode 100644 index 0000000000..848c78eb1a --- /dev/null +++ b/src/common/android/applabel.c @@ -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 +#include +#include +#include +#include +#include +#include + +// 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>==/-<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; +} diff --git a/src/common/android/applabel.h b/src/common/android/applabel.h new file mode 100644 index 0000000000..c0a76e4f63 --- /dev/null +++ b/src/common/android/applabel.h @@ -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); diff --git a/src/common/android/binder.c b/src/common/android/binder.c index 41e1635242..058e33c36c 100644 --- a/src/common/android/binder.c +++ b/src/common/android/binder.c @@ -1,5 +1,8 @@ #include "common/android/binder.h" +#include "common/debug.h" +#include "common/io.h" +#include #include #include #include @@ -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"; } diff --git a/src/common/android/dex.c b/src/common/android/dex.c index e8e68a81cb..9d3dcf6937 100644 --- a/src/common/android/dex.c +++ b/src/common/android/dex.c @@ -1,13 +1,16 @@ #include "common/android/dex.h" +#include "common/android/zip.h" +#include "common/debug.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include #include #include #include -#include #ifdef FF_HAVE_ZLIB - #include "common/library.h" - #include + #include "common/zlib.h" #endif // Offsets into the dex header, from https://source.android.com/docs/core/runtime/dex-format. @@ -18,7 +21,10 @@ #define FF_DEX_OFF_STRING_IDS 0x3C #define FF_DEX_OFF_TYPE_IDS_SIZE 0x40 #define FF_DEX_OFF_TYPE_IDS 0x44 +#define FF_DEX_OFF_FIELD_IDS_SIZE 0x50 #define FF_DEX_OFF_FIELD_IDS 0x54 +#define FF_DEX_OFF_METHOD_IDS_SIZE 0x58 +#define FF_DEX_OFF_METHOD_IDS 0x5C #define FF_DEX_OFF_CLASS_DEFS_SIZE 0x60 #define FF_DEX_OFF_CLASS_DEFS 0x64 @@ -30,21 +36,30 @@ // field_id_item: [u16 class_idx][u16 type_idx][u32 name_idx] #define FF_DEX_FIELD_ID_SIZE 8 +#define FF_DEX_OFF_FIELD_ID_TYPE 2 #define FF_DEX_OFF_FIELD_ID_NAME 4 -// The zip local file header, from APPNOTE.TXT 4.3.7. The central directory would carry the same -// fields, but the local header sits right in front of the data, so one walk over the file finds -// both the bounds and the payload. -#define FF_ZIP_LOCAL_HEADER_SIZE 30 -#define FF_ZIP_OFF_METHOD 8 -#define FF_ZIP_OFF_COMPRESSED_SIZE 18 -#define FF_ZIP_OFF_UNCOMPRESSED_SIZE 22 -#define FF_ZIP_OFF_NAME_LENGTH 26 -#define FF_ZIP_OFF_EXTRA_LENGTH 28 -#define FF_ZIP_METHOD_STORED 0 -#define FF_ZIP_METHOD_DEFLATED 8 +// method_id_item: [u16 class_idx][u16 proto_idx][u32 name_idx] +#define FF_DEX_METHOD_ID_SIZE 8 +#define FF_DEX_OFF_METHOD_ID_NAME 4 + +// code_item: [u16 registers_size][u16 ins_size][u16 outs_size][u16 tries_size][u32 debug_info_off] +// [u32 insns_size][u16 insns...]. Everything before the instruction stream is skipped by reading +// `insns_size` and starting `insns` where it ends. +#define FF_DEX_OFF_CODE_INSNS_SIZE 12 +#define FF_DEX_OFF_CODE_INSNS 16 + +// The seven `iget*` opcodes, which are how a method reads an instance field. `iput*` shares the same +// format and the same operand, and a `writeToParcel` does not store to a field, so one range covers +// both and the walk does not have to tell them apart. +#define FF_DEX_OP_FIELD_FIRST 0x52 +#define FF_DEX_OP_FIELD_LAST 0x5F #define FF_DEX_ENTRY "classes.dex" +// How many `classesN.dex` entries a jar is walked for. A jar that spreads its classes over more +// than this still resolves everything in the entries that were walked; only a class that lives in +// one of the ones past the bound is reported as missing. +#define FF_DEX_MAX_ENTRIES 10 #define FF_DEX_MAGIC "dex\n" static uint16_t dexU16(const uint8_t* p) { @@ -96,18 +111,34 @@ static uint32_t dexUleb128(const uint8_t** p, const uint8_t* end) { } // The type tag a dex encoded_value carries in the low five bits of its header byte, per the -// `encoded_value` table of the dex format (`VALUE_INT = 0x04`, sign-extended four-byte integer). -// 0x1f is VALUE_BOOLEAN in that same table, which is the trap: naming it VALUE_INT and comparing -// against it rejects every real int and accepts booleans. -#define FF_DEX_VALUE_INT 0x04 +// `encoded_value` table of the dex format. +// +// VALUE_NULL and VALUE_BOOLEAN are the trap. Their value *is* the header -- the type for a null, +// the size nibble for a boolean -- and they carry **no payload bytes at all**, so the +// `((size - 1) << 5) | type` rule that describes every other tag does not describe them. Taking the +// size nibble at face value and reading one payload byte walks one byte too far, which pairs every +// field after that with the wrong value. It is not a hypothetical: `IAudioService$Stub` declares ten +// `PERMISSIONS_*` arrays that are all null, right behind its `DESCRIPTOR` string and ahead of its +// 324 transaction constants, and those ten spurious bytes made +// `TRANSACTION_getDevicesForAttributesUnprotected` read as 210 instead of 167 -- a code that reaches +// a different method. `IWifiManager$Stub` has none ahead of the field the wifi module reads, which +// is the only reason this went unnoticed. +// +// Naming VALUE_BOOLEAN also matters for a second reason: it is the tag a `static final boolean` +// carries, and treating it as VALUE_INT would hand back a boolean where an int was asked for. +typedef enum FFDexValueType : uint32_t { + FF_DEX_VALUE_INT = 0x04, // sign-extended; one to four payload bytes + FF_DEX_VALUE_NULL = 0x1e, // no payload + FF_DEX_VALUE_BOOLEAN = 0x1f, // no payload; the boolean is in the header's size nibble +} FFDexValueType; // encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes. // // The `static_values` array is not a list of ints: `IInterface.DESCRIPTOR` is a `static final -// String` and sits in it, and a real Stub class mixes ints, strings and booleans. So a value whose -// tag is not VALUE_INT is skipped -- its payload is stepped over to keep the walk aligned with the -// field list -- and only `isInt` reports whether the *wanted* field ended up being one. Failing on -// the first non-int instead would abort on `DESCRIPTOR`, before the transaction constants that +// String` and sits in it, and a real Stub class mixes ints, strings, nulls and booleans. So a value +// whose tag is not VALUE_INT is skipped -- its payload is stepped over to keep the walk aligned with +// the field list -- and only `isInt` reports whether the *wanted* field ended up being one. Failing +// on the first non-int instead would abort on `DESCRIPTOR`, before the transaction constants that // follow it are ever reached. Measured on a device: `IWifiManager$Stub` carries 347 static fields // whose values are mostly VALUE_INT but include that string, and `getConnectionInfo` is 98 there. // @@ -121,6 +152,15 @@ static bool dexEncodedValue(const uint8_t** p, const uint8_t* end, int32_t* resu } const uint8_t header = *(*p)++; const uint32_t type = (uint32_t) (header & 0x1f); + + // The two tags whose value is the header byte itself. Returning here is what keeps the walk in + // step; `isInt` is false because neither is an int. + if (type == FF_DEX_VALUE_NULL || type == FF_DEX_VALUE_BOOLEAN) { + *result = 0; + *isInt = false; + return true; + } + const uint32_t size = (uint32_t) (header >> 5) + 1; if (size > 8) { return false; @@ -173,12 +213,32 @@ static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t in return memchr(p, '\0', (size_t) (end - p)) != nullptr ? (const char*) p : nullptr; } -// Walks the class's static field list and the class's static value array in step, and returns the -// value paired with `fieldName`. Both lists are ordered by field index and cover exactly the same -// fields -- when they do not, the pairing is not trustworthy and nothing is returned. -static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, int32_t* result) { - const uint8_t* end = dex + size; +// One dex entry, validated. Every offset in the header comes out of the file, so each table is range +// checked before the walk uses it. +// +// `fieldIds` and `methodIds` are the two tables a class's own lists are resolved through: a +// class_data_item names its members by index into them, and the `name_idx` of the entry is what says +// which member it is. Holding them here rather than re-reading the header offset per class also +// turns the per-member bounds check into a count comparison, which has to happen before the index is +// multiplied out anyway -- on a 32-bit build `index * 8` wraps for a large enough index and the +// range check that follows passes again. +// +// `string_ids` is deliberately not held here: `dexString` checks the one entry it reads, and a table +// nothing walks in order does not need a second check. +typedef struct FFDexTables { + const uint8_t* dex; + const uint8_t* end; + const uint8_t* types; + uint32_t typeCount; + const uint8_t* fieldIds; + uint32_t fieldIdCount; + const uint8_t* methodIds; + uint32_t methodIdCount; + const uint8_t* classes; + uint32_t classCount; +} FFDexTables; +static const char* dexOpen(const uint8_t* dex, size_t size, FFDexTables* tables) { if (size < FF_DEX_HEADER_SIZE || dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) { return "Not a dex file"; } @@ -186,24 +246,24 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla if (fileSize > size || fileSize < FF_DEX_HEADER_SIZE) { return "The dex file size is out of range"; } - end = dex + fileSize; + const uint8_t* end = dex + fileSize; - // The class's type index, so that the class_def_item and the field ids can be filtered by class. const uint8_t* types = dex + dexU32(dex + FF_DEX_OFF_TYPE_IDS); const uint32_t typeCount = dexU32(dex + FF_DEX_OFF_TYPE_IDS_SIZE); if (!dexTableInRange(dex, end, types, typeCount, 4)) { return "The dex type table is out of range"; } - uint32_t typeIndex = UINT32_MAX; - for (uint32_t i = 0; i < typeCount; ++i) { - const char* descriptor = dexString(dex, end, dexU32(types + (size_t) i * 4)); - if (descriptor != nullptr && strcmp(descriptor, classDescriptor) == 0) { - typeIndex = i; - break; - } + + const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS); + const uint32_t fieldIdCount = dexU32(dex + FF_DEX_OFF_FIELD_IDS_SIZE); + if (!dexTableInRange(dex, end, fieldIds, fieldIdCount, FF_DEX_FIELD_ID_SIZE)) { + return "The dex field table is out of range"; } - if (typeIndex == UINT32_MAX) { - return "The class is not in the dex"; + + const uint8_t* methodIds = dex + dexU32(dex + FF_DEX_OFF_METHOD_IDS); + const uint32_t methodIdCount = dexU32(dex + FF_DEX_OFF_METHOD_IDS_SIZE); + if (!dexTableInRange(dex, end, methodIds, methodIdCount, FF_DEX_METHOD_ID_SIZE)) { + return "The dex method table is out of range"; } const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS); @@ -212,66 +272,532 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla return "The dex class table is out of range"; } - for (uint32_t i = 0; i < classCount; ++i) { - const uint8_t* classDef = classes + (size_t) i * FF_DEX_CLASS_DEF_SIZE; - if (dexU32(classDef) != typeIndex) { + tables->dex = dex; + tables->end = end; + tables->types = types; + tables->typeCount = typeCount; + tables->fieldIds = fieldIds; + tables->fieldIdCount = fieldIdCount; + tables->methodIds = methodIds; + tables->methodIdCount = methodIdCount; + tables->classes = classes; + tables->classCount = classCount; + return nullptr; +} + +// The descriptor a type index names, e.g. "I", "[F" or "Landroid/os/Parcel;". +static const char* dexTypeString(const FFDexTables* tables, uint32_t typeIndex) { + if (typeIndex >= tables->typeCount) { + return nullptr; + } + return dexString(tables->dex, tables->end, dexU32(tables->types + (size_t) typeIndex * 4)); +} + +// One pass over the type table, resolving the type index of every class that still has a request +// open. This pass is what a lookup costs: `framework.jar`'s classes.dex lists 8428 descriptors, each +// one a `string_data_item` read from a different part of a 51 MB mapping. Matching every request +// against the same pass is what makes a batch cheaper than the sum of its fields -- the descriptor is +// read once however many classes are being looked for, and the pass ends as soon as the last request +// has an index rather than at the end of the table. +// +// `typeIndexes` is parallel to `descriptors` and rebuilt here rather than kept across entries: a +// type index is a property of the entry's own table, and the next entry numbers its types +// differently. +static void dexFindTypes(const FFDexTables* tables, const char* const* descriptors, const uint8_t* settled, uint32_t* typeIndexes, uint32_t count) { + uint32_t remaining = 0; + for (uint32_t r = 0; r < count; ++r) { + typeIndexes[r] = UINT32_MAX; + if (!settled[r]) { + ++remaining; + } + } + + for (uint32_t i = 0; i < tables->typeCount && remaining > 0; ++i) { + const char* descriptor = dexString(tables->dex, tables->end, dexU32(tables->types + (size_t) i * 4)); + if (descriptor == nullptr) { continue; } + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != UINT32_MAX || strcmp(descriptor, descriptors[r]) != 0) { + continue; + } + typeIndexes[r] = i; + --remaining; + } + } +} + +// The lists of a class_data_item, each positioned at its first entry. +// +// A class_data_item is `[uleb static_fields_size][uleb instance_fields_size][uleb direct_methods_size] +// [uleb virtual_methods_size]` followed by the four lists in that order, so the start of each list is +// only reachable by stepping over the one in front of it. That is done once here for the three +// readers that each want a different list: the static field reader pairs the first with the class's +// static value array, the write-sequence reader walks the methods, and the field-existence reader +// walks the second. +// +// A field entry is `[uleb field_idx_diff][uleb access_flags]` and a method entry is the same plus a +// trailing `[uleb code_off]`, which is the offset of the code_item holding its instructions -- or 0 +// for a method with no body, an abstract or native one. +// +// Each of the four lists is delta-encoded from zero on its own, so the counts are kept apart rather +// than summed: a class routinely has a direct method and a virtual one under the same index, and one +// accumulator carried across the two method lists reads the second list's names out of the wrong end +// of the method table. The field lists are read the same way, which is why the reader of each one +// starts its index at zero rather than continuing from the list in front of it. +typedef struct FFDexClassData { + const uint8_t* staticFields; + const uint8_t* instanceFields; + const uint8_t* methods; + uint32_t staticFieldCount; + uint32_t instanceFieldCount; + uint32_t directMethodCount; + uint32_t virtualMethodCount; +} FFDexClassData; - const uint8_t* fields = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_DATA); - const uint8_t* values = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_STATIC_VALUES); - // One byte each, so that the first uleb128 below has something to read. - if (!dexInRange(dex, end, fields, 1) || !dexInRange(dex, end, values, 1)) { - return "The dex class data is out of range"; +// Steps over `count` field entries. A list that ends before its own count is a file that is not what +// it claims, not a short list: the count and the entries both come out of the same file. +static bool dexSkipFields(const uint8_t** p, const uint8_t* end, uint32_t count) { + for (uint32_t i = 0; i < count; ++i) { + if (*p >= end) { + return false; } + (void) dexUleb128(p, end); // field_idx_diff + (void) dexUleb128(p, end); // access_flags + } + return true; +} + +static const char* dexReadClassData(const FFDexTables* tables, const uint8_t* classDef, FFDexClassData* data) { + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + const uint32_t classDataOff = dexU32(classDef + FF_DEX_OFF_CLASS_DEF_DATA); + if (classDataOff == 0) { + // A class with no class_data_item declares nothing of its own -- its members are all + // inherited. That is four empty lists, not a file that cannot be read, and the counts below + // are what carry the answer. + data->staticFields = nullptr; + data->instanceFields = nullptr; + data->methods = nullptr; + data->staticFieldCount = 0; + data->instanceFieldCount = 0; + data->directMethodCount = 0; + data->virtualMethodCount = 0; + return nullptr; + } + + const uint8_t* p = dex + classDataOff; + // One byte, so that the first uleb128 has something to read. + if (!dexInRange(dex, end, p, 1)) { + return "The dex class data is out of range"; + } - const uint32_t staticFields = dexUleb128(&fields, end); - (void) dexUleb128(&fields, end); // instance_fields_size - (void) dexUleb128(&fields, end); // direct_methods_size - (void) dexUleb128(&fields, end); // virtual_methods_size - const uint32_t staticValues = dexUleb128(&values, end); - if (staticFields != staticValues) { - return "The dex static fields and values do not pair up"; + data->staticFieldCount = dexUleb128(&p, end); + data->instanceFieldCount = dexUleb128(&p, end); + data->directMethodCount = dexUleb128(&p, end); + data->virtualMethodCount = dexUleb128(&p, end); + + data->staticFields = p; + if (!dexSkipFields(&p, end, data->staticFieldCount)) { + return "The dex class data is truncated"; + } + data->instanceFields = p; + if (!dexSkipFields(&p, end, data->instanceFieldCount)) { + return "The dex class data is truncated"; + } + data->methods = p; + return nullptr; +} + +// The class_def_item of the class that `typeIndex` names, or nullptr when this dex only references +// the class. +// +// Having a type index is not the same as being defined here: a dex lists every type it references in +// its type table, so a class that a later entry of the jar defines still has a type index in this +// one, with no class_def_item behind it. Only a class_def_item makes the answer this dex's, and +// without one the caller moves on to the next entry. +static const uint8_t* dexFindClassDef(const FFDexTables* tables, uint32_t typeIndex) { + for (uint32_t i = 0; i < tables->classCount; ++i) { + const uint8_t* classDef = tables->classes + (size_t) i * FF_DEX_CLASS_DEF_SIZE; + if (dexU32(classDef) == typeIndex) { + return classDef; } + } + return nullptr; +} + +// Walks the static field list of the class that `typeIndex` names and the class's static value array +// in step, writing the value of every request for that class whose field name matches. Both lists are +// ordered by field index and cover exactly the same fields -- when they do not, the pairing is not +// trustworthy and nothing is written. +// +// `defined` says whether this dex *defines* the class, which the caller needs: a request the class +// cannot answer has to be left open when the class belongs to a later entry of the jar and closed +// when it does not. +// +// A request this class cannot answer -- a field it does not declare, or one it declares as something +// other than an int -- is closed here rather than reported: the class is this dex's, so no later +// entry of the jar could give it a different answer, and one field the caller can live without must +// not hold back the rest of the batch. It is closed with the sentinel, and what this returns is only +// ever a dex that cannot be read, which is the one thing no later entry can fix. +static const char* dexClassStaticInts(const FFDexTables* tables, uint32_t typeIndex, const FFDexStaticIntRequest* requests, const uint32_t* typeIndexes, uint8_t* settled, uint32_t count, bool* defined) { + const uint8_t* classDef = dexFindClassDef(tables, typeIndex); + *defined = classDef != nullptr; + if (classDef == nullptr) { + return nullptr; + } + + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; - const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS); - uint32_t fieldIndex = 0; - for (uint32_t j = 0; j < staticFields; ++j) { - // The two arrays are walked in step, so running out of either one is a truncation. - if (fields >= end || values >= end) { - return "The dex static field list is truncated"; + FFDexClassData classData; + const char* error = dexReadClassData(tables, classDef, &classData); + if (error != nullptr) { + return error; + } + + // The static value array is parallel to the static field list and covers exactly the same fields, + // which is what makes walking the two in step below meaningful. It is absent in two cases -- a + // class with no static field, and a class none of whose static fields has a constant value -- and + // the header writes 0 for it in both. The read is skipped rather than made against whatever byte + // a zero offset points at, which is the dex magic and decodes as a four byte int. + const uint8_t* fields = classData.staticFields; + const uint8_t* values = dex; + bool hasValues = false; + if (classData.staticFieldCount > 0) { + const uint32_t staticValuesOff = dexU32(classDef + FF_DEX_OFF_CLASS_DEF_STATIC_VALUES); + if (staticValuesOff != 0) { + values = dex + staticValuesOff; + if (!dexInRange(dex, end, values, 1)) { + return "The dex class data is out of range"; } + if (classData.staticFieldCount != dexUleb128(&values, end)) { + return "The dex static fields and values do not pair up"; + } + hasValues = true; + } + } + + uint32_t fieldIndex = 0; + for (uint32_t j = 0; j < classData.staticFieldCount; ++j) { + // The two arrays are walked in step, so running out of either one is a truncation. + if (fields >= end || (hasValues && values >= end)) { + return "The dex static field list is truncated"; + } - const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff - if (fieldIndexDiff > UINT32_MAX - fieldIndex) { - return "The dex field index is out of range"; + const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff + if (fieldIndexDiff > UINT32_MAX - fieldIndex) { + return "The dex field index is out of range"; + } + fieldIndex += fieldIndexDiff; + (void) dexUleb128(&fields, end); // access_flags + int32_t value = 0; + // Left false when there is no value array, which closes the field with the sentinel below: + // a class with no static value for the field has no answer for it. + bool isInt = false; + if (hasValues && !dexEncodedValue(&values, end, &value, &isInt)) { + return "The dex static value list is truncated"; + } + + if (fieldIndex >= tables->fieldIdCount) { + return "The dex field table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME)); + if (name == nullptr) { + continue; + } + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != typeIndex || strcmp(name, requests[r].fieldName) != 0) { + continue; } - fieldIndex += fieldIndexDiff; - (void) dexUleb128(&fields, end); // access_flags - int32_t value; - bool isInt; - if (!dexEncodedValue(&values, end, &value, &isInt)) { - return "The dex static value list is truncated"; + if (!isInt) { + // The field exists but is not an int, so its value is not the constant asked for. + // Closed the way a field the class does not declare is, because it is this dex's + // answer too. The check is here rather than on the first non-int in the list because + // the list legitimately holds strings (`DESCRIPTOR`) and booleans. + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + settled[r] = 1; + continue; } + *requests[r].result = value; + settled[r] = 1; + } + } + + // The class is defined here, so a request of its own that went unanswered is one whose field the + // class does not declare as a static field. Closed here for the same reason as above. + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r] && typeIndexes[r] == typeIndex) { + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + settled[r] = 1; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------------------------- +// Walking a method's instructions +// --------------------------------------------------------------------------------------------- + +// The width of every instruction, in code units. This is ART's `Instruction::SizeInCodeUnits`, and +// the walk below cannot stay on the instruction grid without it -- a walk that leaves the grid still +// produces a plausible-looking list of fields, which is what makes this worth a table rather than a +// guess. Unused and odex-only opcodes are 1 rather than 0, so that a byte which is not an instruction +// at all moves the walk on instead of leaving it where it is. +static const uint8_t dexOpcodeSize[256] = { + /* 00 */ 1, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 1, 1, 1, 1, 1, + /* 10 */ 1, 1, 1, 2, 3, 2, 2, 3, 5, 2, 2, 3, 2, 1, 1, 2, + /* 20 */ 2, 1, 2, 2, 3, 3, 3, 1, 1, 2, 3, 3, 3, 2, 2, 2, + /* 30 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, + /* 40 */ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* 50 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* 60 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, + /* 70 */ 3, 3, 3, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, + /* 80 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 90 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* a0 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* b0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* c0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* d0 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* e0 */ 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* f0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 3, 3, 2, 2, +}; + +// The code_item at `codeOff`, with its instruction array range checked. nullptr is one that cannot +// be read: an offset outside the mapping, or an instruction count that runs past the end. +static const uint8_t* dexReadCode(const FFDexTables* tables, uint32_t codeOff) { + const uint8_t* code = tables->dex + codeOff; + if (!dexInRange(tables->dex, tables->end, code, FF_DEX_OFF_CODE_INSNS)) { + return nullptr; + } + const uint32_t insnsSize = dexU32(code + FF_DEX_OFF_CODE_INSNS_SIZE); + if (!dexTableInRange(tables->dex, tables->end, code + FF_DEX_OFF_CODE_INSNS, insnsSize, 2)) { + return nullptr; + } + return code; +} + +// Records `name` and `type` as the next field of a write sequence, unless the name is already in it. +// +// An array field is read twice: once for its length, which is the count written ahead of the elements, +// and again inside the loop that writes them. A class cannot declare two instance fields under one +// name, so a repeat is always that reload, and the first read is where the field belongs in the +// sequence. +// +// A name or descriptor longer than the caller's buffer is refused rather than truncated: half a name +// matches nothing, which is indistinguishable from the field not being in the class at all. Refusing +// one field leaves the whole sequence unwritten, because a caller walking a sequence with a hole in +// it would read every field behind the hole from the wrong place. +static bool dexParcelFieldAdd(FFDexParcelField* fields, uint32_t* count, uint32_t capacity, const char* name, const char* type) { + for (uint32_t i = 0; i < *count; ++i) { + if (strcmp(fields[i].name, name) == 0) { + return true; + } + } + if (*count >= capacity) { + return false; + } + const size_t nameSize = strlen(name) + 1; + const size_t typeSize = strlen(type) + 1; + if (nameSize > FF_DEX_PARCEL_FIELD_NAME_MAX || typeSize > FF_DEX_PARCEL_FIELD_TYPE_MAX) { + return false; + } + memcpy(fields[*count].name, name, nameSize); + memcpy(fields[*count].type, type, typeSize); + ++*count; + return true; +} - if (!dexTableInRange(dex, end, fieldIds, (uint64_t) fieldIndex + 1, FF_DEX_FIELD_ID_SIZE)) { +// Walks the instruction grid of a code_item and records every instance field the method reads, in the +// order it reads them -- which is the order `writeToParcel` puts them on the wire. +// +// The grid is followed by opcode width, so the walk only ever lands on an instruction start, and it +// stops at the first payload pseudo-instruction. A `switch` or a `fill-array-data` leaves one behind +// every reachable instruction; its contents are data rather than instructions, and stepping into it +// desynchronises the grid for good. +// +// `iget*` and `iput*` share a format and put the field index in the code unit behind the opcode in +// both, which is what lets one test cover the whole range. A `writeToParcel` does not store to a +// field, so the `iput` half is never taken -- it is in the range because splitting it would be a +// claim about the method that the opcode does not make. +static const char* dexWalkParcelFields(const FFDexTables* tables, const uint8_t* code, FFDexParcelField* fields, uint32_t capacity, uint32_t* count) { + *count = 0; + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + const uint32_t insnsSize = dexU32(code + FF_DEX_OFF_CODE_INSNS_SIZE); + const uint8_t* insns = code + FF_DEX_OFF_CODE_INSNS; + + for (uint32_t i = 0; i < insnsSize;) { + const uint16_t unit = dexU16(insns + (size_t) i * 2); + // The three payload pseudo-instructions put their ident in the high byte and zero in the low + // one. `nop` is the one instruction whose low byte is zero as well, and it is told apart by + // the whole code unit being zero. Testing the high byte alone is what gets this wrong: a + // `sparse-switch` over register 2 encodes as 0x022c, which is an instruction, not a payload. + if ((unit & 0xFF) == 0 && unit != 0) { + break; + } + const uint32_t opcode = unit & 0xFF; + if (opcode >= FF_DEX_OP_FIELD_FIRST && opcode <= FF_DEX_OP_FIELD_LAST) { + if (i + 1 >= insnsSize) { + break; + } + const uint32_t fieldIndex = dexU16(insns + ((size_t) i + 1) * 2); + if (fieldIndex >= tables->fieldIdCount) { return "The dex field table is out of range"; } - const char* name = dexString(dex, end, dexU32(fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME)); - if (name != nullptr && strcmp(name, fieldName) == 0) { - if (!isInt) { - // The field exists but is not an int, so its value is not the constant asked - // for. Reported here rather than on the first non-int in the list, because the - // list legitimately holds strings (`DESCRIPTOR`) and booleans. - return "The dex static field is not an int"; - } - *result = value; + const uint8_t* fieldId = tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE; + const char* name = dexString(dex, end, dexU32(fieldId + FF_DEX_OFF_FIELD_ID_NAME)); + const char* type = dexTypeString(tables, dexU16(fieldId + FF_DEX_OFF_FIELD_ID_TYPE)); + if (name == nullptr || type == nullptr) { + return "The dex field is not resolvable"; + } + if (!dexParcelFieldAdd(fields, count, capacity, name, type)) { + FF_DEBUG("\"%s\" (%s) does not fit a sequence of %u fields", name, type, capacity); + *count = 0; return nullptr; } } - return "The field is not a static field of the class"; + i += dexOpcodeSize[opcode]; + } + return nullptr; +} + +// Walks the instance field list of one `class_data_item` and writes `true` for every name it holds. +// The list is the caller's, so this answers one request's worth of names or several. +// +// This is the question a Parcelable whose layout is not field-driven needs: `DisplayCutout` reads +// everything through compiler-synthesised accessors, so the set of fields its class declares is the +// only thing that says how long its body is. It is asked as part of a write-sequence request rather +// than by a call of its own -- the list is already positioned by the `class_data_item` the sequence +// comes out of, and a separate call would repeat the whole walk to reach it. +static const char* dexMatchInstanceFields(const FFDexTables* tables, const uint8_t* fields, uint32_t fieldCount, const char* const* names, bool* results, uint32_t nameCount) { + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + uint32_t fieldIndex = 0; + for (uint32_t j = 0; j < fieldCount; ++j) { + if (fields >= end) { + return "The dex class data is truncated"; + } + const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff + if (fieldIndexDiff > UINT32_MAX - fieldIndex) { + return "The dex field index is out of range"; + } + fieldIndex += fieldIndexDiff; + (void) dexUleb128(&fields, end); // access_flags + + if (fieldIndex >= tables->fieldIdCount) { + return "The dex field table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME)); + if (name == nullptr) { + continue; + } + for (uint32_t i = 0; i < nameCount; ++i) { + if (!results[i] && strcmp(name, names[i]) == 0) { + results[i] = true; + } + } + } + return nullptr; +} + +// Walks the method list of the class that `typeIndex` names, and for every request whose method name +// it holds walks that method's instructions. `defined` is what `dexClassStaticInts` reports, for the +// same reason. +// +// A request the class does not answer is closed with a count of 0 -- the count a caller reads as +// "this build does not write that class", which is the truth: the class is this dex's, so no later +// entry of the jar has the method either. +static const char* dexClassParcelFields(const FFDexTables* tables, uint32_t typeIndex, const FFDexParcelRequest* requests, const uint32_t* typeIndexes, uint8_t* settled, uint32_t count, bool* defined) { + const uint8_t* classDef = dexFindClassDef(tables, typeIndex); + *defined = classDef != nullptr; + if (classDef == nullptr) { + return nullptr; + } + + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + FFDexClassData classData; + const char* error = dexReadClassData(tables, classDef, &classData); + if (error != nullptr) { + return error; + } + + // The instance fields first, out of the list the `class_data_item` has already positioned. A + // request that asked for them is answered here whether or not it also asked for a sequence, and + // one that asked for neither is settled by the loop at the end like any other. + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != typeIndex || requests[r].instanceFieldCount == 0) { + continue; + } + error = dexMatchInstanceFields( + tables, classData.instanceFields, classData.instanceFieldCount, + requests[r].instanceFieldNames, requests[r].instanceFieldResults, requests[r].instanceFieldCount + ); + if (error != nullptr) { + return error; + } + } + + const uint8_t* methods = classData.methods; + // Two runs, each delta-encoded from zero on its own -- so the index is reset per run rather than + // carried across, and a direct method and a virtual one under the same index both resolve. + for (uint32_t list = 0; list < 2; ++list) { + const uint32_t listCount = list == 0 ? classData.directMethodCount : classData.virtualMethodCount; + uint32_t methodIndex = 0; + for (uint32_t j = 0; j < listCount; ++j) { + if (methods >= end) { + return "The dex class data is truncated"; + } + const uint32_t methodIndexDiff = dexUleb128(&methods, end); // method_idx_diff + if (methodIndexDiff > UINT32_MAX - methodIndex) { + return "The dex method index is out of range"; + } + methodIndex += methodIndexDiff; + (void) dexUleb128(&methods, end); // access_flags + const uint32_t codeOff = dexUleb128(&methods, end); + + if (methodIndex >= tables->methodIdCount) { + return "The dex method table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->methodIds + (size_t) methodIndex * FF_DEX_METHOD_ID_SIZE + FF_DEX_OFF_METHOD_ID_NAME)); + if (name == nullptr) { + continue; + } + + for (uint32_t r = 0; r < count; ++r) { + // A request that left the sequence out is one that only wanted the instance fields, + // which are already written; it has no method name to match against. + if (settled[r] || typeIndexes[r] != typeIndex || requests[r].methodName == nullptr + || strcmp(name, requests[r].methodName) != 0) { + continue; + } + // A method with no body -- abstract, or native -- writes nothing, and the count stays 0. + const uint8_t* code = codeOff == 0 ? nullptr : dexReadCode(tables, codeOff); + if (codeOff != 0 && code == nullptr) { + return "The dex code is out of range"; + } + if (code != nullptr) { + error = dexWalkParcelFields(tables, code, requests[r].fields, requests[r].capacity, requests[r].count); + if (error != nullptr) { + return error; + } + } + settled[r] = 1; + } + } + } + + // The class is defined here, so a request of its own that went unanswered is one whose method the + // class does not declare. The count it already holds is the 0 that says so. + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r] && typeIndexes[r] == typeIndex) { + settled[r] = 1; + } } - return "The class has no class_def_item"; + return nullptr; } // --------------------------------------------------------------------------------------------- @@ -279,59 +805,63 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla // --------------------------------------------------------------------------------------------- typedef struct FFDexMapping { - uint8_t* mapped; // the jar, as mapped; nullptr once released + uint8_t* mapped; // the jar, as mapped; nullptr once released size_t mappedSize; - const uint8_t* data; // the dex bytes: inside `mapped`, or `inflated` - size_t size; - uint8_t* inflated; // owned; only set when the entry had to be decompressed } FFDexMapping; static void wrapDexMapping(FFDexMapping* mapping) { assert(mapping); - free(mapping->inflated); if (mapping->mapped != nullptr) { munmap(mapping->mapped, mapping->mappedSize); } } -// The local file header of `classes.dex` carries both the payload bounds and its compression, so a -// single walk over the jar is enough to find it either way. AOSP builds framework jars with the -// entry STORED so that ART can map it, which is the common case and needs no decompression. -static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) { - for (size_t i = 0; i + FF_ZIP_LOCAL_HEADER_SIZE <= jarSize; ++i) { - if (memcmp(jar + i, "PK\x03\x04", 4) != 0) { - continue; - } - const uint16_t nameLength = dexU16(jar + i + FF_ZIP_OFF_NAME_LENGTH); - const uint16_t extraLength = dexU16(jar + i + FF_ZIP_OFF_EXTRA_LENGTH); - if (nameLength != sizeof(FF_DEX_ENTRY) - 1) { - continue; - } - const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) nameLength + (size_t) extraLength; - if (headerSize > jarSize - i) { - continue; - } - if (memcmp(jar + i + FF_ZIP_LOCAL_HEADER_SIZE, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY) - 1) != 0) { - continue; - } +// Maps the jar read-only. The pages the walk touches are the only ones faulted in, which is what +// keeps a 52 MB framework.jar off the heap and out of the cost of a lookup. The mapping outlives the +// descriptor, so the descriptor is closed before this returns. +static const char* dexMapJar(const char* jarPath, FFDexMapping* mapping) { + FF_AUTO_CLOSE_FD int fd = open(jarPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("open(%s) failed: %s", jarPath, strerror(errno)); + return "open(jar) failed"; + } - const uint32_t compressedSize = dexU32(jar + i + FF_ZIP_OFF_COMPRESSED_SIZE); - if (compressedSize == 0 || compressedSize > jarSize - i - headerSize) { - // A zero size means a data descriptor follows the payload instead of the header; the - // magic scan in the caller still covers that case as long as the entry is STORED. - continue; - } - *method = dexU16(jar + i + FF_ZIP_OFF_METHOD); - *uncompressedSize = dexU32(jar + i + FF_ZIP_OFF_UNCOMPRESSED_SIZE); - *data = jar + i + headerSize; - *dataSize = compressedSize; - return nullptr; + struct stat st = {}; + const int statStatus = fstat(fd, &st); + if (statStatus != 0 || st.st_size <= 0) { + FF_DEBUG("fstat(%s) reported %s (%lld bytes)", jarPath, + statStatus != 0 ? strerror(errno) : "an empty file", (long long) st.st_size); + return "fstat(jar) failed"; + } + mapping->mappedSize = (size_t) st.st_size; + mapping->mapped = mmap(nullptr, mapping->mappedSize, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapping->mapped == MAP_FAILED) { + FF_DEBUG("mmap(%s, %zu bytes) failed: %s", jarPath, mapping->mappedSize, strerror(errno)); + mapping->mapped = nullptr; + return "mmap(jar) failed"; } - return "The jar has no usable classes.dex entry"; + return nullptr; +} + +// Locates `classesN.dex` in the jar's central directory. The walk that finds an entry is the same +// one the app label reader needs for an APK's resource table, so it lives in common/android/zip.h +// and this is the dex-specific half of the question: which name to ask for. +static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const char* entry, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) { + FFAndroidZipEntry found = {}; + const char* error = ffAndroidZipFindEntry(jar, jarSize, entry, &found); + if (error != nullptr) { + return error; + } + *data = found.payload; + *dataSize = found.payloadSize; + *uncompressedSize = found.uncompressedSize; + *method = found.method; + return nullptr; } // Fallback for a jar whose entry is STORED but whose header has no sizes: the dex magic is then a -// literal run of bytes in the file, and the header that follows it validates or it does not. +// literal run of bytes in the file, and the header that follows it validates or it does not. It +// only ever locates the *first* dex in the jar, so it is a fallback for `classes.dex` alone. static const uint8_t* findDexMagic(const uint8_t* jar, size_t jarSize, size_t* dexSize) { for (size_t i = 0; i + FF_DEX_HEADER_SIZE <= jarSize; ++i) { if (memcmp(jar + i, FF_DEX_MAGIC, sizeof(FF_DEX_MAGIC) - 1) != 0) { @@ -353,13 +883,17 @@ static const uint8_t* findDexMagic(const uint8_t* jar, size_t jarSize, size_t* d #ifdef FF_HAVE_ZLIB static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t uncompressedSize, uint8_t** out) { - FF_LIBRARY_LOAD(zlib, "dlopen(libz) failed", "libz" FF_LIBRARY_EXTENSION, 2) - FF_LIBRARY_LOAD_SYMBOL(zlib, inflateInit2_, "dlsym(inflateInit2_) failed") - FF_LIBRARY_LOAD_SYMBOL(zlib, inflate, "dlsym(inflate) failed") - FF_LIBRARY_LOAD_SYMBOL(zlib, inflateEnd, "dlsym(inflateEnd) failed") + const char* error = ffZlibLoad(FF_ZLIB_INFLATE); + if (error != nullptr) { + FF_DEBUG("The dex cannot be inflated: %s", error); + return error; + } - uint8_t* buffer = malloc(uncompressedSize); + // Released by the cleanup on every path out below; ownership moves to the caller by clearing + // the pointer, which is also why no failure path has to free it by hand. + FF_AUTO_FREE uint8_t* buffer = malloc(uncompressedSize); if (buffer == nullptr) { + FF_DEBUG("malloc(%u) for the inflated dex failed", uncompressedSize); return "malloc failed"; } @@ -372,67 +906,81 @@ static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t unc stream.next_out = buffer; stream.avail_out = (uInt) uncompressedSize; - if (ffinflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) { - free(buffer); + const int initStatus = inflateInit2(&stream, -MAX_WBITS); + if (initStatus != Z_OK) { + FF_DEBUG("inflateInit2() failed: zlib status %d", initStatus); return "inflateInit2 failed"; } - const int status = ffinflate(&stream, Z_FINISH); - ffinflateEnd(&stream); + const int status = inflate(&stream, Z_FINISH); + inflateEnd(&stream); if (status != Z_STREAM_END || stream.total_out != (uLong) uncompressedSize) { - free(buffer); - return "Inflating classes.dex failed"; + FF_DEBUG("inflate() returned %d, produced %lu of %u bytes", + status, stream.total_out, uncompressedSize); + return "Inflating the dex failed"; } *out = buffer; + buffer = nullptr; // the caller owns it from here return nullptr; } #endif -const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result) { - [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; - - const int fd = open(jarPath, O_RDONLY | O_CLOEXEC); - if (fd < 0) { - return "open(jar) failed"; - } +// The dex bytes of one jar entry, and the validated tables of the dex they hold. +typedef struct FFDexEntry { + const uint8_t* data; + size_t size; + FFDexTables tables; +} FFDexEntry; - struct stat st = {}; - if (fstat(fd, &st) != 0 || st.st_size <= 0) { - close(fd); - return "fstat(jar) failed"; - } - mapping.mappedSize = (size_t) st.st_size; - mapping.mapped = mmap(nullptr, mapping.mappedSize, PROT_READ, MAP_PRIVATE, fd, 0); - // The mapping outlives the descriptor, so the descriptor can go either way from here. - close(fd); - if (mapping.mapped == MAP_FAILED) { - mapping.mapped = nullptr; - return "mmap(jar) failed"; +// Locates `classesN.dex`, inflates it when the entry is deflated, and validates the header. +// +// `present` reports whether the jar holds the entry at all. A jar spreads its classes over as many +// entries as it needs and then simply runs out, so an entry that is not there ends the walk rather +// than failing it -- except for the first, which every jar has and which is where the magic scan +// stands in for a directory that cannot be read. `inflated` receives the buffer the caller has to +// release when the entry needed one, and is left alone otherwise. +static const char* dexOpenEntry(const FFDexMapping* mapping, uint32_t index, bool* present, uint8_t** inflated, FFDexEntry* entry) { + // Wide enough for `classes999.dex`, which is the longest name `FF_DEX_MAX_ENTRIES` can ask for. + char name[sizeof("classes999.dex")]; + if (index == 1) { + memcpy(name, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY)); + } else { + (void) snprintf(name, sizeof(name), "classes%u.dex", index); } + *present = true; const uint8_t* data = nullptr; size_t dataSize = 0; uint32_t uncompressedSize = 0; uint16_t method = FF_ZIP_METHOD_STORED; - - if (findDexEntry(mapping.mapped, mapping.mappedSize, &data, &dataSize, &uncompressedSize, &method) != nullptr) { - data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize); + if (findDexEntry(mapping->mapped, mapping->mappedSize, name, &data, &dataSize, &uncompressedSize, &method) != nullptr) { + if (index > 1) { + // The magic scan only ever finds the first dex, so it cannot stand in for a later entry: + // running past the end of the entries that exist ends the walk. + *present = false; + return nullptr; + } + data = findDexMagic(mapping->mapped, mapping->mappedSize, &dataSize); if (data == nullptr) { + FF_DEBUG("Neither %s nor a dex magic signature is in the %zu byte jar", name, mapping->mappedSize); return "No dex in the jar"; } } else if (method == FF_ZIP_METHOD_DEFLATED) { #ifdef FF_HAVE_ZLIB - const char* error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated); + const char* error = inflateDex(data, dataSize, uncompressedSize, inflated); if (error != nullptr) { return error; } - data = mapping.inflated; + data = *inflated; dataSize = uncompressedSize; #else - return "The jar deflates classes.dex and fastfetch was built without zlib"; + (void) inflated; // the parameter is only written on the inflating path + return "The jar deflates its dex entries and fastfetch was built without zlib"; #endif } else if (method != FF_ZIP_METHOD_STORED) { - return "classes.dex uses an unsupported compression method"; + FF_DEBUG("%s uses compression method %u, only %u (stored) and %u (deflated) are handled", + name, method, FF_ZIP_METHOD_STORED, FF_ZIP_METHOD_DEFLATED); + return "A dex entry uses an unsupported compression method"; } // A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that // `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file @@ -440,7 +988,210 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con // value, so a corrupt one reaches past the end of the mapping, which is exactly what the dex // header would then be validated against. - mapping.data = data; - mapping.size = dataSize; - return dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, result); + entry->data = data; + entry->size = dataSize; + return dexOpen(entry->data, entry->size, &entry->tables); +} + +const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count) { + // Every request is written the sentinel before anything can fail, so that a caller reads a result + // rather than whatever its own stack held there, however this returns. Nothing below has to + // remember to do it on its way out. + for (uint32_t r = 0; r < count; ++r) { + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + } + if (count == 0) { + return nullptr; + } + + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; + const char* error = dexMapJar(jarPath, &mapping); + if (error != nullptr) { + return error; + } + + // One byte per request saying whether it is closed -- answered, or given up on -- the type index + // it was found under in the entry being walked, and the descriptor that index was found for. All + // three are sized from `count` rather than from a fixed bound, and all three are released by + // their cleanup however this function returns. + FF_AUTO_FREE uint8_t* settled = calloc(count, 1); + FF_AUTO_FREE uint32_t* typeIndexes = malloc((size_t) count * sizeof(uint32_t)); + FF_AUTO_FREE const char** descriptors = malloc((size_t) count * sizeof(const char*)); + if (settled == nullptr || typeIndexes == nullptr || descriptors == nullptr) { + FF_DEBUG("Allocating the tables for %u static int requests failed", count); + return "malloc failed"; + } + for (uint32_t r = 0; r < count; ++r) { + descriptors[r] = requests[r].classDescriptor; + } + + // A jar spreads its classes over `classes.dex`, `classes2.dex`, `classes3.dex`, ... and the + // class being looked for is in whichever one the build put it in: `IAudioService$Stub` is in the + // second dex of framework.jar, while `IWifiManager$Stub` is in the first of framework-wifi.jar. + // The entries are therefore tried in order, and the first one that defines the class answers. + // The count is a bound on the walk, not a claim about the jar -- AOSP has never shipped more + // than a handful, and a class that is in none of them is left unanswered either way. + // + // The walk ends as soon as the last request is closed, so an entry that holds every class asked + // for -- which is what the modules here ask for, one class per interface -- costs one entry read + // and one pass over its type table. + uint32_t pending = count; + for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) { + // The decompressed dex, when the entry needed one. The cleanup releases it at the end of + // the iteration, so every entry gets a buffer of its own and no way out of the loop -- an + // early return included -- can leak one. + FF_AUTO_FREE uint8_t* inflated = nullptr; + FFDexEntry entry = {}; + bool present = false; + error = dexOpenEntry(&mapping, index, &present, &inflated, &entry); + if (error != nullptr) { + // The dex cannot be read at all, which no later entry can fix. + return error; + } + if (!present) { + break; + } + + dexFindTypes(&entry.tables, descriptors, settled, typeIndexes, count); + + // Every class this entry defines is walked once, however many of its fields were asked for. + // A request whose class is not in this entry's type table keeps `UINT32_MAX` and is left for + // the next entry; one whose class is only referenced here is left the same way by the walk + // below, which is what `defined` reports. + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] == UINT32_MAX) { + continue; + } + const uint32_t typeIndex = typeIndexes[r]; + bool defined = false; + error = dexClassStaticInts(&entry.tables, typeIndex, requests, typeIndexes, settled, count, &defined); + if (error != nullptr) { + // The dex cannot be read, which no later entry can fix. + return error; + } + if (!defined) { + // Only referenced here, so the class is left for the next entry -- and the requests + // that share it are marked as walked, since one lookup of a class answers all of + // them and the class table should not be scanned once per field. + for (uint32_t s = r; s < count; ++s) { + if (typeIndexes[s] == typeIndex) { + typeIndexes[s] = UINT32_MAX; + } + } + } + } + + pending = 0; + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r]) { + ++pending; + } + } + } + + // What is still open is a class no entry of the jar defines. That is an answer like any other -- + // the results already hold the sentinel, written before the walk began -- so the only thing left + // is to say so for a debug build, where the sentinel on its own does not tell "this build has no + // such method" apart from "the reader went wrong". + for (uint32_t r = 0; r < count; ++r) { + if (*requests[r].result == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("\"%s\" is not a static int field of \"%s\"", requests[r].fieldName, requests[r].classDescriptor); + } + } + return nullptr; +} + +const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count) { + // Every request is written its answers before anything can fail, so that a caller reads an answer + // rather than whatever its own stack held there, however this returns. The instance fields go + // first: `false` is what both "the class does not declare it" and "the jar cannot be read" mean, + // and the walk below only ever turns one of them true. + for (uint32_t r = 0; r < count; ++r) { + if (requests[r].methodName != nullptr) { + *requests[r].count = 0; + } + for (uint32_t i = 0; i < requests[r].instanceFieldCount; ++i) { + requests[r].instanceFieldResults[i] = false; + } + } + if (count == 0) { + return nullptr; + } + + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; + const char* error = dexMapJar(jarPath, &mapping); + if (error != nullptr) { + return error; + } + + FF_AUTO_FREE uint8_t* settled = calloc(count, 1); + FF_AUTO_FREE uint32_t* typeIndexes = malloc((size_t) count * sizeof(uint32_t)); + FF_AUTO_FREE const char** descriptors = malloc((size_t) count * sizeof(const char*)); + if (settled == nullptr || typeIndexes == nullptr || descriptors == nullptr) { + FF_DEBUG("Allocating the tables for %u parcel field requests failed", count); + return "malloc failed"; + } + for (uint32_t r = 0; r < count; ++r) { + descriptors[r] = requests[r].classDescriptor; + } + + uint32_t pending = count; + for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) { + FF_AUTO_FREE uint8_t* inflated = nullptr; + FFDexEntry entry = {}; + bool present = false; + error = dexOpenEntry(&mapping, index, &present, &inflated, &entry); + if (error != nullptr) { + // The dex cannot be read at all, which no later entry can fix. + return error; + } + if (!present) { + break; + } + + dexFindTypes(&entry.tables, descriptors, settled, typeIndexes, count); + + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] == UINT32_MAX) { + continue; + } + const uint32_t typeIndex = typeIndexes[r]; + bool defined = false; + error = dexClassParcelFields(&entry.tables, typeIndex, requests, typeIndexes, settled, count, &defined); + if (error != nullptr) { + return error; + } + if (!defined) { + for (uint32_t s = r; s < count; ++s) { + if (typeIndexes[s] == typeIndex) { + typeIndexes[s] = UINT32_MAX; + } + } + } + } + + pending = 0; + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r]) { + ++pending; + } + } + } + + // A request still holding 0 is one whose class no entry of the jar defines, or whose class does + // not declare the method. Both are the same answer -- this build does not write that class -- and + // for a debug build it is worth saying which of the two it was, since the count on its own does + // not tell "this build has no such method" apart from "the reader went wrong". The same goes for + // an instance field left `false`. + for (uint32_t r = 0; r < count; ++r) { + if (requests[r].methodName != nullptr && *requests[r].count == 0) { + FF_DEBUG("The write sequence of \"%s.%s\" is not in the jar", requests[r].classDescriptor, requests[r].methodName); + } + for (uint32_t i = 0; i < requests[r].instanceFieldCount; ++i) { + if (!requests[r].instanceFieldResults[i]) { + FF_DEBUG("\"%s\" is not an instance field of \"%s\"", requests[r].instanceFieldNames[i], requests[r].classDescriptor); + } + } + } + return nullptr; } diff --git a/src/common/android/dex.h b/src/common/android/dex.h index 34197506a5..9f0b93ed2b 100644 --- a/src/common/android/dex.h +++ b/src/common/android/dex.h @@ -24,7 +24,144 @@ #include "fastfetch.h" -// Resolves the int value of `.` 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 `.` 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); diff --git a/src/common/android/package.h b/src/common/android/package.h index e5f3241aa6..f4e4cad6e1 100644 --- a/src/common/android/package.h +++ b/src/common/android/package.h @@ -1,7 +1,6 @@ #pragma once -#include -#include +#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 diff --git a/src/common/android/zip.c b/src/common/android/zip.c new file mode 100644 index 0000000000..5510cb763d --- /dev/null +++ b/src/common/android/zip.c @@ -0,0 +1,100 @@ +#include "common/android/zip.h" +#include "common/debug.h" + +#include + +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"; +} diff --git a/src/common/android/zip.h b/src/common/android/zip.h new file mode 100644 index 0000000000..398991e59c --- /dev/null +++ b/src/common/android/zip.h @@ -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); diff --git a/src/common/apple/Info.plist.in b/src/common/apple/Info.plist.in index 52200538b6..ab69b226dc 100644 --- a/src/common/apple/Info.plist.in +++ b/src/common/apple/Info.plist.in @@ -3,7 +3,7 @@ CFBundleIdentifier - fastfetch + @BUNDLE_ID@ CFBundleName @PROJECT_NAME@ CFBundleShortVersionString diff --git a/src/common/apple/cf_helpers.c b/src/common/apple/cf_helpers.c index 87ea2bbb19..033c0bedbc 100644 --- a/src/common/apple/cf_helpers.c +++ b/src/common/apple/cf_helpers.c @@ -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"; } diff --git a/src/common/apple/smc_temps.c b/src/common/apple/smc_temps.c index 9fb18210f4..48c99d5612 100644 --- a/src/common/apple/smc_temps.c +++ b/src/common/apple/smc_temps.c @@ -1,5 +1,6 @@ #include "smc_temps.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -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"; } diff --git a/src/common/debug.h b/src/common/debug.h index ea7c27f4ca..cb978cbc4a 100644 --- a/src/common/debug.h +++ b/src/common/debug.h @@ -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 diff --git a/src/common/impl/FFPlatform_unix.c b/src/common/impl/FFPlatform_unix.c index 94a73f5dea..8ec80ab0be 100644 --- a/src/common/impl/FFPlatform_unix.c +++ b/src/common/impl/FFPlatform_unix.c @@ -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) { diff --git a/src/common/impl/FFPlatform_windows.c b/src/common/impl/FFPlatform_windows.c index 5f937ca2a7..7568664c93 100644 --- a/src/common/impl/FFPlatform_windows.c +++ b/src/common/impl/FFPlatform_windows.c @@ -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 #include -#include #include #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); diff --git a/src/common/impl/binary_apple.c b/src/common/impl/binary_apple.c index c25df7a922..dd35a5ae77 100644 --- a/src/common/impl/binary_apple.c +++ b/src/common/impl/binary_apple.c @@ -1,7 +1,9 @@ #include "common/binary.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" +#include #include #include #include @@ -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"; } diff --git a/src/common/impl/binary_linux.c b/src/common/impl/binary_linux.c index 2d8d50b199..c9316ecc98 100644 --- a/src/common/impl/binary_linux.c +++ b/src/common/impl/binary_linux.c @@ -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"; } diff --git a/src/common/impl/binary_windows.c b/src/common/impl/binary_windows.c index 005389ff05..dfad1d94c6 100644 --- a/src/common/impl/binary_windows.c +++ b/src/common/impl/binary_windows.c @@ -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"; } diff --git a/src/common/impl/dbus.c b/src/common/impl/dbus.c index ca20efb180..15041e079b 100644 --- a/src/common/impl/dbus.c +++ b/src/common/impl/dbus.c @@ -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"; } diff --git a/src/common/impl/debug.c b/src/common/impl/debug.c new file mode 100644 index 0000000000..b72c3dea34 --- /dev/null +++ b/src/common/impl/debug.c @@ -0,0 +1,108 @@ +#include + +#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 + #include + +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 diff --git a/src/common/impl/debug_windows.c b/src/common/impl/debug_windows.c deleted file mode 100644 index 6247ec4376..0000000000 --- a/src/common/impl/debug_windows.c +++ /dev/null @@ -1,60 +0,0 @@ -#include "common/debug.h" -#include "common/windows/nt.h" - -#include -#include - -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)); -} diff --git a/src/common/impl/format.c b/src/common/impl/format.c index 450870c390..b3b8ab190d 100644 --- a/src/common/impl/format.c +++ b/src/common/impl/format.c @@ -280,183 +280,6 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script } #endif -#if FF_HAVE_QUICKJS - #include - -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 diff --git a/src/common/impl/init.c b/src/common/impl/init.c index 16932e1e23..d6758bae00 100644 --- a/src/common/impl/init.c +++ b/src/common/impl/init.c @@ -178,9 +178,6 @@ void ffDestroyInstance(void) { #if FF_HAVE_LUA #include #endif -#if FF_HAVE_QUICKJS - #include -#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); diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c index 56f9922ef9..2bd8abac26 100644 --- a/src/common/impl/io_unix.c +++ b/src/common/impl/io_unix.c @@ -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 -#else +#elif FF_HAVE_GLOB #include #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"; } diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index d8fb829601..91f58abceb 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -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) { diff --git a/src/common/impl/jsonconfig.c b/src/common/impl/jsonconfig.c index 232d155e52..c0b14e260e 100644 --- a/src/common/impl/jsonconfig.c +++ b/src/common/impl/jsonconfig.c @@ -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); diff --git a/src/common/impl/lua.c b/src/common/impl/lua.c index 871bf43b0c..6b59ec5fc3 100644 --- a/src/common/impl/lua.c +++ b/src/common/impl/lua.c @@ -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 diff --git a/src/common/impl/networking_common.c b/src/common/impl/networking_common.c index 732a011c90..9c6372183f 100644 --- a/src/common/impl/networking_common.c +++ b/src/common/impl/networking_common.c @@ -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 - -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; diff --git a/src/common/impl/networking_linux.c b/src/common/impl/networking_linux.c index f1ee80b334..ebb13dfba1 100644 --- a/src/common/impl/networking_linux.c +++ b/src/common/impl/networking_linux.c @@ -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 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos 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; } diff --git a/src/common/impl/networking_windows.c b/src/common/impl/networking_windows.c index 1b6388c22a..7c6995bd3e 100644 --- a/src/common/impl/networking_windows.c +++ b/src/common/impl/networking_windows.c @@ -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; } diff --git a/src/common/impl/path.c b/src/common/impl/path.c index 32e21ca8fb..1586102078 100644 --- a/src/common/impl/path.c +++ b/src/common/impl/path.c @@ -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"; } diff --git a/src/common/impl/percent.c b/src/common/impl/percent.c index ef276484f5..2858698ee0 100644 --- a/src/common/impl/percent.c +++ b/src/common/impl/percent.c @@ -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); diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c index 604094df38..1b599fa5eb 100644 --- a/src/common/impl/processing_linux.c +++ b/src/common/impl/processing_linux.c @@ -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 diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c index 5ddcdc2367..ed4fcf07b5 100644 --- a/src/common/impl/processing_windows.c +++ b/src/common/impl/processing_windows.c @@ -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"; } } diff --git a/src/common/impl/sysctl.c b/src/common/impl/sysctl.c index 29d4a3afca..1c11363724 100644 --- a/src/common/impl/sysctl.c +++ b/src/common/impl/sysctl.c @@ -1,17 +1,22 @@ #include "common/sysctl.h" +#include "common/debug.h" +#include #include +#include #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"; } diff --git a/src/common/impl/temps.c b/src/common/impl/temps.c index 652f3fbb25..db21441426 100644 --- a/src/common/impl/temps.c +++ b/src/common/impl/temps.c @@ -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); diff --git a/src/common/impl/zlib.c b/src/common/impl/zlib.c new file mode 100644 index 0000000000..77ac015b8c --- /dev/null +++ b/src/common/impl/zlib.c @@ -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 diff --git a/src/common/lua.h b/src/common/lua.h index fab613f128..b012dfc9e2 100644 --- a/src/common/lua.h +++ b/src/common/lua.h @@ -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 diff --git a/src/common/networking.h b/src/common/networking.h index 645787bd0e..a83fabe2d8 100644 --- a/src/common/networking.h +++ b/src/common/networking.h @@ -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 diff --git a/src/common/option.h b/src/common/option.h index ba3940a942..3a97358e85 100644 --- a/src/common/option.h +++ b/src/common/option.h @@ -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; diff --git a/src/common/processing.h b/src/common/processing.h index b52070fdfe..67e1c64dd6 100644 --- a/src/common/processing.h +++ b/src/common/processing.h @@ -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 : 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; } diff --git a/src/common/smbios.h b/src/common/smbios.h index 7aa0d50482..246cbe0d98 100644 --- a/src/common/smbios.h +++ b/src/common/smbios.h @@ -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); diff --git a/src/common/windows/c-logo.sh b/src/common/windows/c-logo.sh index e4d7863cc8..cea8824c7e 100755 --- a/src/common/windows/c-logo.sh +++ b/src/common/windows/c-logo.sh @@ -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(" @@ -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"; } } diff --git a/src/common/windows/folders.c b/src/common/windows/folders.c new file mode 100644 index 0000000000..b42afed958 --- /dev/null +++ b/src/common/windows/folders.c @@ -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 + +// 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\\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; +} diff --git a/src/common/windows/folders.h b/src/common/windows/folders.h new file mode 100644 index 0000000000..683041e41e --- /dev/null +++ b/src/common/windows/folders.h @@ -0,0 +1,47 @@ +#pragma once + +#include "fastfetch.h" +#include "common/io.h" + +#include // 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 -- the call sites include it for reasons of +// their own, and a header that only works when 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); diff --git a/src/common/windows/logo.ico b/src/common/windows/logo.ico index cdd89adbc9..59fd20428c 100644 Binary files a/src/common/windows/logo.ico and b/src/common/windows/logo.ico differ diff --git a/src/common/windows/logo.svg b/src/common/windows/logo.svg deleted file mode 120000 index 068d3f663c..0000000000 --- a/src/common/windows/logo.svg +++ /dev/null @@ -1 +0,0 @@ -../../logo/logo.svg \ No newline at end of file diff --git a/src/common/windows/nt.h b/src/common/windows/nt.h index 26b214dc4b..6b9e4f2347 100644 --- a/src/common/windows/nt.h +++ b/src/common/windows/nt.h @@ -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 +}; diff --git a/src/common/zlib.h b/src/common/zlib.h new file mode 100644 index 0000000000..02a2f79a6b --- /dev/null +++ b/src/common/zlib.h @@ -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 + +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. 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 + // ; 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 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 + + #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 diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c index 369e1372a1..1da91bfcf0 100644 --- a/src/detection/battery/battery_android.c +++ b/src/detection/battery/battery_android.c @@ -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 +#include +#include #include +#include // 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. - -// 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. +// `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 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,226 +276,355 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope return nullptr; } -static const char* parseBinder(FFlist* results) { - [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); - if (error != nullptr) { - FF_DEBUG("Opening /dev/binder failed: %s", error); - return error; +// 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; } - // Released on every path out of this function, including the ones below that return early. - [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder }; - error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle); - if (error != nullptr) { - FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error); - return error; + // `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; } - FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, service.handle); - uint64_t capacity = 0; - error = getProperty(&binder, service.handle, 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; - } + *value = (uint32_t) parsed; + return true; +} - 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; +// 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; } - FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); - initResult(battery); - battery->capacity = (double) capacity; - if (status == FF_BATTERY_ANDROID_STATUS_CHARGING) { - battery->status |= FF_BATTERY_STATUS_CHARGING; - } else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) { - battery->status |= FF_BATTERY_STATUS_DISCHARGING; + // 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; } - // `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. - 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); - return nullptr; + 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); } -// `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. +// 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. // -// `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"; +// 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 (!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"; + 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; } - const char* start = buf.chars + strlen("Current Battery Service state:\n"); + *status = value; + return true; +} - FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate(); - if (!ffParsePropLines(start, "present: ", &temp) || !ffStrbufEqualS(&temp, "true")) { - return nullptr; +// 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"; } - ffStrbufClear(&temp); - FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); - initResult(battery); + uint8_t parcelBuffer[128]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_STATS_DESCRIPTOR); - if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { - battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + 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"; } - ffStrbufClear(&temp); - if (ffParsePropLines(start, "USB powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { - battery->status |= FF_BATTERY_STATUS_USB_CONNECTED; + // 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"; } - ffStrbufClear(&temp); - if (ffParsePropLines(start, "Wireless powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { - battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED; + *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; } - 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; - } + + 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; } - ffStrbufClear(&temp); + 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); +} - // 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; +// 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; } - ffStrbufClear(&temp); - { - double level = 0, scale = 0; - if (ffParsePropLines(start, "level: ", &temp)) { - level = ffStrbufToDouble(&temp, -DBL_MAX); + 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; } - ffStrbufClear(&temp); - if (ffParsePropLines(start, "scale: ", &temp)) { - scale = ffStrbufToDouble(&temp, -DBL_MAX); + FF_AUTO_CLOSE_FD int zone = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (zone < 0) { + continue; } - ffStrbufClear(&temp); - if (level > 0 && scale > 0) { - battery->capacity = level * 100 / scale; + // 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 (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 - } + if (!ffReadFileBufferRelative(zone, "temp", &buffer)) { + FF_DEBUG("The battery zone \"%s\" has no readable temperature", entry->d_name); + return FF_BATTERY_TEMP_UNSET; } - ffStrbufClear(&temp); + + // 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.; } - ffParsePropLines(start, "technology: ", &battery->technology); + 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; + } - return nullptr; + 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) { - // `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; + // 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 }; + error = ffBinderOpen(&binder); + if (error != nullptr) { + FF_DEBUG("Opening /dev/binder failed: %s", error); + return error; + } + + // Released on every path out of this function, including the ones below that return early. + [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder }; + error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle); + if (error != nullptr) { + FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error); + return error; + } + 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, (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; + 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; } - FF_DEBUG("`/system/bin/dumpsys battery` failed: %s. Using the binder route instead", error); } - return parseBinder(results); + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + initResult(battery); + battery->capacity = (double) capacity; + if (status == FF_BATTERY_ANDROID_STATUS_CHARGING) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } 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 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), %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; } diff --git a/src/detection/battery/battery_apple.c b/src/detection/battery/battery_apple.c index fb60303919..c231790df9 100644 --- a/src/detection/battery/battery_apple.c +++ b/src/detection/battery/battery_apple.c @@ -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"; } diff --git a/src/detection/battery/battery_bsd.c b/src/detection/battery/battery_bsd.c index cff41d5a68..517899df24 100644 --- a/src/detection/battery/battery_bsd.c +++ b/src/detection/battery/battery_bsd.c @@ -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 #include #include +#include +#include #include 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"; } diff --git a/src/detection/battery/battery_haiku.c b/src/detection/battery/battery_haiku.c index 523d33d933..12d84b659c 100644 --- a/src/detection/battery/battery_haiku.c +++ b/src/detection/battery/battery_haiku.c @@ -1,23 +1,28 @@ #include "fastfetch.h" #include "battery.h" +#include "common/debug.h" #include "common/io.h" #include #include #include +#include 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"; } diff --git a/src/detection/battery/battery_linux.c b/src/detection/battery/battery_linux.c index 0362cbcd19..4c0c6093d6 100644 --- a/src/detection/battery/battery_linux.c +++ b/src/detection/battery/battery_linux.c @@ -6,6 +6,7 @@ #include #include #include +#include // 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"; } diff --git a/src/detection/battery/battery_nbsd.c b/src/detection/battery/battery_nbsd.c index eaf1fc7155..0d88ec118e 100644 --- a/src/detection/battery/battery_nbsd.c +++ b/src/detection/battery/battery_nbsd.c @@ -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"; } diff --git a/src/detection/battery/battery_obsd.c b/src/detection/battery/battery_obsd.c index c5c6c3c391..f3cd9d69c4 100644 --- a/src/detection/battery/battery_obsd.c +++ b/src/detection/battery/battery_obsd.c @@ -1,6 +1,8 @@ #include "battery.h" +#include "common/debug.h" #include "common/io.h" +#include #include #include #include @@ -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"; } diff --git a/src/detection/battery/battery_sunos.c b/src/detection/battery/battery_sunos.c index e31b7bb9f4..8f8a42440d 100644 --- a/src/detection/battery/battery_sunos.c +++ b/src/detection/battery/battery_sunos.c @@ -1,4 +1,5 @@ #include "battery.h" +#include "common/debug.h" #include @@ -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"; } diff --git a/src/detection/battery/battery_windows.c b/src/detection/battery/battery_windows.c index cbede5d1ef..2c61c871db 100644 --- a/src/detection/battery/battery_windows.c +++ b/src/detection/battery/battery_windows.c @@ -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"; } diff --git a/src/detection/bios/bios_apple.c b/src/detection/bios/bios_apple.c index c9a6759b68..c044cddbfa 100644 --- a/src/detection/bios/bios_apple.c +++ b/src/detection/bios/bios_apple.c @@ -1,5 +1,6 @@ #include "bios.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include @@ -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"; } diff --git a/src/detection/bios/bios_windows.c b/src/detection/bios/bios_windows.c index b1d309f5e0..c367dafd4e 100644 --- a/src/detection/bios/bios_windows.c +++ b/src/detection/bios/bios_windows.c @@ -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"; } diff --git a/src/detection/bluetooth/bluetooth_bsd.c b/src/detection/bluetooth/bluetooth_bsd.c index 9b7b033434..911d333ad3 100644 --- a/src/detection/bluetooth/bluetooth_bsd.c +++ b/src/detection/bluetooth/bluetooth_bsd.c @@ -1,4 +1,5 @@ #include "bluetooth.h" +#include "common/debug.h" #define L2CAP_SOCKET_CHECKED #include @@ -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"; } diff --git a/src/detection/bluetooth/bluetooth_linux.c b/src/detection/bluetooth/bluetooth_linux.c index 78187822a5..65528a4bdb 100644 --- a/src/detection/bluetooth/bluetooth_linux.c +++ b/src/detection/bluetooth/bluetooth_linux.c @@ -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 } diff --git a/src/detection/bluetooth/bluetooth_windows.c b/src/detection/bluetooth/bluetooth_windows.c index f58a12501a..18022abbea 100644 --- a/src/detection/bluetooth/bluetooth_windows.c +++ b/src/detection/bluetooth/bluetooth_windows.c @@ -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 diff --git a/src/detection/bluetooth/bluetooth_windows.cpp b/src/detection/bluetooth/bluetooth_windows.cpp index 58863b56ce..5c82ed7f58 100644 --- a/src/detection/bluetooth/bluetooth_windows.cpp +++ b/src/detection/bluetooth/bluetooth_windows.cpp @@ -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* ledeviceStatics = nullptr; - if (FAILED(ffGetActivationFactory(L"Windows.Devices.Bluetooth.BluetoothLEDevice", &ledeviceStatics)) || !ledeviceStatics) { + HRESULT ledeviceHr = ffGetActivationFactory(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(&selector))) || !selector) { + HRESULT selectorHr = ledeviceStatics->GetDeviceSelectorFromPairingState(true, reinterpret_cast(&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>* 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* enumerationStatics = nullptr; - if (FAILED(ffGetActivationFactory(L"Windows.Devices.Enumeration.DeviceInformation", &enumerationStatics)) || !enumerationStatics) { + HRESULT deviceInfoHr = ffGetActivationFactory(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"; } diff --git a/src/detection/bluetoothradio/bluetoothradio_linux.c b/src/detection/bluetoothradio/bluetoothradio_linux.c index c3a33a9cc0..656a2ceae1 100644 --- a/src/detection/bluetoothradio/bluetoothradio_linux.c +++ b/src/detection/bluetoothradio/bluetoothradio_linux.c @@ -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 } diff --git a/src/detection/bluetoothradio/bluetoothradio_windows.c b/src/detection/bluetoothradio/bluetoothradio_windows.c index 11f7879d5f..bb9c18128e 100644 --- a/src/detection/bluetoothradio/bluetoothradio_windows.c +++ b/src/detection/bluetoothradio/bluetoothradio_windows.c @@ -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); diff --git a/src/detection/board/board_apple.c b/src/detection/board/board_apple.c index f3b39538d9..18396acb8a 100644 --- a/src/detection/board/board_apple.c +++ b/src/detection/board/board_apple.c @@ -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"; } diff --git a/src/detection/board/board_windows.c b/src/detection/board/board_windows.c index 7ce50032dd..eb0c7d362d 100644 --- a/src/detection/board/board_windows.c +++ b/src/detection/board/board_windows.c @@ -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"; } diff --git a/src/detection/bootmgr/bootmgr_apple.c b/src/detection/bootmgr/bootmgr_apple.c index 59aaa78316..beb6019c67 100644 --- a/src/detection/bootmgr/bootmgr_apple.c +++ b/src/detection/bootmgr/bootmgr_apple.c @@ -1,5 +1,6 @@ #include "bootmgr.h" #include "common/io.h" +#include "common/debug.h" #include "common/apple/cf_helpers.h" #include @@ -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"; } diff --git a/src/detection/bootmgr/bootmgr_bsd.c b/src/detection/bootmgr/bootmgr_bsd.c index 93983ab377..cdefdab562 100644 --- a/src/detection/bootmgr/bootmgr_bsd.c +++ b/src/detection/bootmgr/bootmgr_bsd.c @@ -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"; } diff --git a/src/detection/bootmgr/bootmgr_linux.c b/src/detection/bootmgr/bootmgr_linux.c index daae9e79ec..226c53d28d 100644 --- a/src/detection/bootmgr/bootmgr_linux.c +++ b/src/detection/bootmgr/bootmgr_linux.c @@ -1,15 +1,18 @@ #include "bootmgr.h" +#include "common/debug.h" #include "common/io.h" #include "efi_helper.h" #include #define FF_EFIVARS_PATH_PREFIX "/sys/firmware/efi/efivars/" +#define FF_EFI_GLOBAL_GUID "8be4df61-93ca-11d2-aa0d-00e098032b8c" const char* ffDetectBootmgr(FFBootmgrResult* result) { alignas(uint16_t) uint8_t buffer[2048]; if (ffReadFileData(FF_EFIVARS_PATH_PREFIX "BootCurrent-" FF_EFI_GLOBAL_GUID, sizeof(buffer), buffer) != 6) { + FF_DEBUG("Failed to read efivar: BootCurrent"); return "Failed to read efivar: BootCurrent"; } @@ -19,6 +22,7 @@ const char* ffDetectBootmgr(FFBootmgrResult* result) { ssize_t size = ffReadFileData((const char*) buffer, sizeof(buffer), buffer); if (size < 5 + (int) sizeof(FFEfiLoadOption) || size == (ssize_t) sizeof(buffer)) { + FF_DEBUG("Failed to read efivar: Boot####"); return "Failed to read efivar: Boot####"; } diff --git a/src/detection/bootmgr/bootmgr_windows.c b/src/detection/bootmgr/bootmgr_windows.c index 0a1a55994b..c98d1ef78f 100644 --- a/src/detection/bootmgr/bootmgr_windows.c +++ b/src/detection/bootmgr/bootmgr_windows.c @@ -1,4 +1,5 @@ #include "bootmgr.h" +#include "common/debug.h" #include "efi_helper.h" #include "common/io.h" #include "common/windows/nt.h" @@ -8,7 +9,9 @@ const char* enablePrivilege(const wchar_t* privilege) { FF_AUTO_CLOSE_FD HANDLE token = nullptr; - if (!NT_SUCCESS(NtOpenProcessToken(NtCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token))) { + NTSTATUS status = NtOpenProcessToken(NtCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtOpenProcessToken() failed: %s", ffDebugNtStatus(status)); return "NtOpenProcessToken() failed"; } @@ -18,15 +21,18 @@ const char* enablePrivilege(const wchar_t* privilege) { (LUID_AND_ATTRIBUTES) { .Attributes = SE_PRIVILEGE_ENABLED } }, }; if (!LookupPrivilegeValueW(nullptr, privilege, &tp.Privileges[0].Luid)) { + FF_DEBUG("LookupPrivilegeValue() failed: %s", ffDebugWin32Error(GetLastError())); return "LookupPrivilegeValue() failed"; } - NTSTATUS status = NtAdjustPrivilegesToken(token, false, &tp, sizeof(tp), nullptr, nullptr); + status = NtAdjustPrivilegesToken(token, false, &tp, sizeof(tp), nullptr, nullptr); if (!NT_SUCCESS(status)) { + FF_DEBUG("NtAdjustPrivilegesToken() failed: %s", ffDebugNtStatus(status)); return "NtAdjustPrivilegesToken() failed"; } if (status == STATUS_NOT_ALL_ASSIGNED) { + FF_DEBUG("NtAdjustPrivilegesToken() did not assign the privilege: %s", ffDebugNtStatus(status)); return "The token does not have the specified privilege; try sudo please"; } @@ -39,16 +45,21 @@ const char* ffDetectBootmgr(FFBootmgrResult* result) { return err; } - GUID efiGlobalGuid; - if (!NT_SUCCESS(RtlGUIDFromString(&(UNICODE_STRING) RTL_CONSTANT_STRING(L"{" FF_EFI_GLOBAL_GUID L"}"), &efiGlobalGuid))) { - return "RtlGUIDFromString() failed"; - } + GUID efiGlobalGuid = { + .Data1 = 0x8be4df61, + .Data2 = 0x93ca, + .Data3 = 0x11d2, + .Data4 = { 0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c }, + }; ULONG size = sizeof(result->order); - if (!NT_SUCCESS(NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(L"BootCurrent"), &efiGlobalGuid, &result->order, &size, nullptr))) { + NTSTATUS status = NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(L"BootCurrent"), &efiGlobalGuid, &result->order, &size, nullptr); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtQuerySystemEnvironmentValueEx(BootCurrent) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemEnvironmentValueEx(BootCurrent) failed"; } if (size != sizeof(result->order)) { + FF_DEBUG("NtQuerySystemEnvironmentValueEx(BootCurrent) returned unexpected size"); return "NtQuerySystemEnvironmentValueEx(BootCurrent) returned unexpected size"; } @@ -56,10 +67,13 @@ const char* ffDetectBootmgr(FFBootmgrResult* result) { wchar_t key[9]; swprintf(key, ARRAY_SIZE(key), L"Boot%04X", result->order); size = sizeof(buffer); - if (!NT_SUCCESS(NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(key), &efiGlobalGuid, buffer, &size, nullptr))) { + status = NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(key), &efiGlobalGuid, buffer, &size, nullptr); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtQuerySystemEnvironmentValueEx(Boot####) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemEnvironmentValueEx(Boot####) failed"; } if (size < sizeof(FFEfiLoadOption) || size == ARRAY_SIZE(buffer)) { + FF_DEBUG("NtQuerySystemEnvironmentValueEx(Boot####) returned unexpected size"); return "NtQuerySystemEnvironmentValueEx(Boot####) returned unexpected size"; } diff --git a/src/detection/bootmgr/efi_helper.h b/src/detection/bootmgr/efi_helper.h index b52ec8a81d..cd8ae27a6c 100644 --- a/src/detection/bootmgr/efi_helper.h +++ b/src/detection/bootmgr/efi_helper.h @@ -18,5 +18,3 @@ typedef struct FFEfiLoadOption { } FFEfiLoadOption; bool ffEfiFillLoadOption(const FFEfiLoadOption* efiOption, FFBootmgrResult* result); - -#define FF_EFI_GLOBAL_GUID "8be4df61-93ca-11d2-aa0d-00e098032b8c" diff --git a/src/detection/brightness/brightness.h b/src/detection/brightness/brightness.h index abb94787e8..fe7f082ca0 100644 --- a/src/detection/brightness/brightness.h +++ b/src/detection/brightness/brightness.h @@ -11,6 +11,7 @@ typedef struct FFBrightnessResult { enum { FF_DDC_EDID_ADDR = 0x50u, + FF_DDC_EDID_WRITE_ADDR = FF_DDC_EDID_ADDR << 1, FF_DDC_CI_ADDR = 0x37u, FF_DDC_CI_WRITE_ADDR = FF_DDC_CI_ADDR << 1, FF_DDC_CI_READ_ADDR = FF_DDC_CI_WRITE_ADDR | 1, @@ -19,8 +20,55 @@ enum { FF_DDC_CI_SET_VCP = 0x03u, FF_DDC_CI_COMMAND_PACKET = 0x80u, FF_DDC_CI_LUMINANCE_OPCODE = 0x10u, + FF_DDC_CI_VCP_RESPONSE_LENGTH = 8u, + FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE = 11u, + FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE = 12u, + FF_DDC_CI_RESPONSE_CHECKSUM_ADDR = 0x50u, }; #define FF_BRIGHTNESS_DDCCI_SLEEP_SKIP ((uint32_t) -1) #define FF_DDC_CI_MAKE_HEADER(len) (FF_DDC_CI_COMMAND_PACKET | ((len) & 0x7F)) +static inline uint8_t ffDdcCiChecksum(uint8_t address, const uint8_t* data, size_t length) { + uint8_t checksum = address; + for (size_t i = 0; i < length; ++i) { + checksum ^= data[i]; + } + return checksum; +} + +static inline void ffDdcCiBuildGetVcpRequest(uint8_t request[5]) { + request[0] = FF_DDC_CI_VCP_COMMAND; + request[1] = FF_DDC_CI_MAKE_HEADER(2); + request[2] = FF_DDC_CI_GET_VCP; + request[3] = FF_DDC_CI_LUMINANCE_OPCODE; + request[4] = ffDdcCiChecksum(FF_DDC_CI_WRITE_ADDR, request, 4); +} + +static inline bool ffDdcCiValidateGetVcpResponse(const uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + if (response[0] != FF_DDC_CI_WRITE_ADDR + || response[1] != FF_DDC_CI_MAKE_HEADER(FF_DDC_CI_VCP_RESPONSE_LENGTH) + || response[2] != 0x02u + || response[3] != 0x00u + || response[4] != FF_DDC_CI_LUMINANCE_OPCODE + || response[5] != 0x00u) { + return false; + } + + return ffDdcCiChecksum(FF_DDC_CI_RESPONSE_CHECKSUM_ADDR, response, FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE - 1u) == response[FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE - 1u]; +} + +#define FF_I2C_DEVICE_CACHE_MAX 32 + +typedef struct FFBrightnessDdcCache { + int devices[FF_I2C_DEVICE_CACHE_MAX]; + size_t count; +} FFBrightnessDdcCache; + +static inline FFstrbuf ffBrightnessDdcCachePath(void) { + FFstrbuf path = ffStrbufCreateCopy(&instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(&path, '/'); + ffStrbufAppendS(&path, "fastfetch/brightness/i2c_cache.txt"); + return path; +} + const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result); // list of FFBrightnessResult diff --git a/src/detection/brightness/brightness_apple.c b/src/detection/brightness/brightness_apple.c index 14f490067d..f1f27d0b75 100644 --- a/src/detection/brightness/brightness_apple.c +++ b/src/detection/brightness/brightness_apple.c @@ -1,6 +1,7 @@ #include "brightness.h" #include "detection/displayserver/displayserver.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include "common/edidHelper.h" #include "common/time.h" @@ -28,6 +29,7 @@ typedef CFTypeRef IOAVServiceRef; // Works for internal display static const char* detectWithDisplayServices(const FFDisplayServerResult* displayServer, FFlist* result) { if (DisplayServicesGetBrightness == nullptr) { + FF_DEBUG("DisplayServices function DisplayServicesGetBrightness is not available"); return "DisplayServices function DisplayServicesGetBrightness is not available"; } @@ -53,11 +55,13 @@ static const char* detectWithDisplayServices(const FFDisplayServerResult* displa // Works for Apple Silicon and USB-C adapter connection ( but not HTMI ) static const char* detectWithDdcci([[maybe_unused]] const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) { if (!IOAVServiceCreate || !IOAVServiceReadI2C || !IOAVServiceWriteI2C) { + FF_DEBUG("IOAVService is not available"); return "IOAVService is not available"; } FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("DCPAVServiceProxy"), &iterator) != kIOReturnSuccess) { + FF_DEBUG("IOServiceGetMatchingServices() failed"); return "IOServiceGetMatchingServices() failed"; } @@ -154,10 +158,12 @@ static IOOptionBits getSupportedTransactionType(void) { static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) { if (!CGSServiceForDisplayNumber) { + FF_DEBUG("CGSServiceForDisplayNumber is not available"); return "CGSServiceForDisplayNumber is not available"; } IOOptionBits transactionType = getSupportedTransactionType(); if (transactionType == kIOI2CNoTransactionType) { + FF_DEBUG("No supported IOI2C transaction type found"); return "No supported IOI2C transaction type found"; } diff --git a/src/detection/brightness/brightness_bsd.c b/src/detection/brightness/brightness_bsd.c index bf09098b25..d2a25fd045 100644 --- a/src/detection/brightness/brightness_bsd.c +++ b/src/detection/brightness/brightness_bsd.c @@ -4,7 +4,11 @@ #include "common/kmod.h" #include "common/debug.h" #include "common/time.h" +#include "common/edidHelper.h" +#include +#include +#include #include #include #include @@ -12,65 +16,261 @@ #if __has_include() #include -const char* detectWithDdcci([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { - // FIXME: doesn't work for me - for (char i = '0'; i <= '9'; ++i) { - char path[] = "/dev/iic0"; - path[ARRAY_SIZE(path) - 2] = i; + // Try to load cache from file + static bool loadCacheFromFile(FFBrightnessDdcCache* cache) { + cache->count = 0; + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + bool valid = false; + FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars); + if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) { + // Parse cache: first number is count, rest are device numbers + char* ptr = cacheContent.chars; + char* end = ptr + cacheContent.length; - FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); - if (fd < 0) { - int err = errno; - if (err == ENOENT) { - FF_DEBUG("No more I2C devices"); - break; - } else if (i == '0' && !ffKmodLoaded("iicbus")) { - FF_DEBUG("iicbus module is not loaded"); - return "No I2C devices found and iicbus module is not loaded"; + // Read count + char* next; + long count = strtol(ptr, &next, 10); + if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) { + ptr = next; + + // Read device numbers and reject truncated or trailing data. + size_t parsedCount = 0; + for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) { + long dev = strtol(ptr, &next, 10); + if (next == ptr || dev < 0 || dev >= 100) { + break; + } + cache->devices[parsedCount] = (int) dev; + ptr = next; + } + + while (ptr < end && isspace((unsigned char) *ptr)) { + ++ptr; + } + + if (parsedCount == (size_t) count && ptr == end) { + cache->count = parsedCount; + valid = true; + for (size_t i = 0; i < cache->count; ++i) { + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", cache->devices[i]); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], error, strerror(error)); + valid = false; + break; + } + } + } } - FF_DEBUG("open(/dev/iic%c) failed: %s", i, strerror(errno)); - continue; } - uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 }; - i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3]; + if (!valid) { + FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid"); + cache->count = 0; + return false; + } - int ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_WRITE_ADDR, .flags = IIC_M_WR, .len = ARRAY_SIZE(i2cIn), .buf = i2cIn }, .nmsgs = 1 }); - if (ret < 0) { - FF_DEBUG("First ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); - continue; + FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count); + return true; + } + + // Save cache to file + static void saveCacheToFile(const FFBrightnessDdcCache* cache) { + if (cache->count == 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count); + for (size_t i = 0; i < cache->count; i++) { + ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]); + } + + if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) { + FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars); + } else { + FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars); + } + } + + static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + uint8_t request[5]; + ffDdcCiBuildGetVcpRequest(request); + + struct iic_msg msg = { + .slave = FF_DDC_CI_WRITE_ADDR, + .flags = IIC_M_WR, + .len = sizeof(request), + .buf = request + }; + + struct iic_rdwr_data data = { + .msgs = &msg, + .nmsgs = 1 + }; + + int ioctlResult = ioctl(fd, I2CRDWR, &data); + if (ioctlResult < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; } ffTimeSleep(options->ddcciSleep); - uint8_t i2cOut[12] = {}; - ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_READ_ADDR, // LSB will be overridden by kernel to set read bit - .flags = IIC_M_RD, - .len = ARRAY_SIZE(i2cOut), - .buf = i2cOut }, - .nmsgs = 1 }); - if (ret < 0) { - FF_DEBUG("Second ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); - continue; + msg.slave = FF_DDC_CI_WRITE_ADDR; + msg.flags = IIC_M_RD; + msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE; + msg.buf = response; + ioctlResult = ioctl(fd, I2CRDWR, &data); + if (ioctlResult < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; } - if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) { - FF_DEBUG("i2c out validation failed: i2cOut[2] = 0x%02x, i2cOut[3] = 0x%02x", i2cOut[2], i2cOut[3]); - continue; + + if (!ffDdcCiValidateGetVcpResponse(response)) { + FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)", + (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]); + return false; } - uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9]; - uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7]; + FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd, + ((unsigned) response[8] << 8u) | (unsigned) response[9], + ((unsigned) response[6] << 8u) | (unsigned) response[7]); + return true; + } - FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); - brightness->max = max; - brightness->min = 0; - brightness->current = current; - ffStrbufInitS(&brightness->name, path + strlen("/dev/")); - brightness->builtin = false; + static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) { + FF_DEBUG("Brightness DDC/CI: scanning FreeBSD I2C buses"); + cache->count = 0; + + for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) { + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", i); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + continue; + } + + FF_DEBUG("Brightness DDC/CI: probing %s", path); + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + cache->devices[cache->count++] = i; + FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path); + } + + FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count); + return cache->count > 0; } - return nullptr; -} + static bool readEdid(int fd, uint8_t* edidData) { + uint8_t offset = 0; + + struct iic_msg msgs[2]; + msgs[0].slave = FF_DDC_EDID_WRITE_ADDR; + msgs[0].flags = IIC_M_WR; + msgs[0].len = 1; + msgs[0].buf = &offset; + + msgs[1].slave = FF_DDC_EDID_WRITE_ADDR; + msgs[1].flags = IIC_M_RD; + msgs[1].len = 128; + msgs[1].buf = edidData; + + struct iic_rdwr_data data = { + .msgs = msgs, + .nmsgs = 2 + }; + + return ioctl(fd, I2CRDWR, &data) >= 0; + } + + static bool detectCachedDevices(FFBrightnessOptions* options, FFlist* result, FFBrightnessDdcCache* cache) { + const size_t cachedCount = cache->count; + const uint32_t resultLength = result->length; + size_t validCount = 0; + + FF_DEBUG("Brightness DDC/CI: validating %zu cached bus(es)", cachedCount); + for (size_t i = 0; i < cachedCount; ++i) { + const int device = cache->devices[i]; + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", device); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, error, strerror(error)); + continue; + } + + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9]; + uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7]; + if (max == 0 || current > max) { + FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max); + continue; + } + + cache->devices[validCount++] = device; + + // Try to read EDID for monitor name + uint8_t edidData[128]; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32); + if (readEdid(fd, edidData)) { + ffEdidGetName(edidData, &name); + } + if (name.length == 0) { + ffStrbufSetF(&name, "I2C-%d", device); + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + brightness->builtin = false; + ffStrbufInitMove(&brightness->name, &name); + FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max); + } + + cache->count = validCount; + FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length); + return result->length > resultLength; + } + + const char* detectWithDdcci(FFBrightnessOptions* options, FFlist* result) { + FFBrightnessDdcCache cache = {0}; + FF_DEBUG("Brightness DDC/CI: detection started (result count=%u)", result->length); + if (!loadCacheFromFile(&cache)) { + if (!scanI2cDevices(options, &cache)) { + FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan"); + return "No I2C devices found"; + } + } + + if (!detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning"); + if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: rescan found no compatible display"); + return "No DDC/CI compatible displays found"; + } + } + + saveCacheToFile(&cache); + FF_DEBUG("Brightness DDC/CI: detection finished with %u result(s)", result->length); + return nullptr; + } #else @@ -138,10 +338,18 @@ const char* detectWithBacklight([[maybe_unused]] FFBrightnessOptions* options, [ #endif const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { + FF_DEBUG("Brightness: detection started (result count=%u)", result->length); detectWithBacklight(options, result); + FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length); if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length == 0) { + FF_DEBUG("Brightness: trying DDC/CI because no backlight result was found"); detectWithDdcci(options, result); + } else if (options->ddcciSleep == FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) { + FF_DEBUG("Brightness: skipping DDC/CI because it is disabled"); + } else { + FF_DEBUG("Brightness: skipping DDC/CI because backlight detection already found %u result(s)", result->length); } + FF_DEBUG("Brightness: detection finished (result count=%u)", result->length); return nullptr; } diff --git a/src/detection/brightness/brightness_linux.c b/src/detection/brightness/brightness_linux.c index ab82ac0b5c..cdc4b531c5 100644 --- a/src/detection/brightness/brightness_linux.c +++ b/src/detection/brightness/brightness_linux.c @@ -2,9 +2,277 @@ #include "common/io.h" #include "common/edidHelper.h" #include "common/strutil.h" +#include "common/debug.h" +#include "common/time.h" +#include #include +#include #include +#include + +#if defined(__linux__) + #include + #include + #include + #include + #include + #include + + // Try to load cache from file + static bool loadCacheFromFile(FFBrightnessDdcCache* cache) { + cache->count = 0; + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + bool valid = false; + FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars); + if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) { + // Parse cache: first number is count, rest are device numbers + char* ptr = cacheContent.chars; + char* end = ptr + cacheContent.length; + + // Read count + char* next; + long count = strtol(ptr, &next, 10); + if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) { + ptr = next; + + // Read device numbers and reject truncated or trailing data. + size_t parsedCount = 0; + for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) { + long dev = strtol(ptr, &next, 10); + if (next == ptr || dev < 0 || dev >= 100) { + break; + } + cache->devices[parsedCount] = (int) dev; + ptr = next; + } + + while (ptr < end && isspace((unsigned char) *ptr)) { + ++ptr; + } + + if (parsedCount == (size_t) count && ptr == end) { + cache->count = parsedCount; + valid = true; + for (size_t i = 0; i < cache->count; ++i) { + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", cache->devices[i]); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], errno, strerror(errno)); + valid = false; + break; + } + } + } + } + } + + if (!valid) { + FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid"); + cache->count = 0; + return false; + } + + FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count); + return true; + } + + // Save cache to file + static void saveCacheToFile(const FFBrightnessDdcCache* cache) { + if (cache->count == 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count); + for (size_t i = 0; i < cache->count; i++) { + ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]); + } + + if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) { + FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars); + } else { + FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars); + } + } + + static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + uint8_t request[5]; + ffDdcCiBuildGetVcpRequest(request); + + // i2c_msg.addr is a 7-bit address; the 8-bit write/read addresses are + // used only for the DDC/CI checksum. + struct i2c_msg msg = { + .addr = FF_DDC_CI_ADDR, + .flags = 0, + .len = sizeof(request), + .buf = request + }; + + struct i2c_rdwr_ioctl_data data = { + .msgs = &msg, + .nmsgs = 1 + }; + + int ioctlResult = ioctl(fd, I2C_RDWR, &data); + if (ioctlResult != 1) { + FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, errno, strerror(errno)); + return false; + } + + ffTimeSleep(options->ddcciSleep); + + msg.flags = I2C_M_RD; + msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE; + msg.buf = response; + ioctlResult = ioctl(fd, I2C_RDWR, &data); + if (ioctlResult != 1) { + FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, errno, strerror(errno)); + return false; + } + + if (!ffDdcCiValidateGetVcpResponse(response)) { + FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)", + (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]); + return false; + } + + FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd, + ((unsigned) response[8] << 8u) | (unsigned) response[9], + ((unsigned) response[6] << 8u) | (unsigned) response[7]); + return true; + } + + static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) { + FF_DEBUG("Brightness DDC/CI: scanning Linux I2C buses"); + cache->count = 0; + + for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) { + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", i); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + continue; + } + + FF_DEBUG("Brightness DDC/CI: probing %s", path); + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + cache->devices[cache->count++] = i; + FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path); + } + + FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count); + return cache->count > 0; + } + + static bool readEdid(int fd, uint8_t* edidData) { + uint8_t offset = 0; + + struct i2c_msg msgs[2]; + msgs[0].addr = FF_DDC_EDID_ADDR; + msgs[0].flags = 0; + msgs[0].len = 1; + msgs[0].buf = &offset; + + msgs[1].addr = FF_DDC_EDID_ADDR; + msgs[1].flags = I2C_M_RD; + msgs[1].len = 128; + msgs[1].buf = edidData; + + struct i2c_rdwr_ioctl_data data; + data.msgs = msgs; + data.nmsgs = 2; + + return ioctl(fd, I2C_RDWR, &data) == 2; + } + + static bool detectCachedDevices(FFBrightnessOptions* options, FFlist* result, FFBrightnessDdcCache* cache) { + const size_t cachedCount = cache->count; + const uint32_t resultLength = result->length; + size_t validCount = 0; + + FF_DEBUG("Brightness DDC/CI: validating %zu cached bus(es)", cachedCount); + for (size_t i = 0; i < cachedCount; ++i) { + const int device = cache->devices[i]; + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", device); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, errno, strerror(errno)); + continue; + } + + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9]; + uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7]; + if (max == 0 || current > max) { + FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max); + continue; + } + + cache->devices[validCount++] = device; + + // Try to read EDID for monitor name + uint8_t edidData[128]; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32); + if (readEdid(fd, edidData)) { + ffEdidGetName(edidData, &name); + } + if (name.length == 0) { + ffStrbufSetF(&name, "I2C-%d", device); + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + brightness->builtin = false; + ffStrbufInitMove(&brightness->name, &name); + FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max); + } + + cache->count = validCount; + FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length); + return result->length > resultLength; + } + + static const char* detectWithDdcciNative(FFBrightnessOptions* options, FFlist* result) { + FFBrightnessDdcCache cache = {0}; + FF_DEBUG("Brightness DDC/CI: native detection started (result count=%u)", result->length); + if (!loadCacheFromFile(&cache)) { + if (!scanI2cDevices(options, &cache)) { + FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan"); + return "No I2C devices found"; + } + } + + if (!detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning"); + // A cached bus can remain present after its monitor is unplugged or + // its number is reused. Rebuild the cache when no valid reply arrives. + if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: rescan found no compatible display"); + return "No DDC/CI compatible displays found"; + } + } + + saveCacheToFile(&cache); + FF_DEBUG("Brightness DDC/CI: native detection finished with %u result(s)", result->length); + return nullptr; + } +#endif static const char* detectWithBacklight(FFlist* result) { // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-backlight @@ -12,6 +280,7 @@ static const char* detectWithBacklight(FFlist* result) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir(backlightDirPath); if (dirp == nullptr) { + FF_DEBUG("opendir(\"%s\") failed: %s", backlightDirPath, strerror(errno)); return "Failed to open `/sys/class/backlight/`"; } @@ -75,100 +344,30 @@ static const char* detectWithBacklight(FFlist* result) { return nullptr; } -#ifdef FF_HAVE_DDCUTIL - #include "detection/displayserver/displayserver.h" - #include "common/library.h" - #include "common/mallocHelper.h" - - #include - #include - - // Try to be compatible with ddcutil 2.0 - #if DDCUTIL_VMAJOR >= 2 -double ddca_set_default_sleep_multiplier(double multiplier); // ddcutil 1.4 - #else -DDCA_Status ddca_init(const char* libopts, int syslog_level, int opts); - #endif - -static const char* detectWithDdcci([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { - FF_LIBRARY_LOAD_MESSAGE(libddcutil, "libddcutil" FF_LIBRARY_EXTENSION, 5); - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_display_info_list2) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_open_display2) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_any_vcp_value_using_explicit_type) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_free_any_vcp_value) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_close_display) - - #ifndef FF_DISABLE_DLOPEN - FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_init) - if (ffddca_init) { - FF_SUPPRESS_IO(); - // Ref: https://github.com/rockowitz/ddcutil/issues/344 - if (ffddca_init(nullptr, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) { - return "ddca_init() failed"; - } - } else { - FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_set_default_sleep_multiplier); - if (ffddca_set_default_sleep_multiplier) { - ffddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0); - } - - libddcutil = nullptr; // Don't dlclose libddcutil. See https://github.com/rockowitz/ddcutil/issues/330 - } - #else - #if DDCUTIL_VMAJOR >= 2 - if (ddca_init(nullptr, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) { - return "ddca_init() failed"; - } - #else - ddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0); - #endif - #endif - - FF_AUTO_FREE DDCA_Display_Info_List* infoList = nullptr; - if (ffddca_get_display_info_list2(false, &infoList) < 0) { - return "ddca_get_display_info_list2(false, &infoList) failed"; - } - - if (infoList->ct == 0) { - return "No DDC/CI compatible displays found"; - } - - for (int index = 0; index < infoList->ct; ++index) { - const DDCA_Display_Info* display = &infoList->info[index]; - - DDCA_Display_Handle handle; - if (ffddca_open_display2(display->dref, false, &handle) >= 0) { - DDCA_Any_Vcp_Value* vcpValue = nullptr; - if (ffddca_get_any_vcp_value_using_explicit_type(handle, 0x10 /*brightness*/, DDCA_NON_TABLE_VCP_VALUE, &vcpValue) >= 0) { - assert(vcpValue->value_type == DDCA_NON_TABLE_VCP_VALUE); - int current = VALREC_CUR_VAL(vcpValue), max = VALREC_MAX_VAL(vcpValue); - ffddca_free_any_vcp_value(vcpValue); - - FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); - brightness->max = max; - brightness->min = 0; - brightness->current = current; - ffStrbufInitS(&brightness->name, display->model_name); - } - ffddca_close_display(handle); - } - } - - return nullptr; -} -#endif +#include "detection/displayserver/displayserver.h" const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { + FF_DEBUG("Brightness: detection started (result count=%u)", result->length); detectWithBacklight(result); + FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length); -#ifdef FF_HAVE_DDCUTIL if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) { const FFDisplayServerResult* displayServer = ffConnectDisplayServer(); if (result->length < displayServer->displays.length) { - detectWithDdcci(options, result); + FF_DEBUG("Brightness: trying native DDC/CI (result count=%u, display count=%u)", result->length, displayServer->displays.length); +#if defined(__linux__) + const char* nativeError = detectWithDdcciNative(options, result); + if (nativeError != nullptr) { + FF_DEBUG("Native I2C DDC/CI failed: %s", nativeError); + } +#endif + } else { + FF_DEBUG("Brightness: skipping DDC/CI because result count=%u meets display count=%u", result->length, displayServer->displays.length); } + } else { + FF_DEBUG("Brightness: skipping DDC/CI because it is disabled"); } -#endif + FF_DEBUG("Brightness: detection finished (result count=%u)", result->length); return nullptr; } diff --git a/src/detection/brightness/brightness_obsd.c b/src/detection/brightness/brightness_obsd.c index 2c9847c662..5cbafe3440 100644 --- a/src/detection/brightness/brightness_obsd.c +++ b/src/detection/brightness/brightness_obsd.c @@ -1,4 +1,5 @@ #include "brightness.h" +#include "common/debug.h" #include "common/io.h" #include @@ -15,6 +16,7 @@ const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FF if (devfd < 0) { if (errno == EACCES && i == '0') { + FF_DEBUG("open(%s) failed: %s", path, strerror(errno)); return "Permission denied when opening tty device"; } if (errno == ENOENT) { diff --git a/src/detection/btrfs/btrfs_linux.c b/src/detection/btrfs/btrfs_linux.c index 077b1d5dc1..b88af815a5 100644 --- a/src/detection/btrfs/btrfs_linux.c +++ b/src/detection/btrfs/btrfs_linux.c @@ -1,5 +1,6 @@ #include "btrfs.h" +#include "common/debug.h" #include "common/io.h" #include @@ -8,11 +9,13 @@ enum { uuidLen = (uint32_t) __builtin_strlen("00000000-0000-0000-0000-0000000000 static const char* enumerateDevices(FFBtrfsResult* item, int dfd, FFstrbuf* buffer) { int subfd = openat(dfd, "devices", O_RDONLY | O_CLOEXEC | O_DIRECTORY); if (subfd < 0) { + FF_DEBUG("openat(\"devices\") failed: %s", strerror(errno)); return "openat(\"/sys/fs/btrfs/UUID/devices\") == -1"; } FF_AUTO_CLOSE_DIR DIR* dirp = fdopendir(subfd); if (dirp == nullptr) { + FF_DEBUG("fdopendir(\"devices\") failed: %s", strerror(errno)); close(subfd); return "fdopendir(\"/sys/fs/btrfs/UUID/devices\") == nullptr"; } @@ -42,11 +45,13 @@ static const char* enumerateDevices(FFBtrfsResult* item, int dfd, FFstrbuf* buff static const char* enumerateFeatures(FFBtrfsResult* item, int dfd) { int subfd = openat(dfd, "features", O_RDONLY | O_CLOEXEC | O_DIRECTORY); if (subfd < 0) { + FF_DEBUG("openat(\"features\") failed: %s", strerror(errno)); return "openat(\"/sys/fs/btrfs/UUID/features\") == -1"; } FF_AUTO_CLOSE_DIR DIR* dirp = fdopendir(subfd); if (dirp == nullptr) { + FF_DEBUG("fdopendir(\"features\") failed: %s", strerror(errno)); return "fdopendir(\"/sys/fs/btrfs/UUID/features\") == nullptr"; } @@ -67,12 +72,14 @@ static const char* enumerateFeatures(FFBtrfsResult* item, int dfd) { static const char* detectAllocation(FFBtrfsResult* item, int dfd, FFstrbuf* buffer) { FF_AUTO_CLOSE_FD int subfd = openat(dfd, "allocation", O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); if (subfd < 0) { + FF_DEBUG("openat(\"allocation\") failed: %s", strerror(errno)); return "openat(\"/sys/fs/btrfs/UUID/allocation\") == -1"; } if (ffReadFileBufferRelative(subfd, "global_rsv_size", buffer)) { item->globalReservationTotal = ffStrbufToUInt(buffer, 0); } else { + FF_DEBUG("ffReadFileBuffer(\"global_rsv_size\") failed"); return "ffReadFileBuffer(\"/sys/fs/btrfs/UUID/allocation/global_rsv_size\") == nullptr"; } @@ -124,6 +131,7 @@ static const char* detectAllocation(FFBtrfsResult* item, int dfd, FFstrbuf* buff const char* ffDetectBtrfs(FFlist* result) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/fs/btrfs/"); if (dirp == nullptr) { + FF_DEBUG("opendir(\"/sys/fs/btrfs\") failed: %s", strerror(errno)); return "opendir(\"/sys/fs/btrfs\") == nullptr"; } diff --git a/src/detection/camera/camera_android.c b/src/detection/camera/camera_android.c index abdbe3d9d0..a48004740c 100644 --- a/src/detection/camera/camera_android.c +++ b/src/detection/camera/camera_android.c @@ -1,4 +1,5 @@ #include "camera.h" +#include "common/debug.h" #include #include @@ -11,7 +12,7 @@ // Records the largest size the camera can stream in the given pixel format. The stream configurations // are a flat array of (format, width, height, isInput) quads; a negative format matches any of them. -static void ffCameraMaxStreamSize(const ACameraMetadata* metadata, int32_t format, uint32_t* width, uint32_t* height) { +static void getMaxStreamSize(const ACameraMetadata* metadata, int32_t format, uint32_t* width, uint32_t* height) { ACameraMetadata_const_entry entry; if (ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry) != ACAMERA_OK) { return; @@ -48,11 +49,13 @@ const char* ffDetectCamera(FFlist* result) { // API 24, so linking it unconditionally in CMakeLists.txt is fine on every supported device. ACameraManager* manager = ACameraManager_create(); if (!manager) { + FF_DEBUG("ACameraManager_create() failed"); return "ACameraManager_create() failed"; } ACameraIdList* idList = nullptr; if (ACameraManager_getCameraIdList(manager, &idList) != ACAMERA_OK || !idList) { + FF_DEBUG("ACameraManager_getCameraIdList() failed"); ACameraManager_delete(manager); return "ACameraManager_getCameraIdList() failed"; } @@ -91,11 +94,11 @@ const char* ffDetectCamera(FFlist* result) { } camera->width = camera->height = 0; - ffCameraMaxStreamSize(metadata, FF_ANDROID_PIXEL_FORMAT_JPEG, &camera->width, &camera->height); + getMaxStreamSize(metadata, FF_ANDROID_PIXEL_FORMAT_JPEG, &camera->width, &camera->height); if (camera->width == 0) { // Not every camera HAL lists a JPEG format. The largest size of any output format is the // same resolution on those devices. - ffCameraMaxStreamSize(metadata, -1, &camera->width, &camera->height); + getMaxStreamSize(metadata, -1, &camera->width, &camera->height); } ACameraMetadata_free(metadata); diff --git a/src/detection/camera/camera_linux.c b/src/detection/camera/camera_linux.c index 2fc8b129c7..040c252c79 100644 --- a/src/detection/camera/camera_linux.c +++ b/src/detection/camera/camera_linux.c @@ -1,8 +1,10 @@ #include "camera.h" +#include "common/debug.h" #include "common/io.h" #include #include +#include #include #if FF_HAVE_LINUX_VIDEODEV2 @@ -26,6 +28,7 @@ const char* ffDetectCamera(FFlist* result) { if (errno == ENXIO) { continue; } + FF_DEBUG("Failed to open /dev/videoN: %s", strerror(errno)); return "Failed to open /dev/videoN"; } diff --git a/src/detection/camera/camera_windows.cpp b/src/detection/camera/camera_windows.cpp index 22d2fdec19..d997575c2c 100644 --- a/src/detection/camera/camera_windows.cpp +++ b/src/detection/camera/camera_windows.cpp @@ -1,5 +1,6 @@ extern "C" { #include "camera.h" +#include "common/debug.h" #include "common/library.h" #include "common/windows/com.h" } @@ -22,20 +23,26 @@ extern "C" const char* ffDetectCamera([[maybe_unused]] FFlist* result) { } FF_AUTO_RELEASE_COM_OBJECT IMFAttributes* attrs = nullptr; - if (FAILED(ffMFCreateAttributes(&attrs, 1))) { + HRESULT hr = ffMFCreateAttributes(&attrs, 1); + if (FAILED(hr)) { + FF_DEBUG("MFCreateAttributes() failed: %s", ffDebugHResult(hr)); return "MFCreateAttributes() failed"; } - if (FAILED(attrs->SetGUID( + hr = attrs->SetGUID( MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, - MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) { + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (FAILED(hr)) { + FF_DEBUG("SetGUID(MF_*) failed: %s", ffDebugHResult(hr)); return "SetGUID(MF_*) failed"; } IMFActivate** devices = nullptr; uint32_t count; - if (FAILED(ffMFEnumDeviceSources(attrs, &devices, &count))) { + hr = ffMFEnumDeviceSources(attrs, &devices, &count); + if (FAILED(hr)) { + FF_DEBUG("MFEnumDeviceSources() failed: %s", ffDebugHResult(hr)); return "MFEnumDeviceSources() failed"; } diff --git a/src/detection/chassis/chassis_apple.c b/src/detection/chassis/chassis_apple.c index b5a041dcf0..b24a630c71 100644 --- a/src/detection/chassis/chassis_apple.c +++ b/src/detection/chassis/chassis_apple.c @@ -1,4 +1,5 @@ #include "chassis.h" +#include "common/debug.h" #include "detection/host/host.h" const char* ffDetectChassis(FFChassisResult* result) { @@ -11,7 +12,9 @@ const char* ffDetectChassis(FFChassisResult* result) { .uuid = ffStrbufCreate(), .vendor = ffStrbufCreate(), }; - if (ffDetectHost(&host) != nullptr) { + const char* hostError = ffDetectHost(&host); + if (hostError != nullptr) { + FF_DEBUG("ffDetectHost() failed: %s", hostError); return "Failed to detect host"; } diff --git a/src/detection/chassis/chassis_windows.c b/src/detection/chassis/chassis_windows.c index 25b37f85ad..693ccc38ed 100644 --- a/src/detection/chassis/chassis_windows.c +++ b/src/detection/chassis/chassis_windows.c @@ -1,4 +1,5 @@ #include "chassis.h" +#include "common/debug.h" #include "common/smbios.h" // 7.4 @@ -32,11 +33,13 @@ static_assert(offsetof(FFSmbiosSystemEnclosure, ContainedElements) == 0x15, const char* ffDetectChassis(FFChassisResult* result) { const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); if (!smbiosTable) { + FF_DEBUG("System enclosure: failed to get SMBIOS data"); return "Failed to get SMBIOS data"; } const FFSmbiosSystemEnclosure* data = (const FFSmbiosSystemEnclosure*) (*smbiosTable)[FF_SMBIOS_TYPE_SYSTEM_ENCLOSURE]; if (!data) { + FF_DEBUG("System enclosure: section is not found in SMBIOS data"); return "System enclosure is not found in SMBIOS data"; } diff --git a/src/detection/codec/codec_android.c b/src/detection/codec/codec_android.c index d4b465ec66..84a63a81a9 100644 --- a/src/detection/codec/codec_android.c +++ b/src/detection/codec/codec_android.c @@ -1,6 +1,7 @@ #include "codec.h" #include "common/android/api.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -99,5 +100,6 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o // Reading the codec name is the only way to tell a hardware codec from a software one. Without // it there is nothing to report, and listing the codecs as if they were accelerated would be a // guess, so say why instead. + FF_DEBUG("AMediaCodec_getName() requires Android 9 (API 28)"); return "AMediaCodec_getName() requires Android 9 (API 28)"; } diff --git a/src/detection/codec/codec_linux.c b/src/detection/codec/codec_linux.c index f058744bb1..f409aecf7a 100644 --- a/src/detection/codec/codec_linux.c +++ b/src/detection/codec/codec_linux.c @@ -6,6 +6,7 @@ #if FF_HAVE_VA || FF_HAVE_VDPAU + #include "common/debug.h" #include "common/library.h" #include "common/mallocHelper.h" #include "common/strutil.h" @@ -14,6 +15,7 @@ #if FF_HAVE_VA #include + #include #include typedef struct FFVAData { @@ -227,6 +229,7 @@ static const char* detectCodecByVaDrm(FFVAData* vaData, FFCodecOptions* options, FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/dev/dri/"); if (dirp == nullptr) { + FF_DEBUG("opendir(/dev/dri/) failed: %s", strerror(errno)); return "opendir(/dev/dri/) failed"; } int drifd = dirfd(dirp); @@ -268,11 +271,13 @@ static const char* detectCodecByVaX11(FFVAData* vaData, FFCodecOptions* options, Display* x11Display = ffXOpenDisplay(nullptr); if (!x11Display) { + FF_DEBUG("XOpenDisplay() failed"); return "XOpenDisplay() failed"; } VADisplay display = ffvaGetDisplay(x11Display); if (!display) { + FF_DEBUG("vaGetDisplay() failed"); ffXCloseDisplay(x11Display); return "vaGetDisplay() failed"; } @@ -380,14 +385,18 @@ static const char* detectCodecByVdpau(FFCodecOptions* options, FFlist* result) { Display* x11Display = ffXOpenDisplay(nullptr); if (!x11Display) { + FF_DEBUG("XOpenDisplay() failed"); return "XOpenDisplay() failed"; } VdpDevice device = VDP_INVALID_HANDLE; VdpGetProcAddress* ffvdp_get_proc_address = nullptr; - if (ffvdp_device_create_x11(x11Display, ffXDefaultScreen(x11Display), &device, &ffvdp_get_proc_address) != VDP_STATUS_OK || + VdpStatus ffvdp_device_create_x11_status = ffvdp_device_create_x11(x11Display, ffXDefaultScreen(x11Display), &device, &ffvdp_get_proc_address); + if (ffvdp_device_create_x11_status != VDP_STATUS_OK || device == VDP_INVALID_HANDLE || ffvdp_get_proc_address == nullptr) { + FF_DEBUG("vdp_device_create_x11() failed: status=%d, device=%x, getProcAddress=%p", + ffvdp_device_create_x11_status, (unsigned) device, (void*) ffvdp_get_proc_address); ffXCloseDisplay(x11Display); return "vdp_device_create_x11() failed"; } @@ -398,6 +407,7 @@ static const char* detectCodecByVdpau(FFCodecOptions* options, FFlist* result) { ffvdp_get_proc_address(device, VDP_FUNC_ID_DECODER_QUERY_CAPABILITIES, (void**) &ffvdp_decoder_query_capabilities) != VDP_STATUS_OK || ffvdp_device_destroy == nullptr || ffvdp_decoder_query_capabilities == nullptr) { + FF_DEBUG("vdp_device_create_x11() did not provide VDP_FUNC_ID_DEVICE_DESTROY / VDP_FUNC_ID_DECODER_QUERY_CAPABILITIES"); if (ffvdp_device_destroy) { ffvdp_device_destroy(device); } @@ -459,6 +469,7 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /* list } #endif + FF_DEBUG("Neither libva nor libvdpau could initialize a display"); return "Both libva and libvdpau fail to initialize"; } diff --git a/src/detection/codec/codec_windows.cpp b/src/detection/codec/codec_windows.cpp index 1820361df2..1a84b9df4a 100644 --- a/src/detection/codec/codec_windows.cpp +++ b/src/detection/codec/codec_windows.cpp @@ -1,5 +1,6 @@ extern "C" { #include "codec.h" +#include "common/debug.h" #include "common/library.h" #include "common/windows/com.h" #include "common/windows/unicode.h" @@ -523,6 +524,7 @@ const char* detectD3d12va(FFCodecOptions* options, FFlist* result /*list of FFCo }); if (result->length == resultLengthBefore) { + FF_DEBUG("No D3D12 video acceleration support"); return "No D3D12 video acceleration support"; } @@ -539,7 +541,9 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o } FF_AUTO_RELEASE_COM_OBJECT IDXGIFactory1* factory = nullptr; - if (FAILED(ffCreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**) &factory)) || !factory) { + HRESULT hr = ffCreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**) &factory); + if (FAILED(hr) || !factory) { + FF_DEBUG("CreateDXGIFactory1() failed: %s", ffDebugHResult(hr)); return "CreateDXGIFactory1() failed"; } diff --git a/src/detection/command/command.c b/src/detection/command/command.c index 18ea9c915a..c3e1fe1ed0 100644 --- a/src/detection/command/command.c +++ b/src/detection/command/command.c @@ -15,13 +15,19 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand return "No command text specified"; } +#if _WIN32 + if (ffStrbufEqualS(&options->shell, "cmd.exe") && ffStrbufEqualS(&options->param, "/d/c")) { + ffStrbufAppendS(&options->text, " & exit %^errorlevel%"); // Necessary to propagate 9009 from CMD + } +#endif + return ffProcessSpawn(options->param.length ? (char* const[]) { options->shell.chars, options->param.chars, options->text.chars, nullptr } : (char* const[]) { options->shell.chars, options->text.chars, nullptr }, - options->useStdErr, + options->useOutput, FF_PROCESS_INHERIT_STDIN, handle); } diff --git a/src/detection/cpu/cpu_apple.c b/src/detection/cpu/cpu_apple.c index bce7d55b29..2071af1fab 100644 --- a/src/detection/cpu/cpu_apple.c +++ b/src/detection/cpu/cpu_apple.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/apple/smc_temps.h" #include "common/strutil.h" @@ -53,15 +54,18 @@ static const char* detectFrequency(FFCPUResult* cpu) { FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceNameMatching("pmgr")); if (!entryDevice) { + FF_DEBUG("IOServiceGetMatchingService() failed"); return "IOServiceGetMatchingService() failed"; } if (!IOObjectConformsTo(entryDevice, "AppleARMIODevice")) { + FF_DEBUG("\"pmgr\" should conform to \"AppleARMIODevice\""); return "\"pmgr\" should conform to \"AppleARMIODevice\""; } FF_CFTYPE_AUTO_RELEASE CFDataRef freqProperty = (CFDataRef) IORegistryEntryCreateCFProperty(entryDevice, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, kNilOptions); if (!freqProperty || CFGetTypeID(freqProperty) != CFDataGetTypeID()) { + FF_DEBUG("\"voltage-states5-sram\" in \"pmgr\" is not found"); return "\"voltage-states5-sram\" in \"pmgr\" is not found"; } @@ -69,6 +73,7 @@ static const char* detectFrequency(FFCPUResult* cpu) { // voltage-states1-sram stores ecores' CFIndex propLength = CFDataGetLength(freqProperty); if (propLength == 0 || propLength % (CFIndex) (sizeof(uint32_t) * 2) != 0) { + FF_DEBUG("Invalid \"voltage-states5-sram\" length"); return "Invalid \"voltage-states5-sram\" length"; } @@ -106,6 +111,7 @@ static const char* detectFrequency(FFCPUResult* cpu) { static const char* detectCoreCount(FFCPUResult* cpu) { uint32_t nPerfLevels = (uint32_t) ffSysctlGetInt("hw.nperflevels", 0); if (nPerfLevels <= 0) { + FF_DEBUG("sysctl(hw.nperflevels) failed"); return "sysctl(hw.nperflevels) failed"; } @@ -124,7 +130,9 @@ static const char* detectCoreCount(FFCPUResult* cpu) { } const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { - if (ffSysctlGetString("machdep.cpu.brand_string", &cpu->name) != nullptr) { + const char* error = ffSysctlGetString("machdep.cpu.brand_string", &cpu->name); + if (error != nullptr) { + FF_DEBUG("sysctlbyname(machdep.cpu.brand_string) failed: %s", error); return "sysctlbyname(machdep.cpu.brand_string) failed"; } diff --git a/src/detection/cpu/cpu_bsd.c b/src/detection/cpu/cpu_bsd.c index 085b014988..d671d8b6af 100644 --- a/src/detection/cpu/cpu_bsd.c +++ b/src/detection/cpu/cpu_bsd.c @@ -1,7 +1,10 @@ #include "cpu.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/strutil.h" +#include +#include #include #if __has_include() #include @@ -13,6 +16,7 @@ static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { if (options->tempSensor.length > 0) { temp = ffSysctlGetInt(options->tempSensor.chars, -999999); if (temp == -999999) { + FF_DEBUG("sysctlbyname(%s) failed: %s", options->tempSensor.chars, strerror(errno)); return "ffSysctlGetInt(options->tempSensor) failed"; } } else { @@ -21,6 +25,7 @@ static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { // Thermal zone temperature temp = ffSysctlGetInt("hw.acpi.thermal.tz0.temperature", -999999); if (temp == -999999) { + FF_DEBUG("Neither sysctlbyname(dev.cpu.0.temperature) nor sysctlbyname(hw.acpi.thermal.tz0.temperature) is readable: %s", strerror(errno)); return "ffSysctlGetInt(\"dev.cpu.0.temperature\" or \"hw.acpi.thermal.tz0.temperature\") failed"; } } @@ -32,7 +37,9 @@ static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { } const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { - if (ffSysctlGetString("hw.model", &cpu->name) != nullptr) { + const char* modelError = ffSysctlGetString("hw.model", &cpu->name); + if (modelError != nullptr) { + FF_DEBUG("sysctlbyname(hw.model) failed: %s", modelError); return "sysctlbyname(hw.model) failed"; } diff --git a/src/detection/cpu/cpu_haiku.c b/src/detection/cpu/cpu_haiku.c index 5ca06a2842..b7c10e4fb6 100644 --- a/src/detection/cpu/cpu_haiku.c +++ b/src/detection/cpu/cpu_haiku.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include @@ -6,18 +7,23 @@ const char* ffDetectCPUImpl([[maybe_unused]] const FFCPUOptions* options, FFCPUResult* cpu) { system_info sysInfo; - if (get_system_info(&sysInfo) != B_OK) { + status_t status = get_system_info(&sysInfo); + if (status != B_OK) { + FF_DEBUG("get_system_info() failed: %d", (int) status); return "get_system_info() failed"; } uint32 topoNodeCount = 0; get_cpu_topology_info(nullptr, &topoNodeCount); if (topoNodeCount == 0) { + FF_DEBUG("get_cpu_topology_info(nullptr) failed"); return "get_cpu_topology_info(nullptr) failed"; } FF_AUTO_FREE cpu_topology_node_info* topology = malloc(sizeof(*topology) * topoNodeCount); - if (get_cpu_topology_info(topology, &topoNodeCount) != B_OK) { + status_t topoStatus = get_cpu_topology_info(topology, &topoNodeCount); + if (topoStatus != B_OK) { + FF_DEBUG("get_cpu_topology_info(topology) failed: %d", (int) topoStatus); return "get_cpu_topology_info(topology) failed"; } diff --git a/src/detection/cpu/cpu_linux.c b/src/detection/cpu/cpu_linux.c index 4786874dd3..31c2331037 100644 --- a/src/detection/cpu/cpu_linux.c +++ b/src/detection/cpu/cpu_linux.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/io.h" #include "common/processing.h" #include "common/properties.h" @@ -8,6 +9,7 @@ #include #include +#include #include #include #include @@ -706,66 +708,71 @@ static bool detectFrequency(FFCPUResult* cpu, const FFCPUOptions* options) { return true; } -// Also used in tests/cpu.c -bool ffCPUQualcommSnapdragonToName(FFstrbuf* name, const char* model) { - // X2 device trees use platform codenames rather than exact retail part - // numbers. qcom,glymur is used by X2 Elite Extreme boards, qcom,mahua by - // X2 Elite boards, and qcom,kalambo by X2 Plus boards. Do not append a - // part number from /sys/bus/soc/devices/soc0/soc_id: it contains - // SMEM_HW_SW_BUILD_ID.id (the platform ID), 662 is unnamed in the public - // qcom,ids.h, and one compatible can cover multiple retail SKUs. - // https://github.com/fastfetch-cli/fastfetch/issues/2611 - if (ffStrEquals(model, "glymur")) { - ffStrbufSetS(name, "Qualcomm Snapdragon X2 Elite Extreme"); - return true; - } - if (ffStrEquals(model, "mahua")) { - ffStrbufSetS(name, "Qualcomm Snapdragon X2 Elite"); - return true; - } - if (ffStrEquals(model, "kalambo")) { - ffStrbufSetS(name, "Qualcomm Snapdragon X2 Plus"); - return true; +bool ffCPUQualcommSnapdragonToName(FFstrbuf* name, char* compatibles) { + // While this also would be 555 below split it out as it's only seen on the devkit + for (char* p = compatibles; *p; p += strlen(p) + 1) { + if (strstr(p, "x1e001de") != nullptr) { + ffStrbufSetS(name, "Qualcomm Snapdragon X Elite X1E-00-1DE"); + return true; + } } - // SoC models are enumerated in the form of `x[e|p]`, e.g. `x1e80100`, - // which is marketed as `X1E-80-100`, where `e` stands for Elite and `p` for Plus. - // https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips#Snapdragon_X_series - const char* prefix; - uint32_t prefixLen; - - if (ffStrStartsWith(model, "x1e")) { - prefix = "Qualcomm Snapdragon X Elite"; - prefixLen = 3; - } else if (ffStrStartsWith(model, "x1p")) { - prefix = "Qualcomm Snapdragon X Plus"; - prefixLen = 3; - } else if (ffStrStartsWith(model, "x2e")) { - // Only X2E-94-100 and X2E-96-100 are branded as Extreme - bool extreme = ffStrStartsWith(model, "x2e94") || ffStrStartsWith(model, "x2e96"); - prefix = extreme ? "Qualcomm Snapdragon X2 Elite Extreme" : "Qualcomm Snapdragon X2 Elite"; - prefixLen = 3; - } else if (ffStrStartsWith(model, "x2p")) { - prefix = "Qualcomm Snapdragon X2 Plus"; - prefixLen = 3; - } else if (ffStrStartsWith(model, "x1")) { - prefix = "Qualcomm Snapdragon X"; - prefixLen = 2; - } else { + // https://github.com/tianocore/edk2-platforms/blob/9c2cdde/Silicon/Qualcomm/Common/QualcommCommonSiliconPkg/Include/ChipInfoDefs.h + char content[16]; + ssize_t length = ffReadFileData("/sys/devices/soc0/soc_id", ARRAY_SIZE(content), content); + if (length < 1) { return false; } + if (content[length - 1] != '\n') { + return false; // must end with \n + } - uint32_t length = (uint32_t) strlen(model); - bool splitCode = length - prefixLen == 5; - - ffStrbufSetF(name, "%s X", prefix); - for (uint32_t i = 1; i < length; ++i) { - if (i == prefixLen || (splitCode && i == length - 3)) { - ffStrbufAppendC(name, '-'); - } - ffStrbufAppendC(name, (char) toupper(model[i])); + unsigned long long soc_id = strtoull(content, nullptr, 10); + const char* soc = nullptr; + switch (soc_id) { + // glymur + case 662: + soc = "X2 Elite Extreme X2E-94/96-100"; + break; + case 698: + soc = "X2 Elite X2E-88/90-100"; + break; + // mahua + case 699: + soc = "X2 Elite X2E-80/84-100"; + break; + case 693: + soc = "X2 Elite X2E-78-100"; + break; + case 728: + soc = "X2 Plus X2P-64-100"; + break; + // kalambo + case 719: + soc = "X2 Plus X2P-42-100"; + break; + // hamoa + case 555: + soc = "X Elite X1E-78/80/84-100"; + break; + case 615: + soc = "X Plus X1P-64/66-100"; + break; + case 616: + soc = "X X1-26-101"; + break; + // purwa + case 635: + soc = "X X1-26-100 / X Plus X1P-42/46-100"; + break; } - return true; + if (soc) { + ffStrbufSetS(name, "Qualcomm Snapdragon "); + ffStrbufAppendS(name, soc); + return true; + } + + return false; } #if __i386__ || __x86_64__ @@ -826,11 +833,13 @@ bool ffCPUQualcommSnapdragonToName(FFstrbuf* name, const char* model) { static const char* detectPhysicalCores(FFCPUResult* cpu) { int dfd = open("/sys/devices/system/cpu/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (dfd < 0) { + FF_DEBUG("open(/sys/devices/system/cpu/) failed: %s", strerror(errno)); return "open(\"/sys/devices/system/cpu/\") failed"; } FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd); if (!dir) { + FF_DEBUG("fdopendir(/sys/devices/system/cpu/) failed: %s", strerror(errno)); close(dfd); return "fdopendir(dfd) failed"; } @@ -1004,7 +1013,7 @@ static const char* detectPhysicalCores(FFCPUResult* cpu) { #endif else if (ffStrEquals(vendor, "qcom")) { // https://elixir.bootlin.com/linux/latest/source/arch/arm64/boot/dts/qcom - if (!ffCPUQualcommSnapdragonToName(&cpu->name, model)) { + if (!ffCPUQualcommSnapdragonToName(&cpu->name, content)) { if (ffStrStartsWith(model, "sc")) { const char* code = model + 2; uint32_t deviceId = (uint32_t) strtoul(code, nullptr, 10); diff --git a/src/detection/cpu/cpu_nbsd.c b/src/detection/cpu/cpu_nbsd.c index d5ccbf2305..f6cfd4362b 100644 --- a/src/detection/cpu/cpu_nbsd.c +++ b/src/detection/cpu/cpu_nbsd.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/io.h" @@ -8,6 +9,8 @@ #include #include #include +#include +#include static void freePropDict(prop_dictionary_t* pdict) { assert(pdict != nullptr); @@ -20,11 +23,13 @@ static void freePropDict(prop_dictionary_t* pdict) { static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { FF_AUTO_CLOSE_FD int fd = open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC); if (fd < 0) { + FF_DEBUG("open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed: %s", strerror(errno)); return "open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed"; } [[gnu::cleanup(freePropDict)]] prop_dictionary_t root = nullptr; if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &root) < 0) { + FF_DEBUG("prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed: %s", strerror(errno)); return "prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed"; } @@ -33,6 +38,7 @@ static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { if (options->tempSensor.length > 0) { array = prop_dictionary_get(root, options->tempSensor.chars); if (!array) { + FF_DEBUG("No temp data found in specified sensor"); return "No temp data found in specified sensor"; } } else { @@ -47,21 +53,25 @@ static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { array = prop_dictionary_get(root, "acpitz0"); // Thermal Zones } if (!array) { + FF_DEBUG("No temp data found in root dictionary"); return "No temp data found in root dictionary"; } } if (prop_array_count(array) != 2) { + FF_DEBUG("Unexpected `xtemp0` data"); return "Unexpected `xtemp0` data"; } prop_dictionary_t dict = prop_array_get(array, 0); if (prop_object_type(dict) != PROP_TYPE_DICTIONARY) { + FF_DEBUG("Unexpected `xtemp0[0]`"); return "Unexpected `xtemp0[0]`"; } int temp = 0; // in µK if (!prop_dictionary_get_int(dict, "cur-value", &temp)) { + FF_DEBUG("Failed to get temperature"); return "Failed to get temperature"; } diff --git a/src/detection/cpu/cpu_obsd.c b/src/detection/cpu/cpu_obsd.c index 5583fe59f4..1d692bb322 100644 --- a/src/detection/cpu/cpu_obsd.c +++ b/src/detection/cpu/cpu_obsd.c @@ -1,7 +1,9 @@ #include "cpu.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/strutil.h" +#include #include #include #include @@ -19,6 +21,7 @@ static const char* detectCPUTemp(const FFCPUOptions* options, FFCPUResult* cpu) if (errno == ENXIO) { continue; } + FF_DEBUG("sysctl(sensordev) failed: %s", strerror(errno)); return "sysctl(sensordev) failed"; } @@ -37,6 +40,7 @@ static const char* detectCPUTemp(const FFCPUOptions* options, FFCPUResult* cpu) size_t slen = sizeof(struct sensor); if (sysctl(mib, 5, &sensor, &slen, nullptr, 0) < 0) { if (errno != ENOENT) { + FF_DEBUG("sysctl(sensor) failed: %s", strerror(errno)); return "sysctl(sensor) failed"; } continue; @@ -50,11 +54,14 @@ static const char* detectCPUTemp(const FFCPUOptions* options, FFCPUResult* cpu) } } + FF_DEBUG("No sensor for CPU temp found"); return "No sensor for CPU temp found"; } const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { - if (ffSysctlGetString(CTL_HW, HW_MODEL, &cpu->name)) { + const char* error = ffSysctlGetString(CTL_HW, HW_MODEL, &cpu->name); + if (error) { + FF_DEBUG("sysctl(hw.model) failed: %s", error); return "sysctl(hw.model) failed"; } diff --git a/src/detection/cpu/cpu_sunos.c b/src/detection/cpu/cpu_sunos.c index 43b92c3715..cbe24160ed 100644 --- a/src/detection/cpu/cpu_sunos.c +++ b/src/detection/cpu/cpu_sunos.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/processing.h" #include "common/strutil.h" #include @@ -31,6 +32,7 @@ static const char* detectCPUTempByKstat(const FFCPUOptions* options, kstat_ctl_t } } + FF_DEBUG("Failed to find CPU temperature using kstat"); return "Failed to find CPU temperature using kstat"; } @@ -50,6 +52,7 @@ static const char* detectCPUTempByIpmiTool(FFCPUResult* cpu) { } } + FF_DEBUG("ipmitool sdr list failed to find CPU temperature"); return "ipmitool sdr list failed to find CPU temperature"; } @@ -101,15 +104,18 @@ static inline uint16_t countTypeId(kstat_ctl_t* kc, const char* type) { const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { [[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); if (!kc) { + FF_DEBUG("kstat_open() failed"); return "kstat_open() failed"; } kstat_t* ks = kstat_lookup(kc, "cpu_info", -1, nullptr); if (!ks) { + FF_DEBUG("kstat_lookup() failed"); return "kstat_lookup() failed"; } if (kstat_read(kc, ks, nullptr) < 0) { + FF_DEBUG("kstat_read() failed"); return "kstat_read() failed"; } diff --git a/src/detection/cpu/cpu_windows.c b/src/detection/cpu/cpu_windows.c index 8975775d07..725a3bd122 100644 --- a/src/detection/cpu/cpu_windows.c +++ b/src/detection/cpu/cpu_windows.c @@ -1,4 +1,5 @@ #include "cpu.h" +#include "common/debug.h" #include "common/endian.h" #include "common/windows/registry.h" #include "common/windows/nt.h" @@ -34,23 +35,30 @@ const char* detectThermalTemp(const FFCPUOptions* options, double* result) { }; if (options->tempSensor.length > 0) { - if (!NT_SUCCESS(RtlUTF8ToUnicodeN(querySpec.Name, (ULONG) sizeof(querySpec.Name), nullptr, options->tempSensor.chars, (ULONG) options->tempSensor.length + 1))) { + NTSTATUS status = RtlUTF8ToUnicodeN(querySpec.Name, (ULONG) sizeof(querySpec.Name), nullptr, options->tempSensor.chars, (ULONG) options->tempSensor.length + 1); + if (!NT_SUCCESS(status)) { + FF_DEBUG("Invalid temp sensor string: %s", ffDebugNtStatus(status)); return "Invalid temp sensor string"; } } DWORD dataSize = 0; - if (PerfEnumerateCounterSetInstances(nullptr, &querySpec.Identifier.CounterSetGuid, nullptr, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + DWORD ret = PerfEnumerateCounterSetInstances(nullptr, &querySpec.Identifier.CounterSetGuid, nullptr, 0, &dataSize); + if (ret != ERROR_NOT_ENOUGH_MEMORY) { + FF_DEBUG("PerfEnumerateCounterSetInstances() failed: %s", ffDebugWin32Error(ret)); return "PerfEnumerateCounterSetInstances() failed"; } if (dataSize <= sizeof(PERF_INSTANCE_HEADER)) { + FF_DEBUG("No `Thermal Zone Information` instances found"); return "No `Thermal Zone Information` instances found"; } { FF_AUTO_FREE PERF_INSTANCE_HEADER* const pHead = malloc(dataSize); - if (PerfEnumerateCounterSetInstances(nullptr, &querySpec.Identifier.CounterSetGuid, pHead, dataSize, &dataSize) != ERROR_SUCCESS) { + ret = PerfEnumerateCounterSetInstances(nullptr, &querySpec.Identifier.CounterSetGuid, pHead, dataSize, &dataSize); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfEnumerateCounterSetInstances() failed to get instance headers: %s", ffDebugWin32Error(ret)); return "PerfEnumerateCounterSetInstances() failed to get instance headers"; } @@ -70,6 +78,7 @@ const char* detectThermalTemp(const FFCPUOptions* options, double* result) { if (dataSize == 0) { if (options->tempSensor.length > 0) { + FF_DEBUG("Unable to find CPU sensor"); return "Unable to find CPU sensor"; } @@ -81,34 +90,45 @@ const char* detectThermalTemp(const FFCPUOptions* options, double* result) { [[gnu::cleanup(ffPerfCloseQueryHandle)]] HANDLE hQuery = nullptr; - if (PerfOpenQueryHandle(nullptr, &hQuery) != ERROR_SUCCESS) { + ret = PerfOpenQueryHandle(nullptr, &hQuery); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfOpenQueryHandle() failed: %s", ffDebugWin32Error(ret)); return "PerfOpenQueryHandle() failed"; } - if (PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)) != ERROR_SUCCESS) { + ret = PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfAddCounters() failed: %s", ffDebugWin32Error(ret)); return "PerfAddCounters() failed"; } if (querySpec.Identifier.Status != ERROR_SUCCESS) { + FF_DEBUG("PerfAddCounters() reports invalid identifier: %s", ffDebugWin32Error(querySpec.Identifier.Status)); return "PerfAddCounters() reports invalid identifier"; } - if (PerfQueryCounterData(hQuery, nullptr, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + ret = PerfQueryCounterData(hQuery, nullptr, 0, &dataSize); + if (ret != ERROR_NOT_ENOUGH_MEMORY) { + FF_DEBUG("PerfQueryCounterData(nullptr) failed: %s", ffDebugWin32Error(ret)); return "PerfQueryCounterData(nullptr) failed"; } if (dataSize <= sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER)) { // PERF_ERROR_RETURN, should not happen + FF_DEBUG("instance doesn't exist"); return "instance doesn't exist"; } FF_AUTO_FREE PERF_DATA_HEADER* const pDataHeader = malloc(dataSize); - if (PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize) != ERROR_SUCCESS) { + ret = PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfQueryCounterData(pDataHeader) failed: %s", ffDebugWin32Error(ret)); return "PerfQueryCounterData(pDataHeader) failed"; } PERF_COUNTER_HEADER* pCounterHeader = (PERF_COUNTER_HEADER*) (pDataHeader + 1); if (pCounterHeader->dwType != PERF_MULTIPLE_COUNTERS) { + FF_DEBUG("Invalid counter type"); return "Invalid counter type"; } @@ -190,18 +210,21 @@ static_assert(offsetof(FFSmbiosProcessorInfo, ThreadEnabled) == 0x30, static const char* detectMaxSpeedBySmbios(FFCPUResult* cpu) { const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); if (!smbiosTable) { + FF_DEBUG("Failed to get SMBIOS data"); return "Failed to get SMBIOS data"; } const FFSmbiosProcessorInfo* data = (const FFSmbiosProcessorInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_PROCESSOR_INFO]; if (!data) { + FF_DEBUG("Processor information is not found in SMBIOS data"); return "Processor information is not found in SMBIOS data"; } while (data->ProcessorType != 0x03 /*Central Processor*/ || (data->Status & 0b00000111) != 1 /*Enabled*/) { data = (const FFSmbiosProcessorInfo*) ffSmbiosNextEntry(&data->Header); if (data->Header.Type != FF_SMBIOS_TYPE_PROCESSOR_INFO) { + FF_DEBUG("No active CPU is found in SMBIOS data"); return "No active CPU is found in SMBIOS data"; } } @@ -209,6 +232,7 @@ static const char* detectMaxSpeedBySmbios(FFCPUResult* cpu) { uint32_t speed = FF_READ_LE(data->MaxSpeed); // Sometimes SMBIOS reports invalid value. We assume that max speed is small than 2x of base if (speed < cpu->frequencyBase || speed > cpu->frequencyBase * 2) { + FF_DEBUG("Possible invalid CPU max speed in SMBIOS data. See #800"); return "Possible invalid CPU max speed in SMBIOS data. See #800"; } @@ -236,13 +260,16 @@ static const char* detectNCores(const FFCPUOptions* options, FFCPUResult* cpu) { ULONG length = 0; NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), nullptr, 0, &length); if (length == 0) { + FF_DEBUG("GetLogicalProcessorInformationEx(RelationAll, nullptr, &length) failed"); return "GetLogicalProcessorInformationEx(RelationAll, nullptr, &length) failed"; } FF_AUTO_FREE SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* pProcessorInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) malloc(length); - if (!NT_SUCCESS(NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length))) { + NTSTATUS status = NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length); + if (!NT_SUCCESS(status)) { + FF_DEBUG("GetLogicalProcessorInformationEx(RelationAll, pProcessorInfo, &length) failed: %s", ffDebugNtStatus(status)); return "GetLogicalProcessorInformationEx(RelationAll, pProcessorInfo, &length) failed"; } @@ -283,6 +310,7 @@ static const char* detectNCores(const FFCPUOptions* options, FFCPUResult* cpu) { static const char* detectByRegistry(FFCPUResult* cpu) { FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", &hKey, nullptr)) { + FF_DEBUG("ffRegOpenKeyForRead() failed for HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"); return "ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L\"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\", &hKey, nullptr) failed"; } @@ -294,6 +322,7 @@ static const char* detectByRegistry(FFCPUResult* cpu) { nullptr)) { ffStrbufTrimRightSpace(&cpu->vendor); } else { + FF_DEBUG("ffRegReadValues() failed for CPU registry key"); return "ffRegReadValues() failed for CPU registry key"; } diff --git a/src/detection/cpucache/cpucache_apple.c b/src/detection/cpucache/cpucache_apple.c index 50aec02320..f1ed73bc33 100644 --- a/src/detection/cpucache/cpucache_apple.c +++ b/src/detection/cpucache/cpucache_apple.c @@ -1,4 +1,5 @@ #include "cpucache.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/strutil.h" @@ -6,6 +7,7 @@ const char* ffDetectCPUCache(FFCPUCacheResult* result) { // https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_system_capabilities#3901385 uint32_t nPerfLevels = (uint32_t) ffSysctlGetInt("hw.nperflevels", 0); if (nPerfLevels <= 0) { + FF_DEBUG("sysctl(hw.nperflevels) failed"); return "sysctl(hw.nperflevels) failed"; } diff --git a/src/detection/cpucache/cpucache_linux.c b/src/detection/cpucache/cpucache_linux.c index a551849558..ff0551cbef 100644 --- a/src/detection/cpucache/cpucache_linux.c +++ b/src/detection/cpucache/cpucache_linux.c @@ -1,11 +1,16 @@ #include "cpucache.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" +#include +#include + static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, FFstrbuf* buffer, FFstrbuf* added) { uint32_t baseLen = path->length; ffStrbufAppendS(path, "/level"); if (!ffReadFileBuffer(path->chars, buffer)) { + FF_DEBUG("Failed to read \"%s\"", path->chars); return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/level\") == nullptr"; } @@ -17,6 +22,7 @@ static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, ffStrbufSubstrBefore(path, baseLen); ffStrbufAppendS(path, "/size"); if (!ffReadFileBuffer(path->chars, buffer)) { + FF_DEBUG("Failed to read \"%s\"", path->chars); return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/size\") == nullptr"; } @@ -28,6 +34,7 @@ static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, ffStrbufSubstrBefore(path, baseLen); ffStrbufAppendS(path, "/type"); if (!ffReadFileBuffer(path->chars, buffer)) { + FF_DEBUG("Failed to read \"%s\"", path->chars); return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/type\") == nullptr"; } ffStrbufTrimRightSpace(buffer); @@ -47,6 +54,7 @@ static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, cacheType = FF_CPU_CACHE_TYPE_TRACE; break; default: + FF_DEBUG("Unknown cache type \"%c\"", buffer->chars[0]); return "unknown cache type"; } @@ -62,6 +70,7 @@ static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, ffStrbufClear(buffer); ffStrbufAppendC(buffer, '['); if (!ffAppendFileBuffer(path->chars, buffer)) { + FF_DEBUG("Failed to read \"%s\"", path->chars); return "ffAppendFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/shared_cpu_list\") == nullptr"; } ffStrbufTrimRightSpace(buffer); @@ -82,6 +91,7 @@ static const char* parseCpuCache(FFstrbuf* path, FFCPUCacheResult* result, FFstr uint32_t baseLen = path->length; FF_AUTO_CLOSE_DIR DIR* pathCacheDir = opendir(path->chars); if (!pathCacheDir) { + FF_DEBUG("opendir(\"%s\") failed: %s", path->chars, strerror(errno)); return "opendir(\"/sys/devices/system/cpu/cpuX/cache/\") == nullptr"; } @@ -108,6 +118,7 @@ const char* ffDetectCPUCache(FFCPUCacheResult* result) { uint32_t baseLen = path.length; FF_AUTO_CLOSE_DIR DIR* pathCpuDir = opendir(path.chars); if (!pathCpuDir) { + FF_DEBUG("opendir(\"%s\") failed: %s", path.chars, strerror(errno)); return "opendir(\"/sys/devices/system/cpu/\") == nullptr"; } diff --git a/src/detection/cpucache/cpucache_windows.c b/src/detection/cpucache/cpucache_windows.c index 0bfc579821..8aee4d47e7 100644 --- a/src/detection/cpucache/cpucache_windows.c +++ b/src/detection/cpucache/cpucache_windows.c @@ -1,4 +1,5 @@ #include "cpucache.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include "common/windows/nt.h" @@ -7,13 +8,16 @@ const char* ffDetectCPUCache(FFCPUCacheResult* result) { DWORD length = 0; NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), nullptr, 0, &length); if (length == 0) { + FF_DEBUG("GetLogicalProcessorInformationEx(RelationCache, nullptr, &length) failed"); return "GetLogicalProcessorInformationEx(RelationCache, nullptr, &length) failed"; } FF_AUTO_FREE SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* pProcessorInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) malloc(length); - if (!NT_SUCCESS(NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length))) { + NTSTATUS status = NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length); + if (!NT_SUCCESS(status)) { + FF_DEBUG("GetLogicalProcessorInformationEx(RelationCache, pProcessorInfo, &length) failed: %s", ffDebugNtStatus(status)); return "GetLogicalProcessorInformationEx(RelationCache, pProcessorInfo, &length) failed"; } diff --git a/src/detection/cpuusage/cpuusage_apple.c b/src/detection/cpuusage/cpuusage_apple.c index bdec58aa1b..8081b8cfa6 100644 --- a/src/detection/cpuusage/cpuusage_apple.c +++ b/src/detection/cpuusage/cpuusage_apple.c @@ -1,4 +1,5 @@ #include "fastfetch.h" +#include "common/debug.h" #include "detection/cpuusage/cpuusage.h" #include @@ -11,9 +12,11 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { mach_msg_type_number_t numCpuInfo; if (host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUs, &cpuInfo, &numCpuInfo) != KERN_SUCCESS) { + FF_DEBUG("host_processor_info() failed"); return "host_processor_info() failed"; } if (numCPUs * CPU_STATE_MAX != numCpuInfo) { + FF_DEBUG("Unexpected host_processor_info() result"); return "Unexpected host_processor_info() result"; } diff --git a/src/detection/cpuusage/cpuusage_bsd.c b/src/detection/cpuusage/cpuusage_bsd.c index d3c43345c2..96fc977e44 100644 --- a/src/detection/cpuusage/cpuusage_bsd.c +++ b/src/detection/cpuusage/cpuusage_bsd.c @@ -1,10 +1,13 @@ #include "detection/cpuusage/cpuusage.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include #include #include #include +#include +#include #if __OpenBSD__ || __NetBSD__ #include @@ -19,10 +22,12 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { int ctls[] = { CTL_KERN, KERN_CP_TIME }; #endif if (sysctl(ctls, 2, nullptr, &neededLength, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_CPTIME}, 2, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_CPTIME}, 2, nullptr) failed"; } #else if (sysctlbyname("kern.cp_times", nullptr, &neededLength, nullptr, 0) != 0) { + FF_DEBUG("sysctlbyname(kern.cp_times, nullptr) failed: %s", strerror(errno)); return "sysctlbyname(kern.cp_times, nullptr) failed"; } #endif @@ -34,10 +39,12 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { #if __OpenBSD__ || __NetBSD__ if (sysctl(ctls, 2, cpTimes, &neededLength, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_CPTIME}, 2, cpTimes) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_CPTIME}, 2, nullptr) failed"; } #else if (sysctlbyname("kern.cp_times", cpTimes, &neededLength, nullptr, 0) != 0) { + FF_DEBUG("sysctlbyname(kern.cp_times, cpTimes) failed: %s", strerror(errno)); return "sysctlbyname(kern.cp_times, cpTime) failed"; } #endif diff --git a/src/detection/cpuusage/cpuusage_haiku.c b/src/detection/cpuusage/cpuusage_haiku.c index 35fba5949e..7893ecbae3 100644 --- a/src/detection/cpuusage/cpuusage_haiku.c +++ b/src/detection/cpuusage/cpuusage_haiku.c @@ -1,5 +1,6 @@ #include "fastfetch.h" #include "detection/cpuusage/cpuusage.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include @@ -7,11 +8,13 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { system_info sysInfo; if (get_system_info(&sysInfo) != B_OK) { + FF_DEBUG("get_system_info() failed"); return "get_system_info() failed"; } FF_AUTO_FREE cpu_info* cpuInfo = malloc(sizeof(*cpuInfo) * sysInfo.cpu_count); if (get_cpu_info(0, sysInfo.cpu_count, cpuInfo) != B_OK) { + FF_DEBUG("get_cpu_info() failed"); return "get_cpu_info() failed"; } diff --git a/src/detection/cpuusage/cpuusage_linux.c b/src/detection/cpuusage/cpuusage_linux.c index e4711fff9c..9f2bbda3f7 100644 --- a/src/detection/cpuusage/cpuusage_linux.c +++ b/src/detection/cpuusage/cpuusage_linux.c @@ -1,5 +1,6 @@ #include "fastfetch.h" #include "detection/cpuusage/cpuusage.h" +#include "common/debug.h" #include "common/io.h" #include @@ -9,6 +10,7 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { char buf[PROC_FILE_BUFFSIZ]; ssize_t nRead = ffReadFileData("/proc/stat", ARRAY_SIZE(buf) - 1, buf); if (nRead < 0) { + FF_DEBUG("Reading \"/proc/stat\" failed: %s", strerror(errno)); #ifdef __ANDROID__ return "Accessing \"/proc/stat\" is restricted on Android O+"; #else @@ -20,6 +22,7 @@ const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { // Skip first line char* start = nullptr; if ((start = strchr(buf, '\n')) == nullptr) { + FF_DEBUG("skip first line failed"); return "skip first line failed"; } ++start; diff --git a/src/detection/cpuusage/cpuusage_sunos.c b/src/detection/cpuusage/cpuusage_sunos.c index a10f09ed73..48cdf8363d 100644 --- a/src/detection/cpuusage/cpuusage_sunos.c +++ b/src/detection/cpuusage/cpuusage_sunos.c @@ -1,4 +1,5 @@ #include "fastfetch.h" +#include "common/debug.h" #include "detection/cpuusage/cpuusage.h" #include @@ -14,6 +15,7 @@ static inline void kstatFreeWrap(kstat_ctl_t** pkc) { const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { [[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); if (!kc) { + FF_DEBUG("kstat_open() failed"); return "kstat_open() failed"; } diff --git a/src/detection/cpuusage/cpuusage_windows.c b/src/detection/cpuusage/cpuusage_windows.c index a208c40f44..ada71a4396 100644 --- a/src/detection/cpuusage/cpuusage_windows.c +++ b/src/detection/cpuusage/cpuusage_windows.c @@ -10,12 +10,16 @@ static const char* getInfoByNqsi(FFlist* cpuTimes) { ULONG size = 0; - if (NtQuerySystemInformation(SystemProcessorPerformanceInformation, nullptr, 0, &size) != STATUS_INFO_LENGTH_MISMATCH) { + NTSTATUS status = NtQuerySystemInformation(SystemProcessorPerformanceInformation, nullptr, 0, &size); + if (status != STATUS_INFO_LENGTH_MISMATCH) { + FF_DEBUG("NtQuerySystemInformation(SystemProcessorPerformanceInformation, nullptr) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessorPerformanceInformation, nullptr) failed"; } FF_AUTO_FREE SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION* pinfo = (SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION*) malloc(size); - if (!NT_SUCCESS(NtQuerySystemInformation(SystemProcessorPerformanceInformation, pinfo, size, &size))) { + status = NtQuerySystemInformation(SystemProcessorPerformanceInformation, pinfo, size, &size); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtQuerySystemInformation(SystemProcessorPerformanceInformation, size) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessorPerformanceInformation, size) failed"; } @@ -57,19 +61,24 @@ static const char* getInfoByPerflib(FFlist* cpuTimes) { .Name = PERF_WILDCARD_INSTANCE, }; - if (PerfOpenQueryHandle(nullptr, &hQuery) != ERROR_SUCCESS) { + DWORD ret = PerfOpenQueryHandle(nullptr, &hQuery); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfOpenQueryHandle() failed: %s", ffDebugWin32Error(ret)); PerfCloseQueryHandle(hQuery); hQuery = INVALID_HANDLE_VALUE; return "PerfOpenQueryHandle() failed"; } - if (PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)) != ERROR_SUCCESS) { + ret = PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfAddCounters() failed: %s", ffDebugWin32Error(ret)); PerfCloseQueryHandle(hQuery); hQuery = INVALID_HANDLE_VALUE; return "PerfAddCounters() failed"; } if (querySpec.Identifier.Status != ERROR_SUCCESS) { + FF_DEBUG("PerfAddCounters() reports invalid identifier: %s", ffDebugWin32Error(querySpec.Identifier.Status)); PerfCloseQueryHandle(hQuery); hQuery = INVALID_HANDLE_VALUE; return "PerfAddCounters() reports invalid identifier"; @@ -77,35 +86,44 @@ static const char* getInfoByPerflib(FFlist* cpuTimes) { } if (hQuery == INVALID_HANDLE_VALUE) { + FF_DEBUG("Init hQuery failed"); return "Init hQuery failed"; } DWORD dataSize = 0; - if (PerfQueryCounterData(hQuery, nullptr, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + DWORD ret = PerfQueryCounterData(hQuery, nullptr, 0, &dataSize); + if (ret != ERROR_NOT_ENOUGH_MEMORY) { + FF_DEBUG("PerfQueryCounterData(nullptr) failed: %s", ffDebugWin32Error(ret)); return "PerfQueryCounterData(nullptr) failed"; } if (dataSize <= sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER)) { + FF_DEBUG("instance doesn't exist"); return "instance doesn't exist"; } FF_AUTO_FREE PERF_DATA_HEADER* const pDataHeader = (PERF_DATA_HEADER*) malloc(dataSize); - if (PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize) != ERROR_SUCCESS) { + ret = PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize); + if (ret != ERROR_SUCCESS) { + FF_DEBUG("PerfQueryCounterData(pDataHeader) failed: %s", ffDebugWin32Error(ret)); return "PerfQueryCounterData(pDataHeader) failed"; } PERF_COUNTER_HEADER* pCounterHeader = (PERF_COUNTER_HEADER*) (pDataHeader + 1); if (pCounterHeader->dwType != PERF_COUNTERSET) { + FF_DEBUG("Invalid counter type"); return "Invalid counter type"; } PERF_MULTI_COUNTERS* pMultiCounters = (PERF_MULTI_COUNTERS*) (pCounterHeader + 1); if (pMultiCounters->dwCounters == 0) { + FF_DEBUG("No CPU counters found"); return "No CPU counters found"; } PERF_MULTI_INSTANCES* pMultiInstances = (PERF_MULTI_INSTANCES*) ((BYTE*) pMultiCounters + pMultiCounters->dwSize); if (pMultiInstances->dwInstances == 0) { + FF_DEBUG("No CPU instances found"); return "No CPU instances found"; } @@ -135,6 +153,7 @@ static const char* getInfoByPerflib(FFlist* cpuTimes) { if (wcschr(instanceName, L'_') == nullptr /* ignore `_Total` */) { if (processorUtility == UINT64_MAX) { + FF_DEBUG("Counter \"%% Processor Utility\" are not supported"); return "Counter \"% Processor Utility\" are not supported"; } diff --git a/src/detection/de/de_linux.c b/src/detection/de/de_linux.c index e93b604d94..6426a25b60 100644 --- a/src/detection/de/de_linux.c +++ b/src/detection/de/de_linux.c @@ -1,6 +1,7 @@ #include "de.h" #include "common/dbus.h" +#include "common/debug.h" #include "common/io.h" #include "common/library.h" #include "common/parsing.h" @@ -43,12 +44,14 @@ static const char* getGnomeByDbus([[maybe_unused]] FFstrbuf* result) { #ifdef FF_HAVE_DBUS FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != nullptr) { + FF_DEBUG("ffDBusLoadData() failed"); return "ffDBusLoadData() failed"; } ffDBusGetPropertyString(&dbus, "org.gnome.Shell", "/org/gnome/Shell", "org.gnome.Shell", "ShellVersion", result); return nullptr; #else // FF_HAVE_DBUS + FF_DEBUG("ffDBusLoadData() failed: dbus support not compiled in"); return "ffDBusLoadData() failed: dbus support not compiled in"; #endif // FF_HAVE_DBUS } @@ -103,6 +106,7 @@ static const char* getXfce4ByLib(FFstrbuf* result) { return nullptr; #else FF_UNUSED(result); + FF_DEBUG("dlopen is disabled"); return "dlopen is disabled"; #endif } @@ -164,6 +168,7 @@ static const char* getTrinity(FFstrbuf* result, [[maybe_unused]] FFDEOptions* op FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("tde-config", &path); if (error) { + FF_DEBUG("Failed to find tde-config path"); return "Failed to find tde-config path"; } @@ -180,6 +185,7 @@ static const char* getTrinity(FFstrbuf* result, [[maybe_unused]] FFDEOptions* op return nullptr; } + FF_DEBUG("All methods failed"); return "All methods failed"; } @@ -197,6 +203,7 @@ static const char* getCosmic(FFstrbuf* result, [[maybe_unused]] FFDEOptions* opt return nullptr; } + FF_DEBUG("All methods failed"); return "All methods failed"; } @@ -204,27 +211,32 @@ static const char* getEnlightenmentByDbus([[maybe_unused]] FFstrbuf* result) { #ifdef FF_HAVE_DBUS FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != nullptr) { + FF_DEBUG("ffDBusLoadData() failed"); return "ffDBusLoadData() failed"; } DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.enlightenment.wm.service", "/org/enlightenment/wm/RemoteObject", "org.enlightenment.wm.Core", "Version", nullptr, nullptr); if (!reply) { + FF_DEBUG("ffDBusGetMethodReply() failed"); return "ffDBusGetMethodReply() failed"; } DBusMessageIter rootIterator; if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) { dbus.lib->ffdbus_message_unref(reply); + FF_DEBUG("dbus_message_iter_init() failed"); return "dbus_message_iter_init() failed"; } if (!ffDBusGetString(&dbus, &rootIterator, result)) { dbus.lib->ffdbus_message_unref(reply); + FF_DEBUG("ffDBusGetString() failed"); return "ffDBusGetString() failed"; } dbus.lib->ffdbus_message_unref(reply); return nullptr; #else // FF_HAVE_DBUS + FF_DEBUG("ffDBusLoadData() failed: dbus support not compiled in"); return "ffDBusLoadData() failed: dbus support not compiled in"; #endif // FF_HAVE_DBUS } @@ -241,6 +253,7 @@ static void getEnlightenment(FFstrbuf* result, [[maybe_unused]] FFDEOptions* opt } const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOptions* options) { if (!instance.config.general.detectVersion) { + FF_DEBUG("Disabled by config"); return "Disabled by config"; } @@ -270,6 +283,7 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_ENLIGHTENMENT)) { getEnlightenment(result, options); } else { + FF_DEBUG("Unsupported DE"); return "Unsupported DE"; } return nullptr; diff --git a/src/detection/disk/disk.c b/src/detection/disk/disk.c index b1ad006bd4..237681e7f3 100644 --- a/src/detection/disk/disk.c +++ b/src/detection/disk/disk.c @@ -22,8 +22,6 @@ const char* ffDetectDisks(FFDiskOptions* options, FFlist* disks) { FF_LIST_FOR_EACH (FFDisk, disk, *disks) { if (disk->bytesTotal == 0) { disk->type |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT; - } else { - disk->bytesUsed = disk->bytesTotal - (options->calcType == FF_DISK_CALC_TYPE_FREE ? disk->bytesFree : disk->bytesAvailable); } } diff --git a/src/detection/disk/disk.h b/src/detection/disk/disk.h index 57aaf2f22c..2dea21e8d5 100644 --- a/src/detection/disk/disk.h +++ b/src/detection/disk/disk.h @@ -16,9 +16,7 @@ typedef struct FFDisk { FFstrbuf name; FFDiskVolumeType type; - uint64_t bytesUsed; uint64_t bytesFree; - uint64_t bytesAvailable; uint64_t bytesTotal; uint32_t filesUsed; diff --git a/src/detection/disk/disk_bsd.c b/src/detection/disk/disk_bsd.c index aaa69b294a..d57f5b7d1a 100644 --- a/src/detection/disk/disk_bsd.c +++ b/src/detection/disk/disk_bsd.c @@ -1,7 +1,9 @@ #include "disk.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include "common/strutil.h" +#include #include #include #include @@ -28,16 +30,19 @@ static const char* detectFsLabel(struct statfs* fs, FFDisk* disk) { static struct gclass* cLabels; if (!cLabels) { if (geomTree.lg_ident) { + FF_DEBUG("Previous geom_gettree() failed"); return "Previous geom_gettree() failed"; } if (geom_gettree(&geomTree) < 0) { + FF_DEBUG("geom_gettree() failed"); geomTree.lg_ident = (void*) (intptr_t) -1; return "geom_gettree() failed"; } for (cLabels = geomTree.lg_class.lh_first; cLabels && !ffStrEquals(cLabels->lg_name, "LABEL"); cLabels = cLabels->lg_class.le_next); if (!cLabels) { + FF_DEBUG("Class LABEL is not found"); return "Class LABEL is not found"; } } @@ -129,11 +134,13 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { #ifndef __NetBSD__ int size = getfsstat(nullptr, 0, MNT_WAIT); if (size <= 0) { + FF_DEBUG("getfsstat(nullptr, 0, MNT_WAIT) failed: %s", strerror(errno)); return "getfsstat(nullptr, 0, MNT_WAIT) failed"; } #else int size = getvfsstat(nullptr, 0, ST_WAIT); if (size <= 0) { + FF_DEBUG("getvfsstat(nullptr, 0, ST_WAIT) failed: %s", strerror(errno)); return "getvfsstat(nullptr, 0, ST_WAIT) failed"; } #endif @@ -141,10 +148,12 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { FF_AUTO_FREE struct statfs* buf = malloc(sizeof(*buf) * (unsigned) size); #ifndef __NetBSD__ if (getfsstat(buf, (int) (sizeof(*buf) * (unsigned) size), MNT_NOWAIT) <= 0) { + FF_DEBUG("getfsstat(buf, size, MNT_NOWAIT) failed: %s", strerror(errno)); return "getfsstat(buf, size, MNT_NOWAIT) failed"; } #else if (getvfsstat(buf, sizeof(*buf) * (unsigned) size, ST_NOWAIT) <= 0) { + FF_DEBUG("getvfsstat(buf, size, ST_NOWAIT) failed: %s", strerror(errno)); return "getvfsstat(buf, size, ST_NOWAIT) failed"; } #endif @@ -179,9 +188,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); disk->bytesTotal = (uint64_t) fs->f_blocks * (uint64_t) fs->f_bsize; - disk->bytesFree = (uint64_t) fs->f_bfree * (uint64_t) fs->f_bsize; - disk->bytesAvailable = (uint64_t) fs->f_bavail * (uint64_t) fs->f_bsize; - disk->bytesUsed = 0; // To be filled in ./disk.c + disk->bytesFree = (uint64_t) (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs->f_bfree : fs->f_bavail) * (uint64_t) fs->f_bsize; disk->filesTotal = (uint32_t) fs->f_files; disk->filesUsed = (uint32_t) fs->f_files - (uint32_t) fs->f_ffree; diff --git a/src/detection/disk/disk_haiku.cpp b/src/detection/disk/disk_haiku.cpp index bce8932d6b..ad277ba88e 100644 --- a/src/detection/disk/disk_haiku.cpp +++ b/src/detection/disk/disk_haiku.cpp @@ -15,6 +15,10 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { continue; } + if (!ffStrSet(fs.device_name)) { + continue; + } + node_ref node(fs.dev, fs.root); BDirectory dir(&node); BPath path(&dir); @@ -40,8 +44,6 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { disk->bytesTotal = (uint64_t) fs.total_blocks * (uint64_t) fs.block_size; disk->bytesFree = (uint64_t) fs.free_blocks * (uint64_t) fs.block_size; - disk->bytesAvailable = disk->bytesFree; - disk->bytesUsed = 0; // To be filled in ./disk.c disk->filesTotal = (uint32_t) fs.total_nodes; disk->filesUsed = (uint32_t) (fs.total_nodes - fs.free_nodes); diff --git a/src/detection/disk/disk_linux.c b/src/detection/disk/disk_linux.c index bb1a695124..59c88d930a 100644 --- a/src/detection/disk/disk_linux.c +++ b/src/detection/disk/disk_linux.c @@ -1,5 +1,6 @@ #include "disk.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -248,16 +249,14 @@ static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mntent* #endif -static void detectStats(FFDisk* disk) { +static void detectStats(FFDiskOptions* options, FFDisk* disk) { struct statvfs fs; if (statvfs(disk->mountpoint.chars, &fs) != 0) { memset(&fs, 0, sizeof(fs)); // Set all values to 0, so our values get initialized to 0 too } disk->bytesTotal = fs.f_blocks * (uint64_t) fs.f_frsize; - disk->bytesFree = fs.f_bfree * (uint64_t) fs.f_frsize; - disk->bytesAvailable = fs.f_bavail * (uint64_t) fs.f_frsize; - disk->bytesUsed = 0; // To be filled in ./disk.c + disk->bytesFree = (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs.f_bfree : fs.f_bavail) * (uint64_t) fs.f_frsize; if (fs.f_files >= fs.f_ffree) { disk->filesTotal = (uint32_t) fs.f_files; @@ -285,6 +284,7 @@ static void detectStats(FFDisk* disk) { const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { FILE* mountsFile = setmntent("/proc/mounts", "r"); if (mountsFile == nullptr) { + FF_DEBUG("setmntent(\"/proc/mounts\", \"r\") failed: %s", strerror(errno)); return "setmntent(\"/proc/mounts\", \"r\") == nullptr"; } @@ -328,7 +328,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { detectType(disks, disk, device); // Detects stats - detectStats(disk); + detectStats(options, disk); } endmntent(mountsFile); diff --git a/src/detection/disk/disk_sunos.c b/src/detection/disk/disk_sunos.c index 85087fe7bb..bd56aaf44d 100644 --- a/src/detection/disk/disk_sunos.c +++ b/src/detection/disk/disk_sunos.c @@ -85,16 +85,14 @@ static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mnttab* } } -static void detectStats(FFDisk* disk) { +static void detectStats(FFDiskOptions* options, FFDisk* disk) { struct statvfs fs; if (statvfs(disk->mountpoint.chars, &fs) != 0) { memset(&fs, 0, sizeof(fs)); } disk->bytesTotal = fs.f_blocks * fs.f_frsize; - disk->bytesFree = fs.f_bfree * fs.f_frsize; - disk->bytesAvailable = fs.f_bavail * fs.f_frsize; - disk->bytesUsed = 0; // To be filled in ./disk.c + disk->bytesFree = (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs.f_bfree : fs.f_bavail) * fs.f_frsize; disk->filesTotal = (uint32_t) fs.f_files; disk->filesUsed = (uint32_t) (disk->filesTotal - fs.f_ffree); @@ -138,7 +136,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { ffStrbufInitS(&disk->filesystem, device.mnt_fstype); ffStrbufInit(&disk->name); detectType(disks, disk, &device); - detectStats(disk); + detectStats(options, disk); } return nullptr; diff --git a/src/detection/disk/disk_windows.c b/src/detection/disk/disk_windows.c index c5093fed2f..2c4ed2e5b5 100644 --- a/src/detection/disk/disk_windows.c +++ b/src/detection/disk/disk_windows.c @@ -1,4 +1,5 @@ #include "disk.h" +#include "common/debug.h" #include "common/io.h" #include "common/time.h" #include "common/windows/unicode.h" @@ -12,7 +13,9 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { PROCESS_DEVICEMAP_INFORMATION_EX info = {}; ULONG size = 0; - if (!NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(), ProcessDeviceMap, &info, sizeof(info), &size))) { + NTSTATUS status = NtQueryInformationProcess(NtCurrentProcess(), ProcessDeviceMap, &info, sizeof(info), &size); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtQueryInformationProcess(ProcessDeviceMap) failed: %s", ffDebugNtStatus(status)); return "NtQueryInformationProcess(ProcessDeviceMap) failed"; } @@ -72,8 +75,6 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { disk->filesTotal = 0; disk->bytesTotal = 0; disk->bytesFree = 0; - disk->bytesUsed = 0; // To be filled in ./disk.c - disk->bytesAvailable = 0; disk->createTime = 0; ffStrbufInit(&disk->filesystem); ffStrbufInit(&disk->name); @@ -115,12 +116,21 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { } } - FILE_FS_FULL_SIZE_INFORMATION fsFullSize; - if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) { - uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit; - disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units; - disk->bytesFree = (uint64_t) fsFullSize.ActualAvailableAllocationUnits.QuadPart * units; - disk->bytesAvailable = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units; + if (options->calcType == FF_DISK_CALC_TYPE_AVAILABLE || !ffIsFileFsFullSizeInformationExAvailable()) { + FILE_FS_FULL_SIZE_INFORMATION fsFullSize; + if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) { + uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit; + disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units; + disk->bytesFree = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units; + } + } else { + FILE_FS_FULL_SIZE_INFORMATION_EX fsFullSizeEx; + if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSizeEx, sizeof(fsFullSizeEx), FileFsFullSizeInformationEx))) { + // Space info without considering the quota setting + uint64_t units = fsFullSizeEx.BytesPerSector * fsFullSizeEx.SectorsPerAllocationUnit; + disk->bytesTotal = (uint64_t) fsFullSizeEx.ActualTotalAllocationUnits * units; + disk->bytesFree = (uint64_t) fsFullSizeEx.ActualAvailableAllocationUnits * units; + } } } diff --git a/src/detection/diskio/diskio_apple.c b/src/detection/diskio/diskio_apple.c index c389cb2c4d..2cb2b9cb64 100644 --- a/src/detection/diskio/diskio_apple.c +++ b/src/detection/diskio/diskio_apple.c @@ -1,4 +1,5 @@ #include "diskio.h" +#include "common/debug.h" #include "common/apple/cf_helpers.h" #include @@ -11,6 +12,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = 0; if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(kIOBlockStorageDriverClass), &iterator) != KERN_SUCCESS) { + FF_DEBUG("IOServiceGetMatchingServices() failed"); return "IOServiceGetMatchingServices() failed"; } diff --git a/src/detection/diskio/diskio_bsd.c b/src/detection/diskio/diskio_bsd.c index 495d1a95a7..c47503fb1b 100644 --- a/src/detection/diskio/diskio_bsd.c +++ b/src/detection/diskio/diskio_bsd.c @@ -2,6 +2,7 @@ #if __has_include() + #include "common/debug.h" #include "common/strutil.h" #include @@ -11,18 +12,24 @@ #include #include + #include + #include + const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { [[gnu::cleanup(geom_deletetree)]] struct gmesh geomTree = {}; if (geom_gettree(&geomTree) < 0) { + FF_DEBUG("geom_gettree() failed: %s", strerror(errno)); return "geom_gettree() failed"; } if (geom_stats_open() < 0) { + FF_DEBUG("geom_stats_open() failed: %s", strerror(errno)); return "geom_stats_open() failed"; } void* snap = geom_stats_snapshot_get(); if (!snap) { + FF_DEBUG("geom_stats_snapshot_get() returned null"); return "geom_stats_snapshot_get() failed"; } @@ -74,11 +81,15 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { #else + #include "common/debug.h" + #include #include + #include const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { if (checkversion() < 0) { + FF_DEBUG("checkversion() failed: %s", strerror(errno)); return "checkversion() failed"; } @@ -86,6 +97,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { .dinfo = (struct devinfo*) calloc(1, sizeof(struct devinfo)), }; if (getdevs(&stats) < 0) { + FF_DEBUG("getdevs() failed: %s", strerror(errno)); return "getdevs() failed"; } diff --git a/src/detection/diskio/diskio_linux.c b/src/detection/diskio/diskio_linux.c index 327e75b6d2..6a07e75bc0 100644 --- a/src/detection/diskio/diskio_linux.c +++ b/src/detection/diskio/diskio_linux.c @@ -1,4 +1,5 @@ #include "diskio.h" +#include "common/debug.h" #include "common/io.h" #include "common/properties.h" #include "common/strutil.h" @@ -56,10 +57,12 @@ static const char* parseDiskIOCounters(int dfd, const char* devName, FFlist* res char sysBlockStat[PROC_FILE_BUFFSIZ]; ssize_t fileSize = ffReadFileDataRelative(dfd, "stat", ARRAY_SIZE(sysBlockStat) - 1, sysBlockStat); if (fileSize <= 0) { + FF_DEBUG("failed to read stat file"); return "failed to read stat file"; } sysBlockStat[fileSize] = '\0'; if (sscanf(sysBlockStat, "%" PRIu64 "%*u%" PRIu64 "%*u%" PRIu64 "%*u%" PRIu64 "%*u", &nRead, §orRead, &nWritten, §orWritten) <= 0) { + FF_DEBUG("invalid stat file format"); return "invalid stat file format"; } } @@ -78,6 +81,7 @@ static const char* parseDiskIOCounters(int dfd, const char* devName, FFlist* res const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { FF_AUTO_CLOSE_DIR DIR* sysBlockDirp = opendir("/sys/block/"); if (sysBlockDirp == nullptr) { + FF_DEBUG("opendir(\"/sys/block/\") failed: %s", strerror(errno)); return "opendir(\"/sys/block/\") == nullptr"; } diff --git a/src/detection/diskio/diskio_nbsd.c b/src/detection/diskio/diskio_nbsd.c index c8835bf065..22eba546a6 100644 --- a/src/detection/diskio/diskio_nbsd.c +++ b/src/detection/diskio/diskio_nbsd.c @@ -1,8 +1,12 @@ #include "diskio.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" #include "common/mallocHelper.h" +#include +#include + #include #include @@ -10,6 +14,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { int mib[] = { CTL_HW, HW_IOSTATS, sizeof(struct io_sysctl) }; size_t len; if (sysctl(mib, ARRAY_SIZE(mib), nullptr, &len, nullptr, 0) < 0) { + FF_DEBUG("sysctl({HW_IOSTATS}, nullptr) failed: %s", strerror(errno)); return "sysctl({HW_IOSTATS}, nullptr) failed"; } uint32_t nDrive = (uint32_t) (len / sizeof(struct io_sysctl)); @@ -17,6 +22,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { FF_AUTO_FREE struct io_sysctl* stats = malloc(len); if (sysctl(mib, ARRAY_SIZE(mib), stats, &len, nullptr, 0) < 0) { + FF_DEBUG("sysctl({HW_IOSTATS}, stats) failed: %s", strerror(errno)); return "sysctl({HW_IOSTATS}, stats) failed"; } diff --git a/src/detection/diskio/diskio_obsd.c b/src/detection/diskio/diskio_obsd.c index c367dcb988..e3d539a37b 100644 --- a/src/detection/diskio/diskio_obsd.c +++ b/src/detection/diskio/diskio_obsd.c @@ -1,7 +1,9 @@ #include "diskio.h" +#include "common/debug.h" #include "common/strutil.h" #include "common/mallocHelper.h" +#include #include #include @@ -9,6 +11,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { int mib[] = { CTL_HW, HW_DISKSTATS }; size_t len; if (sysctl(mib, ARRAY_SIZE(mib), nullptr, &len, nullptr, 0) < 0) { + FF_DEBUG("sysctl({HW_DISKSTATS}, nullptr) failed: %s", strerror(errno)); return "sysctl({HW_DISKSTATS}, nullptr) failed"; } uint32_t nDrive = (uint32_t) (len / sizeof(struct diskstats)); @@ -16,6 +19,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { FF_AUTO_FREE struct diskstats* stats = malloc(len); if (sysctl(mib, ARRAY_SIZE(mib), stats, &len, nullptr, 0) < 0) { + FF_DEBUG("sysctl({HW_DISKSTATS}, stats) failed: %s", strerror(errno)); return "sysctl({HW_DISKSTATS}, stats) failed"; } diff --git a/src/detection/diskio/diskio_sunos.c b/src/detection/diskio/diskio_sunos.c index c79e3daeab..d21744e182 100644 --- a/src/detection/diskio/diskio_sunos.c +++ b/src/detection/diskio/diskio_sunos.c @@ -1,4 +1,5 @@ #include "diskio.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -12,6 +13,7 @@ static inline void kstatFreeWrap(kstat_ctl_t** pkc) { const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { [[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); if (!kc) { + FF_DEBUG("kstat_open() failed"); return "kstat_open() failed"; } diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index e294e73f46..c0ff1a8ade 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,6 +1,9 @@ #include "displayserver.h" #include "common/android/api.h" +#include "common/android/binder.h" +#include "common/android/dex.h" #include "common/arrutil.h" +#include "common/debug.h" #include "common/settings.h" #include "common/strutil.h" #include "common/processing.h" @@ -8,49 +11,1461 @@ #include -static bool detectWithGetprop(FFDisplayServerResult* ds) { - // Only for MiUI - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); +// --------------------------------------------------------------------------------------------- +// The binder route +// --------------------------------------------------------------------------------------------- +// +// `cmd display get-displays` and `dumpsys display` used to be the routes here, and both are a child +// process -- that fork was all of what this module cost: it took 16.0-26.5 ms on the test device, +// where `cmd display get-displays` itself takes 9.2 ms and a bare fork 8.6 ms. `cmd` does not exist +// before Android 13 either, and `dumpsys` is behind android.permission.DUMP, so on Android 11 and 12 +// an app UID had no display at all. `dumpsys` is gone: `display` is the service it sat on, +// android.hardware.display.IDisplayManager, it lives in system_server, an app UID may call it, and +// one `getDisplayInfo(0)` answers the whole `DisplayInfo` the dump prints, field for field. With the +// fork gone the module took 2.8-3.2 ms where `cmd` took 16.0-26.5 ms, and Android 11 -- the release +// `cmd` could not serve at all -- has been read this way on a device. What is left is dominated by +// the dex read below, not by the transaction. +// +// `cmd display get-displays` stays behind the walk as a fallback, for the case where it is the dex +// read that fails rather than the transaction -- see the `cmd` section below the walk. +// +// What it costs is the parse. The reply is a Java Parcelable and its field set moves with every +// release and every vendor fork -- `DisplayInfo` writes 34 fields on Android 11, 42 on 13, 57 on 16 +// and 57 on 17 -- so neither a fixed offset nor a sequence written against one release survives. The +// sequence is read out of the device's own `framework.jar` instead, from the `iget` order of +// `DisplayInfo.writeToParcel`, and the walk below follows it: a field the release does not write is +// simply not in the sequence, which is what makes every one of them optional by construction. See +// common/android/dex.h for how the sequence is recovered and what it does not say. +// +// What the sequence does not say is how each field is framed: `writeString` and `writeString8` are +// both a `Ljava/lang/String;` on the wire and are not the same length. The type descriptor settles +// the primitives and both array shapes, a handful of names settle the rest, and every nested +// Parcelable is anchored on its class name. A layout this does not recognise fails the route rather +// than answering, which is what the `cmd` fallback behind it is for: a build whose layout is not one +// this parser reads reports nothing rather than a plausible number out of the middle of a different +// field, and `cmd` still has the record in text. +// +// Android 17 moved three of the things this parse hangs on, and all three are read the same way -- out +// of the running build's own jar, so nothing here is a table written against a release: +// +// * `address` arrived as a `DisplayAddress$StablePhysical` where every release up to 16 sent a +// `DisplayAddress$Physical`, and the new class writes the port beside the id -- two fields where +// the old one writes one. Both sequences are read and the class name on the wire picks between +// them, so Android 11 to 17 are all read by the walk below. +// * `frameRateVelocityMapping` is new and is a `writeTypedList`, a count and then one body per +// element, each behind the marker the typed writes use and with no class name anywhere. The +// element's own sequence says how long it is. +// * `DeviceProductInfo.mEdidStructureMetadata` is new and is a `writeTypedObject`: the same marker +// and the same absent class name, one body behind it. + +#define FF_DISPLAY_ANDROID_SERVICE "display" +#define FF_DISPLAY_ANDROID_DESCRIPTOR "android.hardware.display.IDisplayManager" + +// The jar the reply's classes live in, and their dex type descriptors. All of them are asked for in +// one lookup: resolving a descriptor is dominated by a pass over the dex type table, and one pass +// answers every request whose class is named in it -- see common/android/dex.h. +#define FF_DISPLAY_ANDROID_JAR "/system/framework/framework.jar" + +#define FF_DISPLAY_ANDROID_CLASS_INFO "Landroid/view/DisplayInfo;" +#define FF_DISPLAY_ANDROID_CLASS_ADDRESS "Landroid/view/DisplayAddress$Physical;" +#define FF_DISPLAY_ANDROID_CLASS_STABLE_ADDRESS "Landroid/view/DisplayAddress$StablePhysical;" +#define FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO "Landroid/hardware/display/DeviceProductInfo;" +#define FF_DISPLAY_ANDROID_CLASS_EDID_STRUCTURE "Landroid/hardware/display/DeviceProductInfo$EdidStructureMetadata;" +#define FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE "Landroid/hardware/display/DeviceProductInfo$ManufactureDate;" +#define FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES "Landroid/view/Display$HdrCapabilities;" +#define FF_DISPLAY_ANDROID_CLASS_MODE "Landroid/view/Display$Mode;" +#define FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE "Landroid/view/FrameRateCategoryRate;" +#define FF_DISPLAY_ANDROID_CLASS_VELOCITY_POINT "Landroid/view/FrameRateVelocityPoint;" +#define FF_DISPLAY_ANDROID_CLASS_CUTOUT "Landroid/view/DisplayCutout;" + +// The Java class names `writeParcelable` writes in front of each nested body, which is the anchor the +// walk hangs on. It writes the *runtime* class, so the abstract `DisplayAddress` a `DisplayInfo` +// declares arrives as whichever subclass the framework put there -- its `$Physical` up to Android 16 +// and its `$StablePhysical` from 17 on, which is why one field needs two names. +#define FF_DISPLAY_ANDROID_NAME_ADDRESS "android.view.DisplayAddress$Physical" +#define FF_DISPLAY_ANDROID_NAME_STABLE_ADDRESS "android.view.DisplayAddress$StablePhysical" +#define FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO "android.hardware.display.DeviceProductInfo" +#define FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE "android.hardware.display.DeviceProductInfo$ManufactureDate" +#define FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES "android.view.Display$HdrCapabilities" +#define FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE "android.view.FrameRateCategoryRate" + +// `IDisplayManager$Stub`, as the device's own dex declares it. `getDisplayInfo` is the first method +// and has been since the interface was introduced -- it is an `@UnsupportedAppUsage` API and that +// order has never moved -- and `getDisplayIds` is the second. They are not counted out of the AOSP +// source: a vendor fork inserts methods freely, and the seven a fork added on the test device all sit +// behind these two. +#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO 1u +#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS 2u + +// One display's reply is 2404 bytes on the test device, most of it the two mode lists, and it grows +// with the display. A reply that does not fit is a failure rather than a truncation. +#define FF_DISPLAY_ANDROID_REPLY_SIZE 8192 +#define FF_DISPLAY_ANDROID_PARCEL_SIZE 256 + +// How many displays are read at most. The reply buffer holds one display, so this only bounds the id +// list and the array the parsed records are held in until every one of them has parsed. +#define FF_DISPLAY_ANDROID_MAX_DISPLAYS 8 + +// How many fields each write sequence can hold. A sequence that does not fit is reported as empty +// rather than truncated, which fails the route -- half a sequence would walk half a record. See +// common/android/dex.h. +#define FF_DISPLAY_ANDROID_INFO_FIELDS 64 +#define FF_DISPLAY_ANDROID_NESTED_FIELDS 8 +#define FF_DISPLAY_ANDROID_MODE_FIELDS 12 + +// The two instance fields of `DisplayCutout` that lengthen its body, in the order the request writes +// their results. `mCutoutPathParserInfo` arrived in Android 13 and adds nine writes, `mSideOverrides` +// in 16 and adds one; Android 11 declares neither. +typedef enum FFDisplayAndroidCutoutField : uint32_t { + FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO, + FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES, + FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT, +} FFDisplayAndroidCutoutField; + +static const char* const ffDisplayAndroidCutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT] = { + "mCutoutPathParserInfo", + "mSideOverrides", +}; + +// The write sequence of every class the reply is made of, read out of the device's own jar. It is +// what turns the parse from a sequence written against one release into a walk of the release that +// is running. +typedef struct FFDisplayAndroidLayout { + FFDexParcelField displayInfo[FF_DISPLAY_ANDROID_INFO_FIELDS]; + uint32_t displayInfoCount; + FFDexParcelField address[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t addressCount; + FFDexParcelField stableAddress[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t stableAddressCount; + FFDexParcelField productInfo[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t productInfoCount; + FFDexParcelField edidStructureMetadata[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t edidStructureMetadataCount; + FFDexParcelField manufactureDate[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t manufactureDateCount; + FFDexParcelField hdrCapabilities[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t hdrCapabilitiesCount; + FFDexParcelField mode[FF_DISPLAY_ANDROID_MODE_FIELDS]; + uint32_t modeCount; + FFDexParcelField frameRateCategoryRate[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t frameRateCategoryRateCount; + FFDexParcelField velocityPoint[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t velocityPointCount; + + // `DisplayCutout` is the one class whose body is not field-driven -- `writeCutoutToParcel` reads + // everything through compiler-synthesised accessors, so there is no `iget` to walk. What says how + // long its body is, is which fields the class declares, and that is what is asked for here. They + // are one array rather than two members because the request writes them as a run. + bool cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT]; +} FFDisplayAndroidLayout; + +// Reads every sequence above, and `DisplayCutout`'s declared fields, out of the dex entries of +// `jarPath` -- in one lookup, because the walk behind it is what a lookup costs and the class data it +// reads along the way already holds everything asked for here. +// +// A class the jar does not declare leaves an empty sequence, and that is an answer rather than a +// failure for every class a release added part way through: `FrameRateCategoryRate` arrived in +// Android 15 and `StablePhysical`, `EdidStructureMetadata` and `FrameRateVelocityPoint` in 17, and a +// jar that does not declare one of them does not write the field either, so the walk never asks. +// What *is* required is the sequences a record cannot be walked without: `DisplayInfo` itself, a +// `DisplayAddress` -- either of the two shapes, since which one a build sends is what the name on the +// wire says -- a `DeviceProductInfo` and its date, the HDR capabilities and a mode. +// +// A jar that cannot be read at all fails too, which is one of the two cases the `cmd` fallback +// behind this route exists for. Which is the point of the dex being the source: a build this parser +// cannot read declines to answer, rather than reporting a plausible number read out of the middle of +// a different field. +static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layout) { + const FFDexParcelRequest requests[] = { + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_INFO, + .methodName = "writeToParcel", + .fields = layout->displayInfo, + .capacity = FF_DISPLAY_ANDROID_INFO_FIELDS, + .count = &layout->displayInfoCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_ADDRESS, + .methodName = "writeToParcel", + .fields = layout->address, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->addressCount, + }, + { + // Android 17's shape of the same field. A build that does not declare the class leaves the + // sequence empty, which is how the walk knows the field can only be the older one. + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_STABLE_ADDRESS, + .methodName = "writeToParcel", + .fields = layout->stableAddress, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->stableAddressCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO, + .methodName = "writeToParcel", + .fields = layout->productInfo, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->productInfoCount, + }, + { + // Android 17's addition to `DeviceProductInfo`. Same as above: an empty sequence means the + // release does not carry the field, and it is not in the product info walk either. + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_EDID_STRUCTURE, + .methodName = "writeToParcel", + .fields = layout->edidStructureMetadata, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->edidStructureMetadataCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE, + .methodName = "writeToParcel", + .fields = layout->manufactureDate, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->manufactureDateCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES, + .methodName = "writeToParcel", + .fields = layout->hdrCapabilities, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->hdrCapabilitiesCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MODE, + .methodName = "writeToParcel", + .fields = layout->mode, + .capacity = FF_DISPLAY_ANDROID_MODE_FIELDS, + .count = &layout->modeCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE, + .methodName = "writeToParcel", + .fields = layout->frameRateCategoryRate, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->frameRateCategoryRateCount, + }, + { + // Android 17's `frameRateVelocityMapping` is a list of these, and this is the sequence that + // says how long one element is. + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_VELOCITY_POINT, + .methodName = "writeToParcel", + .fields = layout->velocityPoint, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->velocityPointCount, + }, + { + // The one class whose body is not field-driven. `writeCutoutToParcel` reads everything + // through compiler-synthesised accessors, so there is no `iget` to walk and no class name + // on the wire either -- what says how long its body is, is which fields the class + // declares. Asking here rather than by a call of its own is the point: the walk that + // answers the seven sequences above is already holding this class's field list, and a + // second call would repeat it from the mapping down for 7.0 ms of a 23.5 ms lookup. + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_CUTOUT, + .instanceFieldNames = ffDisplayAndroidCutoutFields, + .instanceFieldResults = layout->cutoutFields, + .instanceFieldCount = ARRAY_SIZE(ffDisplayAndroidCutoutFields), + }, + }; + + const char* error = ffDexParcelFields(jarPath, requests, ARRAY_SIZE(requests)); + if (error != nullptr) { + FF_DEBUG("Reading the display layout out of %s failed: %s", jarPath, error); + return false; + } + if (layout->displayInfoCount == 0 || (layout->addressCount == 0 && layout->stableAddressCount == 0) + || layout->productInfoCount == 0 || layout->manufactureDateCount == 0 || layout->hdrCapabilitiesCount == 0 + || layout->modeCount == 0) { + FF_DEBUG("The classes a display is made of are not all declared in %s", jarPath); + return false; + } - if (ffSettingsGetAndroidProperty("persist.sys.miui_resolution", &buffer) && - ffStrbufContainC(&buffer, ',')) { - // 1440,3200,560 => width,height,densityDpi - uint32_t width = (uint32_t) ffStrbufToUInt(&buffer, 0); - ffStrbufSubstrAfterFirstC(&buffer, ','); - uint32_t height = (uint32_t) ffStrbufToUInt(&buffer, 0); - ffStrbufSubstrAfterFirstC(&buffer, ','); - uint32_t dpi = (uint32_t) ffStrbufToUInt(&buffer, 0) * 96 / 160; - FFDisplayResult* display = ffdsAppendDisplay(ds, - width, - height, - 0, - dpi, - 0, - 0, - 0, - 0, - nullptr, - FF_DISPLAY_TYPE_BUILTIN, - false, - 0, - 0, - 0, - "getprop"); - return !!display; + FF_DEBUG( + "Display layout: %u fields in DisplayInfo, %u in a mode, address %s, product info %u + edid %u, velocity point %u, cutout path parser info %s, side overrides %s", + layout->displayInfoCount, layout->modeCount, + layout->addressCount > 0 ? (layout->stableAddressCount > 0 ? "physical and stable" : "physical") : "stable", + layout->productInfoCount, layout->edidStructureMetadataCount, layout->velocityPointCount, + layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO] ? "yes" : "no", + layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES] ? "yes" : "no" + ); + return true; +} + +// `DisplayInfo.type`, i.e. `Display.TYPE_*`. A dump prints these as `INTERNAL` / `EXTERNAL` / +// `WIFI`; the numbers are what the parcel carries. +typedef enum FFDisplayAndroidType : int32_t { + FF_DISPLAY_ANDROID_TYPE_UNKNOWN = 0, + FF_DISPLAY_ANDROID_TYPE_INTERNAL = 1, + FF_DISPLAY_ANDROID_TYPE_EXTERNAL = 2, + FF_DISPLAY_ANDROID_TYPE_WIFI = 3, + FF_DISPLAY_ANDROID_TYPE_OVERLAY = 4, + FF_DISPLAY_ANDROID_TYPE_VIRTUAL = 5, +} FFDisplayAndroidType; + +// The `Parcel.writeValue` tags `DeviceProductInfo` uses. The framework writes one of these in front +// of the value; the numbers are its own. +typedef enum FFDisplayAndroidValue : int32_t { + FF_DISPLAY_ANDROID_VALUE_NULL = -1, + FF_DISPLAY_ANDROID_VALUE_STRING = 0, + FF_DISPLAY_ANDROID_VALUE_INTEGER = 1, + FF_DISPLAY_ANDROID_VALUE_PARCELABLE = 4, +} FFDisplayAndroidValue; + +// A cursor over one reply. Every read is bounds checked and every mismatch raises `failed` rather +// than returning a value: once a field has been read at the wrong place nothing behind it can be +// trusted, and the only safe answer left is to report nothing. +typedef struct FFDisplayAndroidReader { + const uint8_t* data; + size_t size; + size_t offset; + bool failed; +} FFDisplayAndroidReader; + +static bool readerSkip(FFDisplayAndroidReader* reader, size_t bytes) { + if (bytes > reader->size - reader->offset) { + reader->failed = true; + return false; + } + reader->offset += bytes; + return true; +} + +static int32_t readerI32(FFDisplayAndroidReader* reader) { + if (reader->offset + sizeof(int32_t) > reader->size) { + reader->failed = true; + return 0; + } + int32_t value = 0; + memcpy(&value, reader->data + reader->offset, sizeof(int32_t)); + reader->offset += sizeof(int32_t); + return value; +} + +static uint32_t readerU32(FFDisplayAndroidReader* reader) { + return (uint32_t) readerI32(reader); +} + +static uint64_t readerU64(FFDisplayAndroidReader* reader) { + if (reader->offset + sizeof(uint64_t) > reader->size) { + reader->failed = true; + return 0; + } + uint64_t value = 0; + memcpy(&value, reader->data + reader->offset, sizeof(uint64_t)); + reader->offset += sizeof(uint64_t); + return value; +} + +static float readerF32(FFDisplayAndroidReader* reader) { + const uint32_t raw = readerU32(reader); + float value = 0; + memcpy(&value, &raw, sizeof(value)); + return value; +} + +// A `writeBoolean`, which is an int32. Anything but 0 or 1 is a field read at the wrong place. +static bool readerBool(FFDisplayAndroidReader* reader) { + const int32_t value = readerI32(reader); + if (value != 0 && value != 1) { + reader->failed = true; + return false; + } + return value == 1; +} + +// Whether the string16 at `offset` holds `expected`, which is a class name. The length has to be the +// name's own and every code unit has to spell it, so a body that happens to start with a plausible +// length does not pass for one. +static bool readerClassNameAt(const FFDisplayAndroidReader* reader, size_t offset, const char* expected) { + const size_t length = strlen(expected); + if (length > 128 || offset + sizeof(int32_t) + (length + 1) * 2 > reader->size) { + return false; + } + int32_t declared = 0; + memcpy(&declared, reader->data + offset, sizeof(int32_t)); + if ((size_t) declared != length) { + return false; + } + const uint8_t* text = reader->data + offset + sizeof(int32_t); + for (size_t i = 0; i < length; ++i) { + if (text[i * 2] != (uint8_t) expected[i] || text[i * 2 + 1] != 0) { + return false; + } + } + return true; +} + +// The class name in front of a Parcelable body, which `writeParcelable` writes and which is the +// anchor this whole parser hangs on: a field that moved puts something else here, and a name that +// does not decode is how the parser finds out instead of reading a length out of the middle of a +// body. +static bool readerClassName(FFDisplayAndroidReader* reader, const char* expected) { + if (!readerClassNameAt(reader, reader->offset, expected)) { + reader->failed = true; + return false; + } + return readerSkip(reader, sizeof(int32_t) + (((strlen(expected) + 1) * 2 + 3) & ~(size_t) 3)); +} + +// A `writeString`, which is UTF-16 despite the name: an int32 count of code units, the code units, a +// NUL terminator and the padding behind it. A count of -1 is how the same field spells a null String +// and is not an error; `result` may be null where the text is only in the way. +static bool readerString16(FFDisplayAndroidReader* reader, FFstrbuf* result) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + return true; + } + const size_t payload = ((size_t) length * 2 + 2 + 3) & ~(size_t) 3; + if (payload > reader->size - reader->offset) { + reader->failed = true; + return false; + } + if (result != nullptr) { + for (int32_t i = 0; i < length; ++i) { + uint32_t codePoint = (uint32_t) reader->data[reader->offset + (size_t) i * 2] + | ((uint32_t) reader->data[reader->offset + (size_t) i * 2 + 1] << 8); + if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < length) { + // Anything outside the basic plane arrives as a surrogate pair, and the low half is + // what turns it back into one code point. A high surrogate without one is malformed + // and is passed through as it stands rather than guessed at. + const uint32_t low = (uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2] + | ((uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2 + 1] << 8); + if (low >= 0xDC00 && low <= 0xDFFF) { + codePoint = 0x10000u + ((codePoint - 0xD800u) << 10) + (low - 0xDC00u); + ++i; + } + } + ffStrbufAppendUtf32CodePoint(result, codePoint); + } + } + reader->offset += payload; + return true; +} + +// A `writeString8`, which is the one that really is UTF-8: an int32 *byte* count, that many bytes, a +// NUL and the padding. This is what `DisplayInfo.name` and `DisplayInfo.uniqueId` use -- the built-in +// display's name is 12 bytes of UTF-8 where the same text is 4 code units as a string16. +static bool readerString8(FFDisplayAndroidReader* reader, FFstrbuf* result) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + return true; + } + const size_t payload = ((size_t) length + 1 + 3) & ~(size_t) 3; + if (payload > reader->size - reader->offset) { + reader->failed = true; + return false; + } + if (result != nullptr) { + ffStrbufAppendNS(result, (uint32_t) length, (const char*) reader->data + reader->offset); + } + reader->offset += payload; + return true; +} + +// An array of primitives: a count and the values. A count of -1 is a null array, which carries none. +static bool readerSkipArray(FFDisplayAndroidReader* reader, size_t elementSize) { + const int32_t count = readerI32(reader); + if (reader->failed) { + return false; + } + if (count < 0) { + return true; + } + if (count > 1024) { + reader->failed = true; + return false; + } + return readerSkip(reader, (size_t) count * elementSize); +} + +// A `writeValue` whose value is an Integer, or a null which reads as zero. +static int32_t readerValueInteger(FFDisplayAndroidReader* reader) { + switch ((FFDisplayAndroidValue) readerI32(reader)) { + case FF_DISPLAY_ANDROID_VALUE_NULL: + return 0; + case FF_DISPLAY_ANDROID_VALUE_INTEGER: + return readerI32(reader); + default: + reader->failed = true; + return 0; + } +} + +// A `writeValue` whose value is a String, an Integer or a null. Nothing here wants the value; the +// shapes it does not know are what it refuses to guess a length for. +static bool readerSkipValue(FFDisplayAndroidReader* reader) { + switch ((FFDisplayAndroidValue) readerI32(reader)) { + case FF_DISPLAY_ANDROID_VALUE_NULL: + return true; + case FF_DISPLAY_ANDROID_VALUE_STRING: + return readerString16(reader, nullptr); + case FF_DISPLAY_ANDROID_VALUE_INTEGER: + (void) readerI32(reader); + return !reader->failed; + default: + reader->failed = true; + return false; + } +} + +// One field whose dex type descriptor settles how it is framed. `[I` and `[F` are the two array +// shapes the record carries and report their element count; the rest report their value. +typedef struct FFDisplayAndroidScalar { + int32_t i32; + float f32; + int32_t count; +} FFDisplayAndroidScalar; + +// Reads one field whose descriptor settles its framing -- an `I`, an `F`, a `J`, a `Z`, an `[I` or +// an `[F`. A descriptor with no rule here is a failure rather than a guess: `Ljava/lang/String;` +// alone can be a `writeString` or a `writeString8`, the two are not the same length on the wire, and +// stepping over it as if it were either would move every field behind it. +static bool readerScalar(FFDisplayAndroidReader* reader, const char* type, FFDisplayAndroidScalar* value) { + *value = (FFDisplayAndroidScalar) {}; + if (ffStrEquals(type, "I")) { + value->i32 = readerI32(reader); + return !reader->failed; + } + if (ffStrEquals(type, "F")) { + value->f32 = readerF32(reader); + return !reader->failed; } + if (ffStrEquals(type, "J")) { + (void) readerU64(reader); + return !reader->failed; + } + if (ffStrEquals(type, "Z")) { + value->i32 = readerBool(reader) ? 1 : 0; + return !reader->failed; + } + if (ffStrEquals(type, "[I") || ffStrEquals(type, "[F")) { + const int32_t count = readerI32(reader); + if (reader->failed || count > 1024) { + reader->failed = true; + return false; + } + // A negative count is a null array and carries no elements: `writeIntArray(null)` and + // `writeFloatArray(null)` both write a bare -1 where the count would be. Android 11 has one + // of these in the record -- `DeviceProductInfo.mRelativeAddress` is an `[I` there and is null + // on the built-in display. `readerSkipArray` spells the same rule for the arrays it steps + // over, and a null is a value everywhere else in this walk too. + if (count < 0) { + return true; + } + value->count = count; + return readerSkip(reader, (size_t) count * (ffStrEquals(type, "[I") ? sizeof(int32_t) : sizeof(float))); + } + reader->failed = true; + return false; +} +// The class name in front of a nested body, when which class it is decides how long the body is. +// `writeParcelable` writes the runtime class, so a field declared an abstract type arrives as +// whichever subclass the framework put there: `DisplayInfo.address` is a `DisplayAddress`, whose body +// is one field on `$Physical` and two on the `$StablePhysical` Android 17 added. The name is read +// first and the index of the one it matched is reported, so the caller walks the sequence that +// belongs to it -- and a name that matches none of them fails, because there is nothing else that +// says where the body ends. +static bool readerParcelableHeadAny(FFDisplayAndroidReader* reader, const char* const* names, uint32_t nameCount, uint32_t* index, bool* present) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + *present = false; + return true; + } + reader->offset -= sizeof(int32_t); + *present = true; + for (uint32_t i = 0; i < nameCount; ++i) { + if (readerClassNameAt(reader, reader->offset, names[i])) { + *index = i; + return readerClassName(reader, names[i]); + } + } + reader->failed = true; return false; } -// `cmd display get-displays` and `dumpsys display` print the same thing -- the `DisplayInfo` of every -// display, one per line -- so one parser covers both and only the command and the marker in front of -// each record differ: +// The same head for the fields that carry one class only. A null field is a single -1 where the class +// name would be, which is a value and not a failure. +static bool readerParcelableHead(FFDisplayAndroidReader* reader, const char* expected, bool* present) { + uint32_t index = 0; + return readerParcelableHeadAny(reader, &expected, 1, &index, present); +} + +// A `Rect` written by `writeTypedObject` or by `writeTypedArray`: the non-null marker the typed write +// puts in front of the body and then four ints. A null is a marker and no ints behind it. +static bool readerRect(FFDisplayAndroidReader* reader) { + const int32_t marker = readerI32(reader); + if (reader->failed) { + return false; + } + return marker < 0 || readerSkip(reader, 4 * sizeof(int32_t)); +} + +// A nested Parcelable written by `writeTypedObject`, walked out of the sequence the jar gives for its +// own class. What is on the wire is a marker and the body, with no class name in front of it -- the +// reader is handed the `Creator` to build the value with, so the parcel does not have to carry one, +// and that is the whole difference between this and `writeParcelable`. The device's own +// `Parcel.writeTypedObject` writes a zero marker for a null and a one before every body, so anything +// else is a field read at the wrong place rather than a value. +static bool readerTypedObject(FFDisplayAndroidReader* reader, const FFDexParcelField* fields, uint32_t count) { + const int32_t marker = readerI32(reader); + if (reader->failed) { + return false; + } + if (marker == 0) { + return true; + } + if (marker != 1) { + reader->failed = true; + return false; + } + for (uint32_t i = 0; i < count; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, fields[i].type, &value)) { + return false; + } + } + return true; +} + +// A `writeTypedList`: a count and then one `writeTypedObject` per element, all of them the same class, +// which is why one sequence answers the whole list. A count of -1 is a null list and carries none. +static bool readerTypedObjectList(FFDisplayAndroidReader* reader, const FFDexParcelField* fields, uint32_t count) { + const int32_t size = readerI32(reader); + if (reader->failed) { + return false; + } + if (size < 0) { + return true; + } + // A non-empty list whose elements this parser has no sequence for is a failure rather than an empty + // walk: the elements are still on the wire, and stepping over none of them would put every field + // behind the list at the wrong place, which is worse than reporting nothing. + if (size > 64 || (size > 0 && count == 0)) { + reader->failed = true; + return false; + } + for (int32_t i = 0; i < size; ++i) { + if (!readerTypedObject(reader, fields, count)) { + return false; + } + } + return true; +} + +// `writeValue(mManufactureDate)`, the only Parcelable a `writeValue` carries here, and the one place +// the parcel does something the AOSP sources do not describe: on Android 13 and later the tag is +// followed by the size of everything behind it -- the class name and the body -- before the class +// name, which is why a plain `tag + class name` walk lands four bytes early; Android 11 has no size +// at all. Neither layout is assumed -- a value that decodes as the class name has no size, anything +// else is stepped over -- and where there is one the cursor is snapped to the end it promises, so a +// body this parser reads differently can not move the fields behind it. The size counts from its own +// end, not from its start. +static bool readerManufactureDate(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) { + // `mManufactureDate` is `@Nullable`, so a display with no date writes a bare `VAL_NULL` and + // nothing behind it -- and a date is not what the display is being read for. A missing one is + // skipped rather than failed: everything the module needs is still in the record, and dropping + // the display over an absent date would answer nothing at all. Only a tag that is neither a + // Parcelable nor a null fails, because that is a framing this walk does not know how to step over. + const int32_t tag = readerI32(reader); + if (tag == FF_DISPLAY_ANDROID_VALUE_NULL) { + *year = 0; + *week = 0; + return true; + } + if (tag != FF_DISPLAY_ANDROID_VALUE_PARCELABLE) { + reader->failed = true; + return false; + } + + size_t bodyEnd = 0; + if (!readerClassNameAt(reader, reader->offset, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) { + const int32_t declared = readerI32(reader); + if (declared <= 0) { + reader->failed = true; + return false; + } + bodyEnd = reader->offset + (size_t) declared; + } + if (!readerClassName(reader, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) { + return false; + } + + // `writeValue(mWeek)` and `writeValue(mYear)`, each an Integer or a null. Nothing is reported + // unless both are there, so a class that stops writing one leaves the date at nothing rather + // than at half of it. + uint16_t weekValue = 0; + uint16_t yearValue = 0; + for (uint32_t i = 0; i < layout->manufactureDateCount; ++i) { + const int32_t value = readerValueInteger(reader); + if (reader->failed || value < 0 || value > 3000) { + reader->failed = true; + return false; + } + if (ffStrEquals(layout->manufactureDate[i].name, "mWeek")) { + if (value > 53) { + reader->failed = true; + return false; + } + weekValue = (uint16_t) value; + } else if (ffStrEquals(layout->manufactureDate[i].name, "mYear")) { + yearValue = (uint16_t) value; + } + } + *week = weekValue; + *year = yearValue; + + if (bodyEnd > 0) { + if (bodyEnd < reader->offset || bodyEnd > reader->size) { + reader->failed = true; + return false; + } + reader->offset = bodyEnd; + } + return true; +} + +// `deviceProductInfo`, which is where a display's manufacture date is carried. Nothing else in it is +// used, but it sits in front of `name`, the density and the rotation, so it has to be walked whether +// or not its values are wanted -- and it is null on some displays. +// +// Three of its fields are handed to `writeValue`, which frames them by their runtime type rather than +// by their declared one: `mProductId` is declared a `String` and goes through `writeValue` where +// `mName` next to it goes through `writeString`, so those are named rather than dispatched by type. +// The connection type is `mRelativeAddress`, an int array, on Android 11 and `mConnectionToSinkType`, +// an int, on 13 and later; the descriptor settles that one, which is what makes the two read the same +// code. Android 17 added `mVideoInputType`, an int, which the descriptor settles the same way, and +// `mEdidStructureMetadata`, a `writeTypedObject` -- six fields up to 16, eight from 17 on. +static bool readerDeviceProductInfo(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) { + bool present = false; + if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO, &present)) { + return false; + } + if (!present) { + return true; + } + + for (uint32_t i = 0; i < layout->productInfoCount; ++i) { + const FFDexParcelField* field = &layout->productInfo[i]; + if (ffStrEquals(field->name, "mName") || ffStrEquals(field->name, "mManufacturerPnpId")) { + if (!readerString16(reader, nullptr)) { + return false; + } + } else if (ffStrEquals(field->name, "mProductId") || ffStrEquals(field->name, "mModelYear")) { + if (!readerSkipValue(reader)) { + return false; + } + } else if (ffStrEquals(field->name, "mManufactureDate")) { + if (!readerManufactureDate(reader, layout, year, week)) { + return false; + } + } else if (ffStrEquals(field->name, "mEdidStructureMetadata")) { + // Android 17. Named rather than left to the descriptor for the same reason the fields above + // are: `writeTypedObject` and `writeParcelable` are both a `L...;` on the wire and are not + // the same length, and this one is the typed write -- a marker, the body, no class name. + if (!readerTypedObject(reader, layout->edidStructureMetadata, layout->edidStructureMetadataCount)) { + return false; + } + } else { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + } + } + return true; +} + +// `address`, which is null on some displays and a `DisplayAddress` on others: its `$Physical` up to +// Android 16, whose body is one `long`, and its `$StablePhysical` from 17 on, whose body is that +// `long` and the port beside it. Which one is on the wire is what the class name says, and either +// sequence comes out of the jar rather than being assumed, so a release that changes either body is +// read rather than misread. +static bool readerDisplayAddress(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) { + static const char* const names[] = { + FF_DISPLAY_ANDROID_NAME_ADDRESS, + FF_DISPLAY_ANDROID_NAME_STABLE_ADDRESS, + }; + bool present = false; + uint32_t index = 0; + if (!readerParcelableHeadAny(reader, names, ARRAY_SIZE(names), &index, &present)) { + return false; + } + if (!present) { + return true; + } + const bool stable = index != 0; + const FFDexParcelField* fields = stable ? layout->stableAddress : layout->address; + const uint32_t count = stable ? layout->stableAddressCount : layout->addressCount; + for (uint32_t i = 0; i < count; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, fields[i].type, &value)) { + return false; + } + } + return true; +} + +// A nested Parcelable whose values this module does not use, walked because the fields behind it sit +// on the far side of it. `frameRateCategoryRate` is the only one of those left before `uniqueId`. +static bool readerSkippedParcelable(FFDisplayAndroidReader* reader, const char* expected, const FFDexParcelField* fields, uint32_t count) { + bool present = false; + if (!readerParcelableHead(reader, expected, &present)) { + return false; + } + if (!present) { + return true; + } + for (uint32_t i = 0; i < count; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, fields[i].type, &value)) { + return false; + } + } + return true; +} + +// `hdrCapabilities`, whose only value this module uses is how many HDR types the display supports -- +// an empty list is the display saying it can not do HDR at all. +static bool readerHdrCapabilities(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t* hdrTypeCount) { + bool present = false; + if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES, &present)) { + return false; + } + if (!present) { + return true; + } + for (uint32_t i = 0; i < layout->hdrCapabilitiesCount; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, layout->hdrCapabilities[i].type, &value)) { + return false; + } + if (ffStrEquals(layout->hdrCapabilities[i].name, "mSupportedHdrTypes")) { + if (value.count > 16) { + reader->failed = true; + return false; + } + *hdrTypeCount = value.count; + } + } + return true; +} + +// `displayCutout`, the largest nested structure in the record and the one that is easiest to get +// wrong: it is serialized by `DisplayCutout$ParcelableWrapper.writeCutoutToParcel`, because +// `DisplayCutout` has no `writeToParcel` of its own, and that method reads everything through +// `-$$Nest$` accessors the compiler synthesised -- so there is no `iget` to walk and no class name on +// the wire either, and the write sequence the jar gives for every other class says nothing here. +// +// What says how long the body is, is which fields the class declares. Android 11 declares three -- +// `mBounds`, `mSafeInsets`, `mWaterfallInsets` -- Android 13 adds `mCutoutPathParserInfo`, which is +// nine more writes, and Android 16 adds `mSideOverrides`, which is one more. Both are read from the +// jar rather than from a version number. None of it is reported; it is walked because `rotation` and +// everything behind it sit on the far side of it. +static bool readerDisplayCutout(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) { + const int32_t marker = readerI32(reader); + if (reader->failed) { + return false; + } + if (marker != 1) { + // 0 for a null cutout and -1 for `DisplayCutout.NO_CUTOUT`, which is a display with none at + // all. Anything else is a field read at the wrong place. + if (marker == 0 || marker == -1) { + return true; + } + reader->failed = true; + return false; + } + + if (!readerRect(reader)) { // mSafeInsets + return false; + } + const int32_t bounds = readerI32(reader); // mBounds, a typed array of Rects + if (reader->failed || bounds < 0 || bounds > 64) { + reader->failed = true; + return false; + } + for (int32_t i = 0; i < bounds; ++i) { + if (!readerRect(reader)) { + return false; + } + } + if (!readerRect(reader)) { // mWaterfallInsets + return false; + } + + const bool* cutout = layout->cutoutFields; + if (cutout[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO]) { + // the display size the cutout was built for and then the physical one + if (!readerSkip(reader, 4 * sizeof(int32_t))) { + return false; + } + const float density = readerF32(reader); + if (reader->failed || !(density > 0) || density > 1000) { + reader->failed = true; + return false; + } + // the cutout spec, e.g. `M -45,0 L -45,142 L 45,142 L 45,0 Z`, and then the rotation, the + // scale and the physical pixel ratio, which are three plain four byte fields + if (!readerString16(reader, nullptr) || !readerSkip(reader, 3 * sizeof(int32_t))) { + return false; + } + } + if (cutout[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES]) { + return readerSkipArray(reader, sizeof(int32_t)); // mSideOverrides + } + return true; +} + +// One entry of `supportedModes`, which is `Display$Mode`. +typedef struct FFDisplayAndroidMode { + int32_t id; + uint32_t width; + uint32_t height; + float refreshRate; +} FFDisplayAndroidMode; + +// Walks a `Display$Mode[]`: a count and then that many bare bodies, with no non-null marker per +// element, which is what `writeTypedArray` would put there -- the framework writes the length with +// `writeInt` and then one `writeToParcel` per element. The two ids the record carries are looked up +// as it goes, so the list does not have to be kept. +// +// The body is walked out of the jar's own sequence rather than read as a fixed run: `Display$Mode` +// writes four fields on Android 11, five on 13 and eight on 16, and the fourth -- the one the +// refresh rate is in -- is `mRefreshRate` on the first two and `mPeakRefreshRate` on the third. +static bool readerModeList(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t activeId, int32_t defaultId, FFDisplayAndroidMode* active, FFDisplayAndroidMode* preferred) { + const int32_t count = readerI32(reader); + if (reader->failed || count < 0 || count > 64) { + reader->failed = true; + return false; + } + for (int32_t i = 0; i < count; ++i) { + FFDisplayAndroidMode mode = {}; + for (uint32_t j = 0; j < layout->modeCount; ++j) { + const FFDexParcelField* field = &layout->mode[j]; + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + if (ffStrEquals(field->name, "mModeId")) { + mode.id = value.i32; + } else if (ffStrEquals(field->name, "mWidth")) { + mode.width = (uint32_t) value.i32; + } else if (ffStrEquals(field->name, "mHeight")) { + mode.height = (uint32_t) value.i32; + } else if (ffStrEquals(field->name, "mRefreshRate") || ffStrEquals(field->name, "mPeakRefreshRate")) { + mode.refreshRate = value.f32; + } + } + if (reader->failed + || mode.id < 0 || mode.width == 0 || mode.width > 32768 || mode.height == 0 || mode.height > 32768 + || !(mode.refreshRate > 0) || mode.refreshRate > 1000) { + reader->failed = true; + return false; + } + if (active != nullptr && mode.id == activeId) { + *active = mode; + } + if (preferred != nullptr && mode.id == defaultId) { + *preferred = mode; + } + } + return !reader->failed; +} + +// One parsed record. It is held until every display has parsed, because a record this parser does not +// recognise says the layout is not the one it was written for, and half an answer from a layout it +// does not understand is worse than falling back. +typedef struct FFDisplayAndroidInfo { + uint32_t width; + uint32_t height; + uint32_t preferredWidth; + uint32_t preferredHeight; + double preferredRefreshRate; + double refreshRate; + uint32_t density; + double physicalXDpi; + double physicalYDpi; + uint32_t rotation; + uint32_t displayId; + int32_t type; + int32_t modeId; + int32_t defaultModeId; + int32_t hdrTypeCount; + float renderFrameRate; + bool forceSdr; + uint16_t manufactureYear; + uint16_t manufactureWeek; + uint64_t id; + FFstrbuf name; +} FFDisplayAndroidInfo; + +// One `DisplayInfo` walk in progress: the record being filled, the two modes the mode list is +// resolved against, and the id string -- which is not part of the record, it is only read to pull the +// display's id out of it, so it is held here rather than there. +typedef struct FFDisplayAndroidWalk { + FFDisplayAndroidInfo* info; + FFstrbuf* uniqueId; + FFDisplayAndroidMode active; + FFDisplayAndroidMode preferred; +} FFDisplayAndroidWalk; + +// `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` for the panel +// and `virtual:...` for a virtual one. The prefix says what kind of display it is, and the number +// behind the colon is the id. +static uint64_t displayUniqueId(const char* uniqueId) { + const char* colon = strchr(uniqueId, ':'); + return (uint64_t) strtoull(colon != nullptr ? colon + 1 : uniqueId, nullptr, 10); +} + +// One field of the `DisplayInfo` write sequence. The name says which value the field carries; the +// type descriptor says how it is framed, and settles that for every primitive and both array shapes. +// +// Six things the descriptor does not settle, and each is named: +// +// * `address`, whose runtime class decides how long its body is. See readerDisplayAddress. +// * `displayCutout`, which has no write sequence to follow at all. See readerDisplayCutout. +// * `deviceProductInfo`, whose `mEdidStructureMetadata` is a typed write inside a named write. +// * `frameRateCategoryRate`, a Parcelable whose values this module does not use. +// * `frameRateVelocityMapping`, a `writeTypedList` -- a count and then a body per element, all of +// them the class the jar's own sequence was read for. Android 17 added it in place of the +// `appsSupportedModes` that 16 wrote, which was an array of Parcelables written by a loop rather +// than by a `write*Array`, and which `supportedModes` is still the shape of. +// * The `String` fields. `DisplayInfo` writes its own through `writeString8` -- a byte count and +// UTF-8 -- while `DeviceProductInfo` writes its through `writeString`, which is a code-unit count +// and UTF-16; the descriptor is `Ljava/lang/String;` either way. Within one class the framing is +// the class's habit rather than the field's, so a `String` this does not know is read the way the +// ones it does are, which is what a field a later release adds gets. +// +// A *name* this does not know is stepped over by its type, which is what makes a field a later +// release added cost its bytes and nothing else -- `displayGroupId`, `renderFrameRate`, +// `hasArrSupport`, `supportedRefreshRates`, `userPreferredModeId`, `appsSupportedModes`, `isForceSdr` +// and `committedState` are all of them. A *type* this does not know is a failure: the descriptor is +// the only thing that says how long the field is, and a `String` alone can be two different lengths. +static bool displayInfoField(FFDisplayAndroidReader* reader, const FFDexParcelField* field, const FFDisplayAndroidLayout* layout, FFDisplayAndroidWalk* walk) { + FFDisplayAndroidInfo* info = walk->info; + const char* name = field->name; + + if (ffStrEquals(name, "address")) { + return readerDisplayAddress(reader, layout); + } + if (ffStrEquals(name, "deviceProductInfo")) { + return readerDeviceProductInfo(reader, layout, &info->manufactureYear, &info->manufactureWeek); + } + if (ffStrEquals(name, "displayCutout")) { + return readerDisplayCutout(reader, layout); + } + if (ffStrEquals(name, "hdrCapabilities")) { + return readerHdrCapabilities(reader, layout, &info->hdrTypeCount); + } + if (ffStrEquals(name, "frameRateCategoryRate")) { + return readerSkippedParcelable(reader, FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE, layout->frameRateCategoryRate, layout->frameRateCategoryRateCount); + } + if (ffStrEquals(name, "frameRateVelocityMapping")) { + return readerTypedObjectList(reader, layout->velocityPoint, layout->velocityPointCount); + } + if (ffStrEquals(name, "supportedModes") || ffStrEquals(name, "appsSupportedModes")) { + // The two ids the list is resolved against are written in front of it on every release seen. + // One that moved behind it would leave the list resolved against nothing, so the walk says so + // rather than reporting the mode of whatever id happens to sit at the sentinel. + const bool wanted = ffStrEquals(name, "supportedModes"); + if (wanted && (info->modeId == INT32_MIN || info->defaultModeId == INT32_MIN)) { + reader->failed = true; + return false; + } + return readerModeList(reader, layout, info->modeId, info->defaultModeId, + wanted ? &walk->active : nullptr, wanted ? &walk->preferred : nullptr); + } + if (ffStrEquals(field->type, "Ljava/lang/String;")) { + if (ffStrEquals(name, "name")) { + return readerString8(reader, &info->name); + } + if (ffStrEquals(name, "uniqueId")) { + return readerString8(reader, walk->uniqueId); + } + return readerString8(reader, nullptr); // `ownerPackageName`, and anything a later release adds + } + + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + if (ffStrEquals(name, "type")) { + info->type = value.i32; + } else if (ffStrEquals(name, "displayId")) { + info->displayId = (uint32_t) value.i32; + } else if (ffStrEquals(name, "logicalWidth")) { + info->width = (uint32_t) value.i32; + } else if (ffStrEquals(name, "logicalHeight")) { + info->height = (uint32_t) value.i32; + } else if (ffStrEquals(name, "rotation")) { + info->rotation = (uint32_t) value.i32; + } else if (ffStrEquals(name, "modeId")) { + info->modeId = value.i32; + } else if (ffStrEquals(name, "defaultModeId")) { + info->defaultModeId = value.i32; + } else if (ffStrEquals(name, "renderFrameRate")) { + if (!(value.f32 > 0) || value.f32 > 1000) { + reader->failed = true; + return false; + } + info->renderFrameRate = value.f32; + } else if (ffStrEquals(name, "logicalDensityDpi")) { + info->density = (uint32_t) value.i32; + } else if (ffStrEquals(name, "physicalXDpi")) { + info->physicalXDpi = value.f32; + } else if (ffStrEquals(name, "physicalYDpi")) { + info->physicalYDpi = value.f32; + } else if (ffStrEquals(name, "isForceSdr")) { + info->forceSdr = value.i32 != 0; + } + return true; +} + +// How much of a reply a failure prints. A record is 2404 bytes on the test device and most of it is +// the two mode lists, so this covers one whole reply -- and the cap is there only so that a reply +// which is not one cannot fill the log. +#define FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE 4096 + +#ifndef NDEBUG +// The reply is read by shape, so a reply that fails leaves nothing behind to diagnose it with: the +// bytes are the only evidence. Each line carries the text beside the bytes, because what the walk +// hangs on is the class name in front of a nested body -- UTF-16, and the one thing that a length +// read out of the middle of a different field cannot be told from without looking at it. +static void debugDumpReply(const uint8_t* data, size_t size) { + const size_t limit = size < FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE ? size : FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE; + for (size_t offset = 0; offset < limit; offset += 16) { + char hex[16 * 3 + 1]; + char text[16 + 1]; + size_t used = 0; + for (size_t i = 0; i < 16 && offset + i < limit; ++i) { + used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]); + text[i] = data[offset + i] >= 32 && data[offset + i] < 127 ? (char) data[offset + i] : '.'; + text[i + 1] = '\0'; + } + FF_DEBUG(" +0x%04zx %-47s | %s", offset, hex, text); + } +} + #define FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size) +#else + #define FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size) ((void) 0) +#endif + +// One `getDisplayInfo` reply: the exception code the stub writes, the non-null marker +// `writeTypedObject` writes in front of the Parcelable, and the record itself -- walked in the order +// the device's own `DisplayInfo.writeToParcel` writes it, out of the sequence `displayLayoutLoad` +// read from that device's jar. +static bool displayInfoParse(const uint8_t* data, size_t size, const FFDisplayAndroidLayout* layout, FFDisplayAndroidInfo* info) { + FFDisplayAndroidReader reader = { .data = data, .size = size, .offset = 0, .failed = false }; + + const int32_t exception = readerI32(&reader); + if (exception != 0) { + FF_DEBUG("getDisplayInfo raised exception %d", exception); + return false; + } + if (readerI32(&reader) != 1) { + FF_DEBUG("getDisplayInfo answered no display"); + FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size); + return false; + } + + // Sentinels, so a field the release does not write can not be mistaken for one that was read as + // zero. Every one of these is in the sequence on every release seen and the check below is what + // says so; the two that are not -- `renderFrameRate` and `isForceSdr` -- have a defined fallback + // and are the only ones left out of it. + info->type = -1; + info->displayId = UINT32_MAX; + info->width = 0; + info->height = 0; + info->rotation = UINT32_MAX; + info->modeId = INT32_MIN; + info->defaultModeId = INT32_MIN; + info->hdrTypeCount = 0; + info->renderFrameRate = 0; + info->density = 0; + info->physicalXDpi = 0; + info->physicalYDpi = 0; + info->forceSdr = false; + + FF_STRBUF_AUTO_DESTROY uniqueId = ffStrbufCreate(); + FFDisplayAndroidWalk walk = { .info = info, .uniqueId = &uniqueId }; + + // `uniqueId` is the last field this module reads. Everything behind it -- `removeMode`, the + // brightness, the corners and shapes, the throttling -- would have to be walked for nothing, so + // the walk stops here rather than implementing four more nested shapes no value comes out of. + bool complete = false; + for (uint32_t i = 0; i < layout->displayInfoCount; ++i) { + const FFDexParcelField* field = &layout->displayInfo[i]; + if (!displayInfoField(&reader, field, layout, &walk)) { + // Which field, out of how many, what the jar says it is and how far the walk got before it + // gave up -- followed by the reply itself, which is what says whether the layout moved or + // this parser is reading a shape it was not written for. + FF_DEBUG( + "getDisplayInfo(%u) answered a %s (%s) this parser does not recognise -- field %u of %u, cursor at %zu of %zu", + info->displayId, field->name, field->type, i + 1, layout->displayInfoCount, reader.offset, reader.size + ); + FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size); + return false; + } + if (ffStrEquals(field->name, "uniqueId")) { + complete = true; + break; + } + } + if (!complete) { + FF_DEBUG("getDisplayInfo(%u) answered a record with no uniqueId", info->displayId); + FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size); + return false; + } + + if (info->type < 0 || info->type > FF_DISPLAY_ANDROID_TYPE_VIRTUAL + || info->displayId == UINT32_MAX + || info->width == 0 || info->width > 65536 || info->height == 0 || info->height > 65536 + || info->rotation > 3 + || info->modeId < 0 || info->defaultModeId < -1 + || info->density > 4000 + || !(info->physicalXDpi >= 0) || info->physicalXDpi > 2000 + || !(info->physicalYDpi >= 0) || info->physicalYDpi > 2000 + // A display with no name is still a display -- the key falls back to a number -- so only a + // name that could not have come from a string is a reason to drop the record. + || info->name.length > 256 + || uniqueId.length > 256) { + FF_DEBUG( + "getDisplayInfo(%u) answered type %d, %u x %u, rotation %u, mode %d, density %u, %f x %f dpi", + info->displayId, info->type, info->width, info->height, info->rotation, info->modeId, info->density, + info->physicalXDpi, info->physicalYDpi + ); + FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size); + return false; + } + + info->preferredWidth = walk.preferred.width; + info->preferredHeight = walk.preferred.height; + info->preferredRefreshRate = walk.preferred.refreshRate; + // The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports and what + // every other platform reports. `renderFrameRate` is only used when the list has no entry for + // `modeId`, because it is a render cadence that follows the content rather than a property of the + // display -- and it is not written at all before Android 15. `refreshRateOverride` is deliberately + // not consulted either: it is what an app asked the framework to hold the panel at, not a + // property of the display. + info->refreshRate = walk.active.refreshRate > 0 ? walk.active.refreshRate : info->renderFrameRate; + info->id = displayUniqueId(uniqueId.chars); + return true; +} + +static FFDisplayType displayType(int32_t type) { + switch ((FFDisplayAndroidType) type) { + case FF_DISPLAY_ANDROID_TYPE_INTERNAL: + return FF_DISPLAY_TYPE_BUILTIN; + case FF_DISPLAY_ANDROID_TYPE_EXTERNAL: + case FF_DISPLAY_ANDROID_TYPE_WIFI: + // A WIFI display is a wireless sink, which is as external as a wired one + return FF_DISPLAY_TYPE_EXTERNAL; + default: + return FF_DISPLAY_TYPE_UNKNOWN; + } +} + +// `hdrCapabilities.mSupportedHdrTypes` is the display's own answer and an empty list means it can not +// do HDR at all. `isForceSdr` is the framework having turned every capability of this display off and +// `persist.sys.hdr_mode` is the vendor's switch, so `Supported` and `Enabled` stay two different +// questions: the list is what the panel can do, those two are whether anything is asking for it. +// +// A non-empty list does not mean the display does HDR. The list is the answer of the vendor's composer +// HAL, which SurfaceFlinger and DisplayManagerService pass through unchanged, and it is wrong on the +// test device (vivo PD2505, Android 17): `dumpsys display` reports `mSupportedHdrTypes=[1, 2, 3]` -- +// Dolby Vision, HDR10 and HLG -- for a Samsung U32J59x that does not do HDR at all, and the same +// three types hang off every one of its forty-odd modes, down to `640x480@60`, which is a VESA DMT +// mode and not a CTA-861 one. An answer derived per mode from the sink's EDID HDR Static Metadata +// block would not look like that, and no Samsung monitor claims Dolby Vision. Nothing on the device +// contradicts it either: SurfaceFlinger dumps every mode of that display as `hdrOutputType=INVALID` +// where the built-in panel's are `SYSTEM`, its only color mode is `ColorMode::NATIVE`, and `cmd +// display get-reported-hdr-types 4` repeats the same `[1, 2, 3]`. The raw EDID would settle it and is +// out of reach: `/sys/class/drm/card0-DP-1/edid` exists but is denied by SELinux, and `adb root` is +// refused on a retail build. So the list is reported as it arrives -- it is the only answer the +// platform gives, and there is nothing here to cross check it against. +static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) { + if (info->hdrTypeCount <= 0) { + return FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate(); + if (!info->forceSdr && ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) && ffStrbufToUInt(&hdrMode, 0) > 0) { + return FF_DISPLAY_HDR_STATUS_ENABLED; + } + return FF_DISPLAY_HDR_STATUS_SUPPORTED; +} + +// Reads every enabled display over binder. Returns false without appending anything when the service, +// the transaction or the layout is not what this parser was written for, which is also what hands the +// module over to the `cmd` fallback. +static bool detectWithBinder(FFDisplayServerResult* ds) { + // The write sequence first. It is what the whole parse is made of, and a build whose jar cannot be + // read has nothing to walk -- so failing here costs one file open rather than a round trip to a + // service whose answer could not be read anyway. + FFDisplayAndroidLayout layout = {}; + if (!displayLayoutLoad(FF_DISPLAY_ANDROID_JAR, &layout)) { + return false; + } + + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + const char* error = ffBinderOpen(&binder); + if (error != nullptr) { + FF_DEBUG("Reading the display over binder failed: %s", error); + return false; + } + + // Released on every path out, including the early returns below. + [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder }; + error = ffBinderLookupService(&binder, FF_DISPLAY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle); + if (error != nullptr) { + FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_DISPLAY_ANDROID_SERVICE, error); + return false; + } + + uint8_t parcelBuffer[FF_DISPLAY_ANDROID_PARCEL_SIZE]; + uint8_t replyBuffer[FF_DISPLAY_ANDROID_REPLY_SIZE]; + + // `getDisplayIds(false)` is the list of the enabled displays, which is the set a display dump + // prints. It is a separate call rather than a loop over 0..n because the ids are not contiguous, + // and it is twelve bytes. + // + // The argument is written unconditionally although the method takes none on Android 11: the + // generated stub reads the parameters its own signature declares and leaves the rest of the + // parcel alone, so the extra int is dropped on those releases. + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_DISPLAY_ANDROID_DESCRIPTOR); + ffBinderParcelPutI32(&parcel, 0); // includeDisabled + + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS, 0, &parcel, &reply); + if (error != nullptr) { + FF_DEBUG("getDisplayIds failed: %s", error); + return false; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("getDisplayIds answered status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0)); + return false; + } + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("getDisplayIds raised exception %d", exception); + return false; + } + const int32_t count = ffBinderReadI32(reply.data, reply.size, sizeof(int32_t)); + if (count <= 0 || count > FF_DISPLAY_ANDROID_MAX_DISPLAYS) { + FF_DEBUG("getDisplayIds answered %d displays", count); + FF_DISPLAY_ANDROID_DEBUG_DUMP(reply.data, reply.size); + return false; + } + + int32_t displayIds[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {}; + for (int32_t i = 0; i < count; ++i) { + displayIds[i] = ffBinderReadI32(reply.data, reply.size, (size_t) (2 + i) * sizeof(int32_t)); + if (displayIds[i] < 0) { + FF_DEBUG("getDisplayIds answered display id %d", displayIds[i]); + FF_DISPLAY_ANDROID_DEBUG_DUMP(reply.data, reply.size); + return false; + } + } + + // Every display has to parse before any of them is reported, so the records are held here until + // the loop has been through all of them. + FFDisplayAndroidInfo infos[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {}; + int32_t parsed = 0; + for (; parsed < count; ++parsed) { + FFDisplayAndroidInfo* info = &infos[parsed]; + ffStrbufInit(&info->name); + + FFBinderParcel request = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&request, FF_DISPLAY_ANDROID_DESCRIPTOR); + ffBinderParcelPutI32(&request, displayIds[parsed]); + + reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO, 0, &request, &reply); + if (error != nullptr) { + FF_DEBUG("getDisplayInfo(%d) failed: %s", displayIds[parsed], error); + break; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("getDisplayInfo(%d) answered status %d instead of a parcel", displayIds[parsed], (int) ffBinderReadI32(reply.data, reply.size, 0)); + break; + } + if (!displayInfoParse(reply.data, reply.size, &layout, info)) { + FF_DEBUG("getDisplayInfo(%d) answered a record this parser does not recognise", displayIds[parsed]); + break; + } + } + + if (parsed != count) { + for (int32_t i = 0; i <= parsed && i < count; ++i) { + ffStrbufDestroy(&infos[i].name); + } + return false; + } + + for (int32_t i = 0; i < count; ++i) { + FFDisplayAndroidInfo* info = &infos[i]; + + // The physical dpi describes the panel itself and does not change with the logical display + // size, so the physical size has to be derived from the native resolution. Nothing in the + // record carries it directly. + uint32_t physicalWidth = 0, physicalHeight = 0; + if (info->physicalXDpi > 0) { + physicalWidth = (uint32_t) ((info->preferredWidth ? info->preferredWidth : info->width) * 25.4 / info->physicalXDpi + 0.5); + } + if (info->physicalYDpi > 0) { + physicalHeight = (uint32_t) ((info->preferredHeight ? info->preferredHeight : info->height) * 25.4 / info->physicalYDpi + 0.5); + } + + FFDisplayResult* display = ffdsAppendDisplay(ds, + info->width, + info->height, + info->refreshRate, + info->density * 96 / 160, + info->preferredWidth, + info->preferredHeight, + info->preferredRefreshRate, + info->rotation, + &info->name, // moved + displayType(info->type), + info->displayId == 0, // Display 0 is the default one + info->id, + physicalWidth, + physicalHeight, + "binder"); + if (display == nullptr) { + // `ffdsAppendDisplay` moves `name` only when it keeps the display, so it is still ours + ffStrbufDestroy(&info->name); + continue; + } + display->manufactureYear = info->manufactureYear; + display->manufactureWeek = info->manufactureWeek; + display->hdrStatus = displayHdrStatus(info); + } + return ds->displays.length > 0; +} + +// --------------------------------------------------------------------------------------------- +// The `cmd` fallback +// --------------------------------------------------------------------------------------------- +// +// The route behind the walk, for a build whose `framework.jar` can not be read at all or whose parcel +// layout is not one the walk knows. `cmd display get-displays` prints the `DisplayInfo` of every +// display as text, one record per line, so the same fields are there to read with no dex and no +// parcel -- at the price of a fork, which is what the walk above exists to avoid. // -// * `cmd display get-displays` needs no permission, which is what makes it usable for an app UID, -// but the subcommand was only added to `DisplayManagerShellCommand` in Android 13. Android 11 and -// 12 answer `Unknown command: get-displays` on stdout with exit code 255. -// * `dumpsys display` covers every release, including the ones that predate `get-displays`, but it -// is gated behind `android.permission.DUMP`, so it only answers for `adb shell` and root. +// It needs no permission, which is what makes it usable for an app UID, but the subcommand was only +// added to `DisplayManagerShellCommand` in Android 13: Android 11 and 12 answer `Unknown command: +// get-displays` on stdout with exit code 255 and this route reports nothing. Those two releases are +// read by the walk instead, so the fallback only has to cover 13 and newer. // // The record layout has changed across releases, and every difference is accepted rather than version // checked: @@ -64,8 +1479,10 @@ static bool detectWithGetprop(FFDisplayServerResult* ds) { // * `displayGroupId` is printed from Android 12 on, the physical dpi behind `density` from // Android 11 on, and `isForceSdr` from Android 15 on. // -// A record is one line, and the `DisplayInfo{` inside it is what gets parsed, so the `Display id 0: ` -// of the one command and the `mBaseDisplayInfo=` of the other are both skipped by the same code. +// A record is one line and the `DisplayInfo{` inside it is what gets parsed, so the `Display id 0: ` +// prefix the command prints in front of each record is stepped over by the same search that finds it. +// `marker` and `platformApi` are the two things a second command would differ in; only `cmd` is wired +// up, `dumpsys display` prints the same records behind `mBaseDisplayInfo=` and is not a route here. static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], const char* marker, const char* platformApi) { FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); FFProcessHandle handle; @@ -73,8 +1490,8 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con // transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal. // Detaching the child from our stdin is only needed here, so the low level API is called instead // of `ffProcessAppendStdOut`. - if (ffProcessSpawn(argv, false, ffGetNullFD(), &handle) != nullptr) { - return false; // Neither command is available on every Android version + if (ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, ffGetNullFD(), &handle) != nullptr) { + return false; // `/system/bin/cmd` is not there before Android 13 } if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) { @@ -134,6 +1551,13 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con // The modes are listed with their resolution in the natural orientation, which is also how // the preferred values are reported on the other platforms. `defaultMode` is the mode the // display itself prefers, so it carries the panel's native resolution. + // + // One entry is a `{...}`, and the three fields are searched for inside it rather than matched + // as a single prefix: Android 15 prints `parentModeId`, `sfModeId` and `flags` between `id` + // and `width`, where `sscanf("{id=%u, width=%u, ...")` stops after the id and reports one + // conversion -- measured on the test device, that is a preferred resolution of 0 x 0 and a + // rate that falls back to `renderFrameRate`. Everything up to the entry's own closing brace + // belongs to it, so a field past that brace is the next entry's and ends the walk. uint32_t preferredWidth = 0, preferredHeight = 0; double preferredRefreshRate = 0, activeModeRefreshRate = 0; field = strstr(info, ", supportedModes ["); @@ -141,10 +1565,22 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con field = strstr(info, ", modes ["); // Android 14 and older } while (field && (field = strstr(field, "{id="))) { - // {id=2, width=1440, height=3168, fps=60.000004, ... + // {id=2, ..., width=1440, height=3168, fps=60.000004, ... + const char* end = strchr(field, '}'); + const char* modeWidthField = strstr(field, ", width="); + const char* modeHeightField = strstr(field, ", height="); + const char* modeFpsField = strstr(field, ", fps="); unsigned id = 0, modeWidth = 0, modeHeight = 0; double fps = 0; - if (sscanf(field, "{id=%u, width=%u, height=%u, fps=%lf", &id, &modeWidth, &modeHeight, &fps) < 4) { + if (end == nullptr || modeWidthField == nullptr || modeWidthField > end || + modeHeightField == nullptr || modeHeightField > end || + modeFpsField == nullptr || modeFpsField > end) { + break; // Not a mode entry this parser can read + } + if (sscanf(field, "{id=%u", &id) < 1 || + sscanf(modeWidthField, ", width=%u", &modeWidth) < 1 || + sscanf(modeHeightField, ", height=%u", &modeHeight) < 1 || + sscanf(modeFpsField, ", fps=%lf", &fps) < 1) { break; } if (id == activeMode) { @@ -232,8 +1668,8 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con } } - // `displayId` sits inside the record, not in front of it: `cmd` prints the id again in its - // own prefix, but `dumpsys` prints only `mBaseDisplayInfo=` + // `displayId` sits inside the record, not in front of it: the id in the prefix the command + // prints is what the marker search stepped over, and the field itself is what this reads unsigned displayId = 0; if ((field = strstr(info, ", displayId ")) && sscanf(field, ", displayId %u", &displayId) < 1) { displayId = 0; @@ -271,6 +1707,10 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con // since Android 11, where an empty list means that the display can not do HDR at all. // The two fallbacks below it are device wide vendor properties, which is the price of // answering for a record that does not print the field. + // + // This is the same field and the same vendor answer the walk above reads, so the same + // caveat applies: a non-HDR display can arrive with a non-empty list, see + // displayHdrStatus. FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); field = strstr(info, "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=["); if (field) { @@ -301,9 +1741,8 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con index = nextIndex < buf.length ? nextIndex + 1 : buf.length; } - // A command that produced no `DisplayInfo` record has to be reported as a failure: the caller - // keys off this value to decide whether to try the other command, and on Android 12 and older - // `dumpsys` is the only one that can still yield a display. + // A command that produced no `DisplayInfo` record has to be reported as a failure, so that the + // caller can tell "this build answers differently" from "this build answered nothing" return ds->displays.length > 0; } @@ -314,23 +1753,6 @@ static bool detectWithCmd(FFDisplayServerResult* ds) { "cmd"); } -static bool detectWithDumpsys(FFDisplayServerResult* ds) { - // `dumpsys` needs android.permission.DUMP, which only the shell UID and root hold. Every other UID - // is answered with `Permission Denial: can't dump DisplayManagerService from from pid=..., uid=... - // due to missing android.permission.DUMP permission` on stdout and a zero exit status, so the fork - // buys a child process and the record loop then finds no `DisplayInfo` -- the same "no display" the - // caller reads as "try the next route". Not forking is the only difference this makes, but it is - // the difference between the fallback chain describing what is available and it guessing. - if (!ffAndroidIsRootOrShell(instance.state.platform.uid)) { - return false; - } - - return detectWithCommand(ds, - (char*[]) { "/system/bin/dumpsys", "display", nullptr }, - "mBaseDisplayInfo=", - "dumpsys"); -} - // Several vendors embed the UI name and its version in `ro.build.display.id` without any // separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`. static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) { @@ -672,12 +2094,11 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); - // `cmd` comes first because it needs no permission and therefore also works for an app UID. - // `dumpsys` is the only route that answers on Android 12 and older, and only for `adb shell` and - // root -- it is skipped without a fork for every other UID, see detectWithDumpsys -- and `getprop` - // is MiUI specific and the last resort. - if (!detectWithCmd(ds) && !detectWithDumpsys(ds)) { - detectWithGetprop(ds); + // Binder comes first: it needs no permission, no child process, and it is the only route that + // answers for an app UID on Android 12 and older, where `cmd display get-displays` does not exist. + // `cmd` is behind it for the case the dex read is what fails, see detectWithCommand. + if (!detectWithBinder(ds)) { + detectWithCmd(ds); } detectDE(ds); diff --git a/src/detection/displayserver/displayserver_apple.c b/src/detection/displayserver/displayserver_apple.c index 94c86dfb6c..948f346bb2 100644 --- a/src/detection/displayserver/displayserver_apple.c +++ b/src/detection/displayserver/displayserver_apple.c @@ -12,13 +12,24 @@ #include #ifdef MAC_OS_X_VERSION_10_15 -[[clang::weak_import]] extern Boolean CoreDisplay_Display_SupportsHDRMode(CGDirectDisplayID display); -[[clang::weak_import]] extern Boolean CoreDisplay_Display_IsHDRModeEnabled(CGDirectDisplayID display); -[[clang::weak_import]] extern CFDictionaryRef CoreDisplay_DisplayCreateInfoDictionary(CGDirectDisplayID display); +// CoreDisplay_Display_* are thin wrappers around these: the CoreDisplay versions just dlsym the +// SkyLight symbol and call it with the same display id (CoreDisplay_DisplayCreateInfoDictionary +// only falls back to its own Mach IPC path when SkyLight is absent). Call SkyLight directly. +[[clang::weak_import]] extern Boolean SLSDisplaySupportsHDRMode(CGDirectDisplayID display); +[[clang::weak_import]] extern Boolean SLSDisplayIsHDRModeEnabled(CGDirectDisplayID display); +[[clang::weak_import]] extern CFDictionaryRef SLSCopyDisplayInfoDictionary(CGDirectDisplayID display); #else #include #endif +// A CGDisplayModeRef keeps the IOKit mode description in a private CFDictionaryRef that +// CoreGraphics never hands out (it only reads single keys out of it by itself), so there is no +// public API for e.g. BitsPerSample. SkyLight exports a plain accessor for it instead, which is +// the same value the private dictionary holds for the display's current mode: +// {_CGSDisplayModeDescription=... "bitsPerPixel"i "bitsPerSample"i "samplesPerPixel"i ...} +// Use that rather than reading CGDisplayModeRef's private layout. +[[clang::weak_import]] extern int SLDisplayBitsPerSample(CGDirectDisplayID display); + static void detectDisplays(FFDisplayServerResult* ds) { CGDirectDisplayID screens[128]; uint32_t screenCount; @@ -52,8 +63,8 @@ static void detectDisplays(FFDisplayServerResult* ds) { ffStrbufClear(&buffer); FF_CFTYPE_AUTO_RELEASE CFDictionaryRef displayInfo = nullptr; #ifdef MAC_OS_X_VERSION_10_15 - if (CoreDisplay_DisplayCreateInfoDictionary) { - displayInfo = CoreDisplay_DisplayCreateInfoDictionary(screen); + if (SLSCopyDisplayInfoDictionary) { + displayInfo = SLSCopyDisplayInfoDictionary(screen); } #else { @@ -151,21 +162,13 @@ static void detectDisplays(FFDisplayServerResult* ds) { physicalHeight, "CoreGraphics"); if (display) { -#ifndef MAC_OS_X_VERSION_10_11 - FF_CFTYPE_AUTO_RELEASE CFStringRef pe = CGDisplayModeCopyPixelEncoding(mode); - if (pe) { - display->bitDepth = (uint8_t) (CFStringGetLength(pe) - CFStringFind(pe, CFSTR("B"), 0).location); - } -#else - // https://stackoverflow.com/a/33519316/9976392 - // Also shitty, but better than parsing `CFCopyDescription(mode)` - CFDictionaryRef dict = (CFDictionaryRef) * ((int64_t*) mode + 2); - if (CFGetTypeID(dict) == CFDictionaryGetTypeID()) { - int32_t bitDepth; - ffCfDictGetInt(dict, kCGDisplayBitsPerSample, &bitDepth); - display->bitDepth = (uint8_t) bitDepth; + if (SLDisplayBitsPerSample) { + // See the comment on SLDisplayBitsPerSample. Returns 0 for an unknown display. + int bitsPerSample = SLDisplayBitsPerSample(screen); + if (bitsPerSample > 0) { + display->bitDepth = (uint8_t) bitsPerSample; + } } -#endif if (display->type == FF_DISPLAY_TYPE_BUILTIN && displayInfo) { display->hdrStatus = CFDictionaryContainsKey(displayInfo, CFSTR("ReferencePeakHDRLuminance")) @@ -173,10 +176,10 @@ static void detectDisplays(FFDisplayServerResult* ds) { : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; } #ifdef MAC_OS_X_VERSION_10_15 - else if (CoreDisplay_Display_SupportsHDRMode) { - if (CoreDisplay_Display_SupportsHDRMode(screen)) { + else if (SLSDisplaySupportsHDRMode) { + if (SLSDisplaySupportsHDRMode(screen)) { display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; - if (CoreDisplay_Display_IsHDRModeEnabled && CoreDisplay_Display_IsHDRModeEnabled(screen)) { + if (SLSDisplayIsHDRModeEnabled && SLSDisplayIsHDRModeEnabled(screen)) { display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; } } else { diff --git a/src/detection/dns/dns_windows.c b/src/detection/dns/dns_windows.c index edab21e86f..f24029b599 100644 --- a/src/detection/dns/dns_windows.c +++ b/src/detection/dns/dns_windows.c @@ -1,67 +1,246 @@ #include "detection/dns/dns.h" #include "common/netif.h" -#include "common/mallocHelper.h" +#include "common/debug.h" +#include "common/windows/registry.h" #include #include -const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) { - FF_AUTO_FREE IP_ADAPTER_ADDRESSES* adapter_addresses = nullptr; - - // Multiple attempts in case interfaces change while - // we are in the middle of querying them. - DWORD adapter_addresses_buffer_size = 0; - for (int attempts = 0;; ++attempts) { - if (adapter_addresses_buffer_size) { - adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); - assert(adapter_addresses); +// The resolvers of an interface are stored in the registry, under a key named after its GUID: +// HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{GUID} IPv4 +// HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\{GUID} IPv6 +// A statically configured `NameServer` takes precedence over the value handed out by DHCP, which is +// the precedence the IP stack applies as well - verified against GetAdaptersAddresses() on a machine +// with both set, where only the static list was reported. +// +// This replaces GetAdaptersAddresses(), which builds the unicast, anycast, multicast and DNS server +// lists of every adapter - about 9 ms on a machine with 52 of them - even though the module only +// reports the resolvers of the interface that carries the default route. Reading the one key that +// matters costs about 16 us (12 us to open it, 4 us to read the value, see +// src/common/windows/registry.c), and the module as a whole goes from about 11 ms to about 0.3 ms. +// +// Known limitation: a resolver learned from a router advertisement (RDNSS) is not written to the +// registry by any Windows version, so on a SLAAC-only network (no DHCPv6 lease, no static +// `NameServer`) IPv6 resolvers are not reported at all. GetAdaptersAddresses() reported them. +#define FF_DNS_REGISTRY_KEY_V4 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\" +#define FF_DNS_REGISTRY_KEY_V6 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces\\" + +// Enough for either key path plus the 38 characters of a GUID +#define FF_DNS_REGISTRY_PATH_SIZE 256 + +static void appendAddress(FFlist* results, FFstrbuf* servers, uint32_t start, uint32_t end, FFDNSShowType showType) { + // A link local address may carry a zone index, which the reported address never includes + char address[INET6_ADDRSTRLEN]; + uint32_t length = end - start; + if (length >= sizeof(address)) { + length = sizeof(address) - 1; + } + + uint32_t i = 0; + for (; i < length && servers->chars[start + i] != '%'; ++i) { + address[i] = servers->chars[start + i]; + } + address[i] = '\0'; + + if (showType & FF_DNS_TYPE_IPV4_BIT) { + IN_ADDR ipv4; + if (InetPtonA(AF_INET, address, &ipv4) == 1) { + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitA(item, INET_ADDRSTRLEN); + item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4, item->chars) - item->chars); + return; + } + } + + if (showType & FF_DNS_TYPE_IPV6_BIT) { + IN6_ADDR ipv6; + if (InetPtonA(AF_INET6, address, &ipv6) == 1) { + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitA(item, INET6_ADDRSTRLEN); + item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6, item->chars) - item->chars); + } + } +} + +static void appendAddressList(FFstrbuf* servers, FFDNSShowType showType, FFlist* results) { + uint32_t start = 0; + while (start < servers->length) { + while (start < servers->length && (servers->chars[start] == ',' || servers->chars[start] == ' ' || servers->chars[start] == '\t')) { + ++start; + } + + uint32_t end = start; + while (end < servers->length && servers->chars[end] != ',' && servers->chars[end] != ' ' && servers->chars[end] != '\t') { + ++end; } - DWORD error = GetAdaptersAddresses( - options->showType & FF_DNS_TYPE_IPV4_BIT - ? options->showType & FF_DNS_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET - : AF_INET6, - GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_FRIENDLY_NAME, - nullptr, - adapter_addresses, - &adapter_addresses_buffer_size); - - if (error == ERROR_SUCCESS) { - break; - } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { - continue; - } else { - return "GetAdaptersAddresses() failed"; + if (end > start) { + appendAddress(results, servers, start, end, showType); } + start = end; + } +} + +// A DHCPv6 lease stores its resolvers as a plain list of 16 byte addresses +static void appendAddressArray(const BYTE* data, uint32_t dataLength, FFDNSShowType showType, FFlist* results) { + if (!(showType & FF_DNS_TYPE_IPV6_BIT)) { + return; } - uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; - // Iterate through all of the adapters - for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { - if (adapter->IfIndex != defaultRouteIfIndex) { - continue; + for (uint32_t offset = 0; offset + sizeof(IN6_ADDR) <= dataLength; offset += sizeof(IN6_ADDR)) { + IN6_ADDR ipv6; + memcpy(&ipv6, data + offset, sizeof(ipv6)); + + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitA(item, INET6_ADDRSTRLEN); + item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6, item->chars) - item->chars); + } +} + +// `Dhcpv6InterfaceOptions` stores the DHCPv6 options of the last lease as an array of records: a 20 +// byte header (option code, 0, payload length, 0, 0xFFFFFFFF) followed by the payload, padded to 16 +// bytes. Option 23 is OPTION_DNS_SERVERS (RFC 3646). +static void appendDhcpv6Options(const BYTE* data, uint32_t dataLength, FFDNSShowType showType, FFlist* results) { + for (uint32_t offset = 0; offset + 20 <= dataLength;) { + uint32_t code, length; + memcpy(&code, data + offset, sizeof(code)); + memcpy(&length, data + offset + 8, sizeof(length)); + + uint32_t payloadLength = dataLength - (offset + 20); + if (payloadLength > length) { + payloadLength = length; } - if (adapter->OperStatus != IfOperStatusUp) { - continue; + + if (code == 23) { + appendAddressArray(data + offset + 20, payloadLength, showType, results); + return; } - for (IP_ADAPTER_DNS_SERVER_ADDRESS_XP* ifa = adapter->FirstDnsServerAddress; ifa; ifa = ifa->Next) { - // The element must only be added once it can be filled: `FF_LIST_ADD` does not zero the - // new element, so an unrecognized address family used to leave an uninitialized FFstrbuf - // in the list, which the callers then read and free - if (ifa->Address.lpSockaddr->sa_family == AF_INET) { - SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr; - FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); - ffStrbufInitA(item, INET_ADDRSTRLEN); - item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4->sin_addr, item->chars) - item->chars); - } else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) { - SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr; - FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); - ffStrbufInitA(item, INET6_ADDRSTRLEN); - item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6->sin6_addr, item->chars) - item->chars); - } + if (payloadLength < length) { + return; } - break; + offset += 20 + ((payloadLength + 15) & ~(uint32_t) 15); } +} + +static void detectV4(const wchar_t* guid, FFDNSOptions* options, FFlist* results) { + wchar_t keyPath[FF_DNS_REGISTRY_PATH_SIZE] = FF_DNS_REGISTRY_KEY_V4; + wcscat(keyPath, guid); + FF_DEBUG("v4 key '%ls'", keyPath); + + FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, keyPath, &hKey, nullptr)) { + FF_DEBUG("v4 key open failed"); + return; + } + + FF_STRBUF_AUTO_DESTROY servers = ffStrbufCreate(); + // `NameServer` holds the statically configured list and wins over the one handed out by DHCP. A + // value that exists but holds no address must not shadow the fallback, hence the length checks. + bool got = (ffRegReadStrbuf(hKey, L"NameServer", &servers, nullptr) && servers.length) || + (ffRegReadStrbuf(hKey, L"DhcpNameServer", &servers, nullptr) && servers.length); + FF_DEBUG("v4 got=%d servers='%s'", got, servers.chars); + if (!got) { + return; + } + + appendAddressList(&servers, options->showType, results); + FF_DEBUG("v4 results=%u", results->length); +} + +static void detectV6(const wchar_t* guid, FFDNSOptions* options, FFlist* results) { + wchar_t keyPath[FF_DNS_REGISTRY_PATH_SIZE] = FF_DNS_REGISTRY_KEY_V6; + wcscat(keyPath, guid); + FF_DEBUG("v6 key '%ls'", keyPath); + + FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, keyPath, &hKey, nullptr)) { + FF_DEBUG("v6 key open failed"); + return; + } + + FF_STRBUF_AUTO_DESTROY servers = ffStrbufCreate(); + if (ffRegReadStrbuf(hKey, L"NameServer", &servers, nullptr) && servers.length) { + FF_DEBUG("v6 servers='%s'", servers.chars); + appendAddressList(&servers, options->showType, results); + FF_DEBUG("v6 results=%u", results->length); + return; + } + + // The two DHCPv6 values below are documented as REG_BINARY and are always written that way; the + // registry reader reports the bytes without their type, so the parsing below trusts that. + BYTE data[4096]; + + FFArgBuffer dhcpv6Servers = { .data = data, .length = sizeof(data) }; + if (ffRegReadData(hKey, L"Dhcpv6DNSServers", &dhcpv6Servers, nullptr)) { + FF_DEBUG("v6 Dhcpv6DNSServers cb=%u", dhcpv6Servers.length); + appendAddressArray(data, dhcpv6Servers.length, options->showType, results); + return; + } + + FFArgBuffer dhcpv6Options = { .data = data, .length = sizeof(data) }; + if (ffRegReadData(hKey, L"Dhcpv6InterfaceOptions", &dhcpv6Options, nullptr)) { + FF_DEBUG("v6 Dhcpv6InterfaceOptions cb=%u", dhcpv6Options.length); + appendDhcpv6Options(data, dhcpv6Options.length, options->showType, results); + return; + } + + FF_DEBUG("v6 no resolver"); +} + +const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) { + FF_DEBUG("enter, showType=%u", (unsigned) options->showType); + // The interface that carries the default route is the one the IP stack resolves names through. + // ffNetifGetDefaultRouteV4() already ignores the interfaces that are not operational. + NET_IFINDEX ifIndex = (NET_IFINDEX) ffNetifGetDefaultRouteV4()->ifIndex; + FF_DEBUG("ifIndex=%u", (unsigned) ifIndex); + if (!ifIndex) { + ifIndex = (NET_IFINDEX) ffNetifGetDefaultRouteV6()->ifIndex; + } + if (!ifIndex) { + FF_DEBUG("Failed to detect default route"); + return "Failed to detect default route"; + } + + NET_LUID luid; + DWORD ret = ConvertInterfaceIndexToLuid(ifIndex, &luid); + if (ret != NO_ERROR) { + FF_DEBUG("ConvertInterfaceIndexToLuid() failed: %s", ffDebugWin32Error(ret)); + return "ConvertInterfaceIndexToLuid() failed"; + } + + GUID guid; + ret = ConvertInterfaceLuidToGuid(&luid, &guid); + if (ret != NO_ERROR) { + FF_DEBUG("ConvertInterfaceLuidToGuid() failed: %s", ffDebugWin32Error(ret)); + return "ConvertInterfaceLuidToGuid() failed"; + } + + wchar_t guidStr[64]; + // clang-format off + swprintf(guidStr, ARRAY_SIZE(guidStr), L"{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", + (unsigned long) guid.Data1, + (unsigned int) guid.Data2, + (unsigned int) guid.Data3, + (unsigned int) guid.Data4[0], + (unsigned int) guid.Data4[1], + (unsigned int) guid.Data4[2], + (unsigned int) guid.Data4[3], + (unsigned int) guid.Data4[4], + (unsigned int) guid.Data4[5], + (unsigned int) guid.Data4[6], + (unsigned int) guid.Data4[7]); + // clang-format on + + // IPv4 first, so the list is grouped by address family the same way the printed output is + if (options->showType & FF_DNS_TYPE_IPV4_BIT) { + detectV4(guidStr, options, results); + } + + if (options->showType & FF_DNS_TYPE_IPV6_BIT) { + detectV6(guidStr, options, results); + } + + FF_DEBUG("return, results=%u", results->length); return nullptr; } diff --git a/src/detection/font/font_windows.c b/src/detection/font/font_windows.c index 960a75abd9..e1260fde24 100644 --- a/src/detection/font/font_windows.c +++ b/src/detection/font/font_windows.c @@ -1,4 +1,5 @@ #include "font.h" +#include "common/debug.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" @@ -23,6 +24,7 @@ static void generateString(FFFontResult* font) { const char* ffDetectFontImpl(FFFontResult* result) { FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Control Panel\\Desktop\\WindowMetrics", &hKey, nullptr)) { + FF_DEBUG("ffRegOpenKeyForRead(Control Panel\\Desktop\\WindowMetrics) failed"); return "ffRegOpenKeyForRead(HKEY_CURRENT_USER\\Control Panel\\Desktop\\WindowMetrics) failed"; } @@ -41,6 +43,7 @@ const char* ffDetectFontImpl(FFFontResult* result) { FF_ARG(fontBuffers[3], L"StatusFont"), }, nullptr)) { + FF_DEBUG("ffRegReadValues(CaptionFont/MenuFont/MessageFont/StatusFont) failed"); return "ffRegReadValues(HKEY_CURRENT_USER\\Control Panel\\Desktop\\WindowMetrics) failed"; } diff --git a/src/detection/gamepad/gamepad_linux.c b/src/detection/gamepad/gamepad_linux.c index 2c2e10587e..83f6700703 100644 --- a/src/detection/gamepad/gamepad_linux.c +++ b/src/detection/gamepad/gamepad_linux.c @@ -1,4 +1,5 @@ #include "gamepad.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -65,6 +66,7 @@ static void detectGamepad(FFlist* devices, FFstrbuf* name, FFstrbuf* path) { const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/input/"); if (dirp == nullptr) { + FF_DEBUG("opendir(\"/sys/class/input/\") failed: %s", strerror(errno)); return "opendir(\"/sys/class/input/\") == nullptr"; } diff --git a/src/detection/gamepad/gamepad_windows.c b/src/detection/gamepad/gamepad_windows.c index 4d4371f1b5..7e3d22b278 100644 --- a/src/detection/gamepad/gamepad_windows.c +++ b/src/detection/gamepad/gamepad_windows.c @@ -1,4 +1,5 @@ #include "gamepad.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" #include "common/time.h" @@ -112,7 +113,7 @@ static bool readHidReport(HANDLE hHidFile, uint8_t* buffer, uint32_t length, DWO // than by handle (CancelIo would take every other read on the same handle with it), and the // cancellation is waited for before returning. CancelIoEx(hHidFile, &overlapped); - GetOverlappedResult(hHidFile, &overlapped, &read, TRUE); + GetOverlappedResultEx(hHidFile, &overlapped, &read, INFINITE, FALSE); return false; } } @@ -339,13 +340,16 @@ static FFGamepadBatteryKind detectBatteryKind(uint32_t vendorId, uint32_t produc const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { UINT nDevices = 0; if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + FF_DEBUG("GetRawInputDeviceList(nullptr) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(nullptr) failed"; } if (nDevices == 0) { + FF_DEBUG("No HID devices found"); return "No HID devices found"; } FF_AUTO_FREE RAWINPUTDEVICELIST* pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + FF_DEBUG("GetRawInputDeviceList(pRawInputDeviceList) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(pRawInputDeviceList) failed"; } diff --git a/src/detection/gpu/gpu.h b/src/detection/gpu/gpu.h index 546de07495..584a18b70b 100644 --- a/src/detection/gpu/gpu.h +++ b/src/detection/gpu/gpu.h @@ -80,6 +80,8 @@ typedef struct FFGpuDriverPciBusId { } FFGpuDriverPciBusId; #if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__) +#define FF_GPU_HAS_PCI_IDS 1 +void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu); diff --git a/src/detection/gpu/gpu_android.c b/src/detection/gpu/gpu_android.c index f25756b4cd..2bae2dc7e9 100644 --- a/src/detection/gpu/gpu_android.c +++ b/src/detection/gpu/gpu_android.c @@ -1,4 +1,5 @@ #include "gpu.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -51,5 +52,6 @@ double ffGPUDetectTempFromTZ(void) { const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { FF_UNUSED(options, gpus); + FF_DEBUG("No permission. Fallbacks to Vulkan, OpenCL or OpenGL instead"); return "No permission. Fallbacks to Vulkan, OpenCL or OpenGL instead"; } diff --git a/src/detection/gpu/gpu_apple.c b/src/detection/gpu/gpu_apple.c index f3775bc5a3..bc4b5119fb 100644 --- a/src/detection/gpu/gpu_apple.c +++ b/src/detection/gpu/gpu_apple.c @@ -1,6 +1,7 @@ #include "gpu.h" #include "common/apple/cf_helpers.h" #include "common/apple/smc_temps.h" +#include "common/debug.h" #include @@ -58,21 +59,25 @@ static const char* detectFrequency(FFGPUResult* gpu) { FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceNameMatching("pmgr")); if (!entryDevice) { + FF_DEBUG("IOServiceGetMatchingServices() failed"); return "IOServiceGetMatchingServices() failed"; } if (!IOObjectConformsTo(entryDevice, "AppleARMIODevice")) { + FF_DEBUG("\"pmgr\" should conform to \"AppleARMIODevice\""); return "\"pmgr\" should conform to \"AppleARMIODevice\""; } FF_CFTYPE_AUTO_RELEASE CFDataRef freqProperty = (CFDataRef) IORegistryEntryCreateCFProperty(entryDevice, CFSTR("voltage-states9-sram"), kCFAllocatorDefault, kNilOptions); if (!freqProperty || CFGetTypeID(freqProperty) != CFDataGetTypeID()) { + FF_DEBUG("\"voltage-states9-sram\" in \"pmgr\" is not found"); return "\"voltage-states9-sram\" in \"pmgr\" is not found"; } // voltage-states9-sram stores supported pairs of gpu from the lowest to the highest CFIndex propLength = CFDataGetLength(freqProperty); if (propLength == 0 || propLength % (CFIndex) (sizeof(uint32_t) * 2) != 0) { + FF_DEBUG("Invalid \"voltage-states9-sram\" length"); return "Invalid \"voltage-states9-sram\" length"; } @@ -101,6 +106,7 @@ const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { CFMutableDictionaryRef matches = IOServiceMatching(kIOAcceleratorClassName); CFDictionaryAddValue(matches, CFSTR("IOMatchCategory"), CFSTR(kIOAcceleratorClassName)); if (IOServiceGetMatchingServices(MACH_PORT_NULL, matches, &iterator) != kIOReturnSuccess) { + FF_DEBUG("IOServiceGetMatchingServices() failed"); return "IOServiceGetMatchingServices() failed"; } } diff --git a/src/detection/gpu/gpu_bsd.c b/src/detection/gpu/gpu_bsd.c index 64e1a74ecd..8be6416b3b 100644 --- a/src/detection/gpu/gpu_bsd.c +++ b/src/detection/gpu/gpu_bsd.c @@ -1,8 +1,10 @@ #include "gpu.h" #include "gpu_driver_specific.h" +#include "common/debug.h" #include "common/io.h" +#include #include #include #if __has_include() @@ -14,6 +16,7 @@ static const char* detectByPci(const FFGPUOptions* options, FFlist* gpus) { FF_AUTO_CLOSE_FD int fd = open("/dev/pci", O_RDONLY | O_CLOEXEC); if (fd < 0) { + FF_DEBUG("open(\"/dev/pci\") failed: %s", strerror(errno)); return "open(\"/dev/pci\", O_RDONLY | O_CLOEXEC, 0) failed"; } @@ -31,10 +34,12 @@ static const char* detectByPci(const FFGPUOptions* options, FFlist* gpus) { }; if (ioctl(fd, PCIOCGETCONF, &pcio) < 0) { + FF_DEBUG("ioctl(PCIOCGETCONF) failed: %s", strerror(errno)); return "ioctl(fd, PCIOCGETCONF, &pc) failed"; } if (pcio.status == PCI_GETCONF_ERROR) { + FF_DEBUG("ioctl(fd, PCIOCGETCONF, &pc) returned error"); return "ioctl(fd, PCIOCGETCONF, &pc) returned error"; } diff --git a/src/detection/gpu/gpu_drm.c b/src/detection/gpu/gpu_drm.c index 77f31799e6..15d31a31a2 100644 --- a/src/detection/gpu/gpu_drm.c +++ b/src/detection/gpu/gpu_drm.c @@ -394,6 +394,8 @@ const char* ffGPUDetectDriverSpecific(const FFGPUOptions* options, FFGPUResult* }, soName); } +#else + FF_UNUSED(options, gpu, pciBusId); #endif return "No driver-specific detection function found for the GPU vendor"; diff --git a/src/detection/gpu/gpu_haiku.c b/src/detection/gpu/gpu_haiku.c index 6d2bf7d3fa..7ea91c3b4e 100644 --- a/src/detection/gpu/gpu_haiku.c +++ b/src/detection/gpu/gpu_haiku.c @@ -1,11 +1,16 @@ #include "gpu.h" +#include "common/debug.h" #include "common/io.h" +#include +#include + #include const char* ffDetectGPUImpl([[maybe_unused]] const FFGPUOptions* options, FFlist* gpus) { FF_AUTO_CLOSE_FD int pokefd = open(POKE_DEVICE_FULLNAME, O_RDWR | O_CLOEXEC); if (pokefd < 0) { + FF_DEBUG("open(%s) failed: %s", POKE_DEVICE_FULLNAME, strerror(errno)); return "open(POKE_DEVICE_FULLNAME) failed"; } diff --git a/src/detection/gpu/gpu_linux.c b/src/detection/gpu/gpu_linux.c index d812e08b75..a594f74554 100644 --- a/src/detection/gpu/gpu_linux.c +++ b/src/detection/gpu/gpu_linux.c @@ -1,4 +1,5 @@ #include "detection/gpu/gpu.h" +#include "common/debug.h" #include "common/io.h" #include "common/FFstrbuf.h" #include "common/strutil.h" @@ -57,6 +58,7 @@ static bool detectDriverFromSysfs(FFstrbuf* result, FFstrbuf* pciDir, FFstrbuf* sprintf(path, "/sys/class/drm/%s/device/drm", drmCardKey); FF_AUTO_CLOSE_DIR DIR* dirp = opendir(path); if (!dirp) { + FF_DEBUG("opendir(%s) failed: %s", path, strerror(errno)); return "Failed to open `/sys/class/drm/{drmCardKey}/device/drm`"; } @@ -68,6 +70,7 @@ static bool detectDriverFromSysfs(FFstrbuf* result, FFstrbuf* pciDir, FFstrbuf* return nullptr; } } + FF_DEBUG("Failed to find render device"); return "Failed to find render device"; } @@ -214,6 +217,7 @@ static const char* drmDetectIntelSpecific(FFGPUResult* gpu, const char* drmKey, ffStrbufAppendS(buffer, drmKey); FF_AUTO_CLOSE_FD int fd = open(buffer->chars, O_RDONLY | O_CLOEXEC); if (fd < 0) { + FF_DEBUG("open(%s) failed: %s", buffer->chars, strerror(errno)); return "Failed to open drm device"; } @@ -222,6 +226,7 @@ static const char* drmDetectIntelSpecific(FFGPUResult* gpu, const char* drmKey, } else if (ffStrbufEqualS(&gpu->driver, "i915")) { return ffDrmDetectI915(gpu, fd); } + FF_DEBUG("Unknown Intel GPU driver"); return "Unknown Intel GPU driver"; } @@ -257,6 +262,7 @@ static const char* drmDetectNouveauSpecific(FFGPUResult* gpu, const char* drmKey ffStrbufAppendS(buffer, drmKey); FF_AUTO_CLOSE_FD int fd = open(buffer->chars, O_RDONLY | O_CLOEXEC); if (fd < 0) { + FF_DEBUG("open(%s) failed: %s", buffer->chars, strerror(errno)); return "Failed to open drm device"; } @@ -336,6 +342,7 @@ static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf uint32_t vendorId, deviceId, subVendorId, subDeviceId; uint8_t classId, subclassId; if (sscanf(buffer->chars + strlen("pci:"), "v%8" SCNx32 "d%8" SCNx32 "sv%8" SCNx32 "sd%8" SCNx32 "bc%2" SCNx8 "sc%2" SCNx8, &vendorId, &deviceId, &subVendorId, &subDeviceId, &classId, &subclassId) != 6) { + FF_DEBUG("Failed to parse pci modalias"); return "Failed to parse pci modalias"; } @@ -348,6 +355,7 @@ static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf if (drmKey) { ssize_t pathLength = readlink(deviceDir->chars, pciPath, ARRAY_SIZE(pciPath) - 1); if (pathLength <= 0) { + FF_DEBUG("readlink(%s) failed: %s", deviceDir->chars, strerror(errno)); return "Unable to get PCI device path"; } pciPath[pathLength] = '\0'; @@ -365,6 +373,7 @@ static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf uint32_t pciDomain, pciBus, pciDevice, pciFunc; if (sscanf(pPciPath, "%" SCNx32 ":%" SCNx32 ":%" SCNx32 ".%" SCNx32, &pciDomain, &pciBus, &pciDevice, &pciFunc) != 4) { + FF_DEBUG("Invalid PCI device path"); return "Invalid PCI device path"; } @@ -517,6 +526,7 @@ static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf static const char* detectOf(FFlist* gpus, FFstrbuf* buffer, FFstrbuf* drmDir, const char* drmKey) { char compatible[256]; // vendor,model-name if (sscanf(buffer->chars + strlen("of:"), "NgpuT%*[^C]C%255[^C]", compatible) != 1) { + FF_DEBUG("Failed to parse of modalias or not a GPU device"); return "Failed to parse of modalias or not a GPU device"; } @@ -573,6 +583,7 @@ static const char* drmDetectGPUs(const FFGPUOptions* options, FFlist* gpus) { FF_AUTO_CLOSE_DIR DIR* dir = opendir(drmDir.chars); if (dir == nullptr) { + FF_DEBUG("opendir(%s) failed: %s", drmDir.chars, strerror(errno)); return "Failed to open `/sys/class/drm/`"; } @@ -612,6 +623,7 @@ static const char* pciDetectGPUs(const FFGPUOptions* options, FFlist* gpus) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pciDirPath); if (dirp == nullptr) { + FF_DEBUG("opendir(%s) failed: %s", pciDirPath, strerror(errno)); return "Failed to open `/sys/bus/pci/devices/`"; } diff --git a/src/detection/gpu/gpu_nbsd.c b/src/detection/gpu/gpu_nbsd.c index e3d04c14e4..5dd2713a71 100644 --- a/src/detection/gpu/gpu_nbsd.c +++ b/src/detection/gpu/gpu_nbsd.c @@ -1,4 +1,5 @@ #include "gpu.h" +#include "common/debug.h" #include "common/io.h" #include @@ -37,6 +38,7 @@ const char* ffDetectGPUImpl([[maybe_unused]] const FFGPUOptions* options, FFlist if (errno == ENOENT) { break; // No more /dev/pciN devices } + FF_DEBUG("open(\"%s\", O_RDONLY | O_CLOEXEC) failed: %s", pciDevPath, strerror(errno)); return "open(\"/dev/pciN\", O_RDONLY | O_CLOEXEC) failed"; } diff --git a/src/detection/gpu/gpu_obsd.c b/src/detection/gpu/gpu_obsd.c index 4dd2d83278..10722f1086 100644 --- a/src/detection/gpu/gpu_obsd.c +++ b/src/detection/gpu/gpu_obsd.c @@ -1,6 +1,9 @@ #include "gpu.h" +#include "common/debug.h" #include "common/io.h" +#include +#include #include #include #include @@ -31,6 +34,7 @@ const char* detectByPci([[maybe_unused]] const FFGPUOptions* options, FFlist* gp char pciDevPath[] = "/dev/pci0"; FF_AUTO_CLOSE_FD int pcifd = open(pciDevPath, O_RDONLY | O_CLOEXEC); if (pcifd < 0) { + FF_DEBUG("open(\"%s\", O_RDONLY | O_CLOEXEC) failed: %s", pciDevPath, strerror(errno)); return "open(\"/dev/pci0\", O_RDONLY | O_CLOEXEC) failed"; } diff --git a/src/detection/gpu/gpu_pci.c b/src/detection/gpu/gpu_pci.c index eb1b874f6b..2aa98745cf 100644 --- a/src/detection/gpu/gpu_pci.c +++ b/src/detection/gpu/gpu_pci.c @@ -60,62 +60,78 @@ static const FFstrbuf* loadPciIds() { return &pciids; } -static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { +void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { FF_DEBUG("Searching pci.ids for vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass); if (content->length) { + const char* const fileEnd = content->chars + content->length; char buffer[32]; // Search for vendor uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor); - char* start = (char*) memmem(content->chars, content->length, buffer, len); - char* end = content->chars + content->length; + const char* start = memmem(content->chars, content->length, buffer, len); if (start) { FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor); start += len; - end = memchr(start, '\n', (uint32_t) (end - start)); - if (!end) { - end = content->chars + content->length; + const char* vendorEnd = memchr(start, '\n', (size_t) (fileEnd - start)); + if (!vendorEnd) { + vendorEnd = fileEnd; } if (!gpu->vendor.length) { - ffStrbufSetNS(&gpu->vendor, (uint32_t) (end - start), start); + ffStrbufSetNS(&gpu->vendor, (uint32_t) (vendorEnd - start), start); } - start = end; // point to '\n' of vendor - end = start + 1; // point to start of devices - // find the start of next vendor - while (end[0] == '\t' || end[0] == '#') { - end = strchr(end, '\n'); - if (!end) { - end = content->chars + content->length; - break; - } else { - end++; - } - } - - // Search for device - len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x ", device); - start = memmem(start, (size_t) (end - start), buffer, len); - if (start) { - FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device); - start += len; - end = memchr(start, '\n', (uint32_t) (end - start)); - if (!end) { - end = content->chars + content->length; - } + // Search for device. Device lines are `\tXXXX name`; their ids only + // grow inside a vendor block, so one linear walk over the block is + // enough: compare in place and stop either at the line starting the + // next vendor, or -- because the ids are sorted -- right after the + // first id greater than the one we are looking for. Doing the compare + // here also removes the need to know where the block ends, so no + // length is ever derived from a pointer that may have run past it. + // pci.ids only asks contributors to keep the entries sorted, and it + // is strictly lowercase hex, so the byte comparison below matches the + // numeric order. A block that is not sorted (only reachable via + // FF_CUSTOM_PCI_IDS_PATH) makes us give up early and fall back to the + // synthesized name below. + char deviceId[8]; + snprintf(deviceId, ARRAY_SIZE(deviceId), "%04x", device); + [[maybe_unused]] bool found = false; + for (const char* p = vendorEnd < fileEnd ? vendorEnd + 1 : fileEnd; + p < fileEnd && (*p == '\t' || *p == '#');) { + if (*p == '\t' && fileEnd - p >= 7) { + int order = memcmp(p + 1, deviceId, 4); + if (order > 0) { + break; + } + if (order == 0 && p[5] == ' ' && p[6] == ' ') { + FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device); + start = p + 7; + const char* end = memchr(start, '\n', (size_t) (fileEnd - start)); + if (!end) { + end = fileEnd; + } - char* closingBracket = end - 1; - if (*closingBracket == ']') { - char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start)); - if (openingBracket) { - openingBracket++; - ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + const char* closingBracket = end - 1; + if (*closingBracket == ']') { + const char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start)); + if (openingBracket) { + openingBracket++; + ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + } + } + if (!gpu->name.length) { + ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start); + } + found = true; + break; } } - if (!gpu->name.length) { - ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start); + p = memchr(p, '\n', (size_t) (fileEnd - p)); + if (!p) { + break; } - } else { + p++; + } + if (!found) { FF_DEBUG("PCI device entry 0x%04x:0x%04x was not found in pci.ids", vendor, device); } } else { @@ -231,7 +247,7 @@ void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, return; } #endif - parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); + ffGPUParsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); FF_DEBUG("Resolved PCI GPU name from pci.ids: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars); } diff --git a/src/detection/gpu/gpu_sunos.c b/src/detection/gpu/gpu_sunos.c index fb9db7a29b..c34471a598 100644 --- a/src/detection/gpu/gpu_sunos.c +++ b/src/detection/gpu/gpu_sunos.c @@ -1,4 +1,5 @@ #include "gpu.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -46,6 +47,7 @@ static int walkDevTree(di_node_t node, [[maybe_unused]] di_minor_t minor, FFlist const char* ffDetectGPUImpl([[maybe_unused]] const FFGPUOptions* options, FFlist* gpus) { di_node_t rootNode = di_init("/", DINFOCPYALL); if (rootNode == DI_NODE_NIL) { + FF_DEBUG("di_init() failed"); return "di_init() failed"; } di_walk_minor(rootNode, DDI_NT_DISPLAY, DI_WALK_CLDFIRST, gpus, (void*) walkDevTree); diff --git a/src/detection/gtk_qt/qt.c b/src/detection/gtk_qt/qt.c index 05317fff66..74046ed2b4 100644 --- a/src/detection/gtk_qt/qt.c +++ b/src/detection/gtk_qt/qt.c @@ -1,7 +1,9 @@ #include "fastfetch.h" #include "common/properties.h" +#include "common/io.h" #include "common/strutil.h" #include "common/FFcache.h" +#include "common/mallocHelper.h" #include "detection/gtk_qt/gtk_qt.h" #include "detection/displayserver/displayserver.h" @@ -24,12 +26,12 @@ typedef enum PlasmaCategory: uint8_t { } PlasmaCategory; static bool detectPlasmaFromFile(const char* filename, FFQtResult* result) { - FILE* kdeglobals = fopen(filename, "r"); + FF_AUTO_CLOSE_FILE FILE* kdeglobals = fopen(filename, "r"); if (kdeglobals == nullptr) { return false; } - char* line = nullptr; + FF_AUTO_FREE char* line = nullptr; size_t len = 0; PlasmaCategory category = PLASMA_CATEGORY_OTHER; @@ -72,10 +74,6 @@ static bool detectPlasmaFromFile(const char* filename, FFQtResult* result) { } } - free(line); - - fclose(kdeglobals); - return true; } diff --git a/src/detection/host/host_apple.c b/src/detection/host/host_apple.c index a855ba5a49..3e3f591e62 100644 --- a/src/detection/host/host_apple.c +++ b/src/detection/host/host_apple.c @@ -1,17 +1,20 @@ #include "host.h" -#include "common/sysctl.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" +#include "common/sysctl.h" #include const char* getProductNameWithIokit(FFstrbuf* result) { FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/product"); if (!registryEntry) { + FF_DEBUG("IORegistryEntryFromPath(IODeviceTree:/product) failed"); return "IOServiceGetMatchingService() failed"; } FF_CFTYPE_AUTO_RELEASE CFStringRef productName = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("product-name"), kCFAllocatorDefault, kNilOptions); if (!productName) { + FF_DEBUG("IORegistryEntryCreateCFProperty(product-name) failed"); return "IORegistryEntryCreateCFProperty() failed"; } @@ -21,6 +24,7 @@ const char* getProductNameWithIokit(FFstrbuf* result) { const char* getOthersByIokit(FFHostResult* host) { FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("IOPlatformExpertDevice")); if (!registryEntry) { + FF_DEBUG("IOServiceGetMatchingService(IOPlatformExpertDevice) failed"); return "IOServiceGetMatchingService() failed"; } diff --git a/src/detection/host/host_windows.c b/src/detection/host/host_windows.c index a39a3041ef..003a94c11d 100644 --- a/src/detection/host/host_windows.c +++ b/src/detection/host/host_windows.c @@ -1,4 +1,5 @@ #include "host.h" +#include "common/debug.h" #include "common/endian.h" #include "common/smbios.h" @@ -32,11 +33,13 @@ static_assert(offsetof(FFSmbiosSystemInfo, Family) == 0x1A, const char* ffDetectHost(FFHostResult* host) { const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); if (!smbiosTable) { + FF_DEBUG("System information: failed to get SMBIOS data"); return "Failed to get SMBIOS data"; } const FFSmbiosSystemInfo* data = (const FFSmbiosSystemInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_SYSTEM_INFO]; if (!data) { + FF_DEBUG("System information: section is not found in SMBIOS data"); return "System information is not found in SMBIOS data"; } diff --git a/src/detection/icons/icons_linux.c b/src/detection/icons/icons_linux.c index fb1b12621a..b76dceb12e 100644 --- a/src/detection/icons/icons_linux.c +++ b/src/detection/icons/icons_linux.c @@ -1,4 +1,5 @@ #include "icons.h" +#include "common/debug.h" #include "common/parsing.h" #include "detection/gtk_qt/gtk_qt.h" #include "detection/displayserver/displayserver.h" @@ -16,6 +17,7 @@ const char* ffDetectIcons(FFIconsResult* result) { const FFstrbuf* gtk4 = &ffDetectGTK4()->icons; if (plasma->length == 0 && gtk2->length == 0 && gtk3->length == 0 && gtk4->length == 0) { + FF_DEBUG("No icons could be found"); return "No icons could be found"; } diff --git a/src/detection/initsystem/initsystem_haiku.c b/src/detection/initsystem/initsystem_haiku.c index bdcb64d9a3..4f3818df1e 100644 --- a/src/detection/initsystem/initsystem_haiku.c +++ b/src/detection/initsystem/initsystem_haiku.c @@ -1,4 +1,5 @@ #include "initsystem.h" +#include "common/debug.h" #include "common/strutil.h" #include "common/haiku/version.h" #include "common/io.h" @@ -11,6 +12,7 @@ const char* ffDetectInitSystem(FFInitSystemResult* result) { // so we can't query be_roster for it. const char* path = "/boot/system/servers/launch_daemon"; if (!ffPathExists(path, FF_PATHTYPE_FILE)) { + FF_DEBUG("%s does not exist", path); return "launch_daemon is not found"; } diff --git a/src/detection/initsystem/initsystem_windows.c b/src/detection/initsystem/initsystem_windows.c index 561d02449c..fa2d9a7e76 100644 --- a/src/detection/initsystem/initsystem_windows.c +++ b/src/detection/initsystem/initsystem_windows.c @@ -1,4 +1,5 @@ #include "initsystem.h" +#include "common/debug.h" #include "common/windows/unicode.h" #include "common/windows/nt.h" #include "common/windows/version.h" @@ -52,6 +53,7 @@ const char* ffDetectInitSystem(FFInitSystemResult* result) { ULONG size = sizeof(buffer); NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, buffer, size, &size); if (status != STATUS_INFO_LENGTH_MISMATCH && !NT_SUCCESS(status)) { + FF_DEBUG("NtQuerySystemInformation(SystemProcessInformation) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessInformation) failed"; } diff --git a/src/detection/keyboard/keyboard_apple.c b/src/detection/keyboard/keyboard_apple.c index 63adcafd74..8dd299062c 100644 --- a/src/detection/keyboard/keyboard_apple.c +++ b/src/detection/keyboard/keyboard_apple.c @@ -1,5 +1,6 @@ #include "keyboard.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include @@ -20,6 +21,7 @@ static void enumSet(IOHIDDeviceRef value, FFlist* results) { const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { FF_CFTYPE_AUTO_RELEASE IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + FF_DEBUG("IOHIDManagerOpen() failed"); return "IOHIDManagerOpen() failed"; } diff --git a/src/detection/keyboard/keyboard_bsd.c b/src/detection/keyboard/keyboard_bsd.c index 46a703b579..b079a46056 100644 --- a/src/detection/keyboard/keyboard_bsd.c +++ b/src/detection/keyboard/keyboard_bsd.c @@ -1,6 +1,8 @@ #include "keyboard.h" +#include "common/debug.h" #include "common/io.h" +#include #include #include #include @@ -15,6 +17,7 @@ static const char* detectByIoctl(FFlist* devices) { keyboard_info_t kbdInfo; if (ioctl(STDIN_FILENO, KDGKBINFO, &kbdInfo) != 0) { + FF_DEBUG("ioctl(KDGKBINFO) failed: %s", strerror(errno)); return "ioctl(KDGKBINFO) failed"; } diff --git a/src/detection/keyboard/keyboard_haiku.cpp b/src/detection/keyboard/keyboard_haiku.cpp index 2063f059f0..480f4070fa 100644 --- a/src/detection/keyboard/keyboard_haiku.cpp +++ b/src/detection/keyboard/keyboard_haiku.cpp @@ -1,5 +1,6 @@ extern "C" { #include "keyboard.h" +#include "common/debug.h" } #include @@ -8,7 +9,9 @@ extern "C" { const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { BList list; - if (get_input_devices(&list) != B_OK) { + status_t status = get_input_devices(&list); + if (status != B_OK) { + FF_DEBUG("get_input_devices() failed: %d", (int) status); return "get_input_devices() failed"; } diff --git a/src/detection/keyboard/keyboard_linux.c b/src/detection/keyboard/keyboard_linux.c index 539282ab85..efb0e80b0b 100644 --- a/src/detection/keyboard/keyboard_linux.c +++ b/src/detection/keyboard/keyboard_linux.c @@ -1,4 +1,5 @@ #include "keyboard.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -9,6 +10,7 @@ const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { // This detects both wired and Bluetooth keyboards uniformly. FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); if (!ffAppendFileBuffer("/proc/bus/input/devices", &content)) { + FF_DEBUG("ffAppendFileBuffer(\"/proc/bus/input/devices\") failed"); return "ffAppendFileBuffer(\"/proc/bus/input/devices\") == nullptr"; } diff --git a/src/detection/keyboard/keyboard_windows.c b/src/detection/keyboard/keyboard_windows.c index 7d98ac4edd..81217c2946 100644 --- a/src/detection/keyboard/keyboard_windows.c +++ b/src/detection/keyboard/keyboard_windows.c @@ -1,6 +1,7 @@ #define INITGUID #include "keyboard.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" #include "common/windows/unicode.h" @@ -13,14 +14,17 @@ const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { UINT nDevices = 0; if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + FF_DEBUG("GetRawInputDeviceList(nullptr) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(nullptr) failed"; } if (nDevices == 0) { + FF_DEBUG("No HID devices found"); return "No HID devices found"; } FF_AUTO_FREE RAWINPUTDEVICELIST* pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + FF_DEBUG("GetRawInputDeviceList(pRawInputDeviceList) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(pRawInputDeviceList) failed"; } diff --git a/src/detection/libc/libc_android.c b/src/detection/libc/libc_android.c index 14f4ab1ba4..ae4b310811 100644 --- a/src/detection/libc/libc_android.c +++ b/src/detection/libc/libc_android.c @@ -1,4 +1,5 @@ #include "libc.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -17,6 +18,7 @@ const char* ffDetectLibc(FFLibcResult* result) { ; return nullptr; #else + FF_DEBUG("Unknown Android libc"); return "Unknown Android libc"; #endif } diff --git a/src/detection/lm/lm_linux.c b/src/detection/lm/lm_linux.c index 2abfab97a8..e9eb016b52 100644 --- a/src/detection/lm/lm_linux.c +++ b/src/detection/lm/lm_linux.c @@ -1,4 +1,5 @@ #include "lm.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" #include "common/properties.h" @@ -9,6 +10,7 @@ #include "common/dbus.h" #endif +#include #include #if __FreeBSD__ @@ -30,6 +32,7 @@ static const char* getGdmVersion(FFstrbuf* version) { if (error || version->length == 0) { error = ffProcessAppendStdOut(version, (char* const[]) { "gdm", "--version", nullptr }); if (error || version->length == 0) { + FF_DEBUG("Failed to get GDM version"); return "Failed to get GDM version"; } } @@ -52,38 +55,38 @@ static const char* getSshdVersion(FFstrbuf* version) { } #ifdef FF_HAVE_ZLIB - #include "common/library.h" #include "common/path.h" + #include "common/zlib.h" #include - #include static const char* getSddmVersion(FFstrbuf* version) { - FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzopen) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzread) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzerror) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gztell) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzrewind) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzclose) - - gzFile file = ffgzopen(FF_PATH_PKG_BASE "/share/man/man1/sddm.1.gz", "rb"); + const char* error = ffZlibLoad(FF_ZLIB_GZFILE); + if (error != nullptr) { + FF_DEBUG("The sddm version cannot be read: %s", error); + return error; + } + + gzFile file = gzopen(FF_PATH_PKG_BASE "/share/man/man1/sddm.1.gz", "rb"); if (file == Z_NULL) { - return "ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed"; + FF_DEBUG("gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed: %s", strerror(errno)); + return "gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed"; } ffStrbufEnsureFree(version, 2047); memset(version->chars, 0, version->allocated); - int size = ffgzread(file, version->chars, version->allocated - 1); - ffgzclose(file); + int size = gzread(file, version->chars, version->allocated - 1); + gzclose(file); if (size <= 0) { - return "ffgzread(file, version->chars, version->length) failed"; + FF_DEBUG("gzread(file, version->chars, version->length) failed"); + return "gzread(file, version->chars, version->length) failed"; } version->length = (uint32_t) size; uint32_t index = ffStrbufFirstIndexS(version, ".TH "); if (index == version->length) { + FF_DEBUG(".TH is not found"); ffStrbufClear(version); return ".TH is not found"; } @@ -137,6 +140,7 @@ static const char* detectBySystemdDbus(FFLMResult* result) { // and requires no $XDG_SESSION_ID being available FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; if (ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus) != nullptr) { + FF_DEBUG("Failed to load system DBus"); return "Failed to load system DBus"; } @@ -147,6 +151,7 @@ static const char* detectBySystemdDbus(FFLMResult* result) { "org.freedesktop.login1.Session", "Service", &result->service)) { + FF_DEBUG("Failed to get systemd session Service property"); return "Failed to get systemd session Service property"; } @@ -170,6 +175,7 @@ static const char* detectBySystemdPrivate(FFLMResult* result) { // This is actually buggy, and assumes current user is using DE // `sd_pid_get_session` can be a better option, but we need to find a pid to use if (!ffParsePropFile(path.chars, "DISPLAY=", &sessionId)) { + FF_DEBUG("Failed to get $XDG_SESSION_ID"); return "Failed to get $XDG_SESSION_ID"; } } @@ -255,11 +261,13 @@ const char* detectByProcesses(FFLMResult* result) { size_t length = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed"; } FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed"; } @@ -277,12 +285,14 @@ const char* detectByProcesses(FFLMResult* result) { size_t length = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed"; } FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed"; } @@ -297,6 +307,7 @@ const char* detectByProcesses(FFLMResult* result) { #elif __sun FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); if (procdir == nullptr) { + FF_DEBUG("opendir(\"/proc\") failed: %s", strerror(errno)); return "opendir(\"/proc\") failed"; } @@ -331,6 +342,7 @@ const char* detectByProcesses(FFLMResult* result) { #elif __linux__ || __GNU__ FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); if (procdir == nullptr) { + FF_DEBUG("opendir(\"/proc\") failed: %s", strerror(errno)); return "opendir(\"/proc\") failed"; } @@ -384,12 +396,14 @@ const char* detectByProcesses(FFLMResult* result) { size_t size = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &size, nullptr, 0) != 0) { + FF_DEBUG("sysctl(KERN_PROC_UID, nullptr) failed: %s", strerror(errno)); return "sysctl(KERN_PROC_UID, nullptr) failed"; } FF_AUTO_FREE struct kinfo_proc2* procs = malloc(size); if (sysctl(request, ARRAY_SIZE(request), procs, &size, nullptr, 0) != 0) { + FF_DEBUG("sysctl(KERN_PROC_UID, procs) failed: %s", strerror(errno)); return "sysctl(KERN_PROC_UID, procs) failed"; } @@ -403,6 +417,7 @@ const char* detectByProcesses(FFLMResult* result) { #endif if (result->service.length == 0) { + FF_DEBUG("Failed to detect login manager by processes"); return "Failed to detect login manager by processes"; } diff --git a/src/detection/loadavg/loadavg_bsd.c b/src/detection/loadavg/loadavg_bsd.c index 7ef95ed77a..925376f4f9 100644 --- a/src/detection/loadavg/loadavg_bsd.c +++ b/src/detection/loadavg/loadavg_bsd.c @@ -1,5 +1,8 @@ #include "detection/loadavg/loadavg.h" +#include "common/debug.h" +#include +#include #include #if __FreeBSD__ || __OpenBSD__ || __NetBSD__ @@ -14,6 +17,7 @@ const char* ffDetectLoadavg(double result[3]) { struct loadavg load; size_t size = sizeof(load); if (sysctl((int[]) { CTL_VM, VM_LOADAVG }, 2, &load, &size, nullptr, 0) < 0) { + FF_DEBUG("sysctl({CTL_VM, VM_LOADAVG}) failed: %s", strerror(errno)); return "sysctl({CTL_VM, VM_LOADAVG}) failed"; } for (int i = 0; i < 3; i++) { diff --git a/src/detection/loadavg/loadavg_linux.c b/src/detection/loadavg/loadavg_linux.c index fb27551b21..d231c43b1d 100644 --- a/src/detection/loadavg/loadavg_linux.c +++ b/src/detection/loadavg/loadavg_linux.c @@ -1,6 +1,9 @@ #include "detection/loadavg/loadavg.h" +#include "common/debug.h" #include "common/io.h" +#include +#include #include const char* ffDetectLoadavg(double result[3]) { @@ -22,6 +25,7 @@ const char* ffDetectLoadavg(double result[3]) { // getloadavg requires higher ANDROID_API version struct sysinfo si; if (sysinfo(&si) < 0) { + FF_DEBUG("both /proc/loadavg and sysinfo() failed: %s", strerror(errno)); return "both /proc/loadavg and sysinfo() failed"; } diff --git a/src/detection/locale/locale_linux.c b/src/detection/locale/locale_linux.c index ab630a7a3c..81618ae6b8 100644 --- a/src/detection/locale/locale_linux.c +++ b/src/detection/locale/locale_linux.c @@ -1,4 +1,5 @@ #include "detection/locale/locale.h" +#include "common/debug.h" #include @@ -18,5 +19,6 @@ const char* ffDetectLocale(FFstrbuf* result) { return nullptr; } + FF_DEBUG("LC_ALL, LANG and setlocale(LC_TIME, nullptr) are all empty"); return "Failed to detect locale"; } diff --git a/src/detection/locale/locale_windows.c b/src/detection/locale/locale_windows.c index 75a0e47f09..c70f3923e1 100644 --- a/src/detection/locale/locale_windows.c +++ b/src/detection/locale/locale_windows.c @@ -1,4 +1,5 @@ #include "detection/locale/locale.h" +#include "common/debug.h" #include "common/windows/unicode.h" #include @@ -17,6 +18,7 @@ const char* ffDetectLocale(FFstrbuf* result) { wchar_t name[LOCALE_NAME_MAX_LENGTH]; int size = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH); if (size <= 1) { // including '\0' + FF_DEBUG("GetUserDefaultLocaleName() failed"); return "GetUserDefaultLocaleName() failed"; } ffStrbufAppendNWS(result, (uint32_t) size - 1, name); diff --git a/src/detection/localip/localip_apple.c b/src/detection/localip/localip_apple.c new file mode 100644 index 0000000000..4055443d4f --- /dev/null +++ b/src/detection/localip/localip_apple.c @@ -0,0 +1,141 @@ +#include "localip.h" +#include "common/apple/cf_helpers.h" +#include "common/debug.h" + +#include +#include +#include +#include +#include + +// macOS 27 hides the link-layer address from every process that is not an Apple platform +// binary, i.e. anything whose code signature has no `Platform identifier` field -- which +// includes ad-hoc and linker-signed builds. Both getifaddrs() and sysctl(NET_RT_IFLIST) +// then report the placeholder 02:00:00:00:00:00 for all interfaces. Re-signing the very +// same /sbin/ifconfig ad-hoc flips its `ether` line from the real address to the +// placeholder while every other field stays byte-identical, so the placeholder itself is +// the tell. +// +// The IORegistry is not affected. The node matching the BSD name never carries the +// property itself and the depth at which it appears depends on the driver -- en0 has it on +// the parent (IO80211Controller), en4 on the parent (AppleUSBDeviceNCM11Data), ap1 on the +// node itself -- so walk up until it shows up. +// +// This reports the *hardware* address. That differs from the address a platform binary +// sees whenever Private Wi-Fi Address is enabled (for en0 the randomized address sits one +// level further up, on IOUserNetworkWLAN), so it is only used to replace the placeholder +// and never preferred over a value read from the interface. +// +// Measured with `fastfetch -s localip --stat` on this machine: replacing the placeholders +// costs ~0.2ms for a single interface and ~1.3ms for the 21 interfaces getifaddrs() +// reports. The SystemConfiguration alternative needed a single SCNetworkInterfaceCopyAll() +// at 2.6-5.2ms (an XPC round trip to configd) and does not even list anpi0-2 or ap1, so the +// IORegistry covers strictly more interfaces for less. +#define FF_LOCALIP_REDACTED_MAC "02:00:00:00:00:00" + +// The classes that expose a `BSD Name` in the IORegistry. anpi*/en* are IONetworkInterface +// subclasses, but the dext-based Wi-Fi interfaces (ap1) are IOUserNetworkWLAN and are not, +// so they need a pass of their own. +static const char* const iokitInterfaceClasses[] = { + "IONetworkInterface", + "IOUserNetworkWLAN", +}; + +// Looks for the first IOMACAddress on `entry` or one of its ancestors. Does not consume it. +static bool getMacFromRegistry(io_object_t entry, FFstrbuf* result) { + if (!entry) { + return false; + } + + FF_IOOBJECT_AUTO_RELEASE io_object_t current = entry; + IOObjectRetain(current); + + for (uint32_t depth = 0; depth < 4; ++depth) { + FF_CFTYPE_AUTO_RELEASE CFDataRef data = (CFDataRef) IORegistryEntryCreateCFProperty(current, CFSTR(kIOMACAddress), kCFAllocatorDefault, kNilOptions); + if (data) { + if (CFGetTypeID(data) != CFDataGetTypeID() || CFDataGetLength(data) < 6) { + return false; + } + const uint8_t* ptr = CFDataGetBytePtr(data); + ffStrbufSetF(result, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + return true; + } + + io_object_t parent = IO_OBJECT_NULL; + if (IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent) != KERN_SUCCESS) { + return false; + } + IOObjectRelease(current); + current = parent; + } + + return false; +} + +static FFLocalIpResult* findRedacted(FFlist* results, const char* ifName) { + FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) { + if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC) && ffStrbufEqualS(&item->name, ifName)) { + return item; + } + } + return nullptr; +} + +// Enumerating the interface classes once and matching on `BSD Name` is measurably cheaper +// than looking every interface up by name -- 1.5ms versus 3.0ms for the 21 interfaces +// getifaddrs() reports here -- which matters as soon as defaultRouteOnly is disabled and a +// dozen interfaces are listed at once. Interfaces the enumeration does not reach are looked +// up by name afterwards, so this can only ever be faster than, never worse than, the direct +// lookup. +void ffLocalIpFixRedactedMacs(FFlist* results) { + bool hasRedacted = false; + FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) { + if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) { + hasRedacted = true; + break; + } + } + if (!hasRedacted) { + return; + } + + for (uint32_t c = 0; c < ARRAY_SIZE(iokitInterfaceClasses); ++c) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(iokitInterfaceClasses[c]), &iterator) != KERN_SUCCESS) { + continue; + } + + io_object_t entry; + while ((entry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + FF_IOOBJECT_AUTO_RELEASE io_object_t entryRef = entry; + + FF_CFTYPE_AUTO_RELEASE CFStringRef bsdName = (CFStringRef) IORegistryEntryCreateCFProperty(entryRef, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kNilOptions); + if (!bsdName) { + continue; + } + + char ifName[IFNAMSIZ]; + if (CFGetTypeID(bsdName) != CFStringGetTypeID() || !CFStringGetCString(bsdName, ifName, sizeof(ifName), kCFStringEncodingUTF8)) { + continue; + } + + FFLocalIpResult* item = findRedacted(results, ifName); + if (item && getMacFromRegistry(entryRef, &item->mac)) { + FF_DEBUG("Replaced the redacted MAC address for interface %s", ifName); + } + } + } + + FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) { + if (!ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) { + continue; + } + + FF_IOOBJECT_AUTO_RELEASE io_object_t service = IOServiceGetMatchingService(MACH_PORT_NULL, IOBSDNameMatching(MACH_PORT_NULL, 0, item->name.chars)); + if (service && getMacFromRegistry(service, &item->mac)) { + FF_DEBUG("Replaced the redacted MAC address for interface %s", item->name.chars); + } else { + FF_DEBUG("No IOMACAddress in the IORegistry for interface %s", item->name.chars); + } + } +} diff --git a/src/detection/localip/localip_linux.c b/src/detection/localip/localip_linux.c index 203543c67c..3a4d49e567 100644 --- a/src/detection/localip/localip_linux.c +++ b/src/detection/localip/localip_linux.c @@ -4,16 +4,10 @@ #include "common/strutil.h" #include "common/debug.h" -#include -#include -#include #include -#include #include -#include #include #include -#include #ifdef __linux__ #include @@ -280,7 +274,7 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { struct ifaddrs* ifAddrStruct = nullptr; if (getifaddrs(&ifAddrStruct) < 0) { - FF_DEBUG("getifaddrs() failed"); + FF_DEBUG("getifaddrs() failed: %s", strerror(errno)); return "getifaddrs(&ifAddrStruct) failed"; } @@ -535,6 +529,11 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { #endif } +#if defined(__APPLE__) + void ffLocalIpFixRedactedMacs(FFlist* results); + ffLocalIpFixRedactedMacs(results); +#endif + FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) { ffListDestroy(&adapter->ipv4); ffListDestroy(&adapter->ipv6); diff --git a/src/detection/localip/localip_windows.c b/src/detection/localip/localip_windows.c index c36118015c..78c8a0f7a9 100644 --- a/src/detection/localip/localip_windows.c +++ b/src/detection/localip/localip_windows.c @@ -2,27 +2,269 @@ #include #include "common/netif.h" -#include "common/mallocHelper.h" #include "common/windows/unicode.h" #include "common/debug.h" #include "localip.h" -#define FF_LOCALIP_NIFLAG(name) { IP_ADAPTER_##name, #name } - -static const FFLocalIpNIFlag niFlagOptions[] = { - FF_LOCALIP_NIFLAG(DDNS_ENABLED), - FF_LOCALIP_NIFLAG(REGISTER_ADAPTER_SUFFIX), - FF_LOCALIP_NIFLAG(DHCP_ENABLED), - FF_LOCALIP_NIFLAG(RECEIVE_ONLY), - FF_LOCALIP_NIFLAG(NO_MULTICAST), - FF_LOCALIP_NIFLAG(IPV6_OTHER_STATEFUL_CONFIG), - FF_LOCALIP_NIFLAG(NETBIOS_OVER_TCPIP_ENABLED), - FF_LOCALIP_NIFLAG(IPV4_ENABLED), - FF_LOCALIP_NIFLAG(IPV6_ENABLED), - FF_LOCALIP_NIFLAG(IPV6_MANAGE_ADDRESS_CONFIG), - // sentinel - {}, -}; +static inline void appendFlag(FFstrbuf* flags, bool enabled, const char* name) { + if (!enabled) { + return; + } + + if (flags->length) { + ffStrbufAppendC(flags, ','); + } + ffStrbufAppendS(flags, name); +} + +static void fillInterfaceFlags(FFstrbuf* flags, const MIB_IF_ROW2* row) { + appendFlag(flags, row->InterfaceAndOperStatusFlags.HardwareInterface, "HARDWARE"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.FilterInterface, "FILTER"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.ConnectorPresent, "CONNECTOR_PRESENT"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.NotAuthenticated, "NOT_AUTHENTICATED"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.NotMediaConnected, "NO_MEDIA"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.Paused, "PAUSED"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.LowPower, "LOW_POWER"); + appendFlag(flags, row->InterfaceAndOperStatusFlags.EndPointInterface, "ENDPOINT"); + + switch (row->DirectionType) { + case NET_IF_DIRECTION_SENDONLY: + appendFlag(flags, true, "SEND_ONLY"); + break; + case NET_IF_DIRECTION_RECEIVEONLY: + appendFlag(flags, true, "RECEIVE_ONLY"); + break; + case NET_IF_DIRECTION_SENDRECEIVE: + appendFlag(flags, true, "SEND_RECEIVE"); + break; + default: + break; + } + + // Exactly one value of each of these enums is reported per interface, so every value gets + // a flag of its own +#define FF_LOCALIP_FLAG_CASE(value, name) \ + case value: appendFlag(flags, true, name); break; + + switch (row->MediaConnectState) { + FF_LOCALIP_FLAG_CASE(MediaConnectStateUnknown, "MEDIA_UNKNOWN") + FF_LOCALIP_FLAG_CASE(MediaConnectStateConnected, "MEDIA_CONNECTED") + FF_LOCALIP_FLAG_CASE(MediaConnectStateDisconnected, "MEDIA_DISCONNECTED") + } + + // The interface type, as registered with IANA and listed in ipifcons.h + switch (row->Type) { + FF_LOCALIP_FLAG_CASE(IF_TYPE_OTHER, "OTHER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_REGULAR_1822, "REGULAR_1822") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HDH_1822, "HDH_1822") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DDN_X25, "DDN_X25") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RFC877_X25, "RFC877_X25") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ETHERNET_CSMACD, "ETHERNET_CSMACD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IS088023_CSMACD, "IS088023_CSMACD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88024_TOKENBUS, "ISO88024_TOKENBUS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88025_TOKENRING, "ISO88025_TOKENRING") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88026_MAN, "ISO88026_MAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_STARLAN, "STARLAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROTEON_10MBIT, "PROTEON_10MBIT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROTEON_80MBIT, "PROTEON_80MBIT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HYPERCHANNEL, "HYPERCHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FDDI, "FDDI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_LAP_B, "LAP_B") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SDLC, "SDLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DS1, "DS1") + FF_LOCALIP_FLAG_CASE(IF_TYPE_E1, "E1") + FF_LOCALIP_FLAG_CASE(IF_TYPE_BASIC_ISDN, "BASIC_ISDN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PRIMARY_ISDN, "PRIMARY_ISDN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_POINT2POINT_SERIAL, "PROP_POINT2POINT_SERIAL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PPP, "PPP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SOFTWARE_LOOPBACK, "SOFTWARE_LOOPBACK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_EON, "EON") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ETHERNET_3MBIT, "ETHERNET_3MBIT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_NSIP, "NSIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SLIP, "SLIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ULTRA, "ULTRA") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DS3, "DS3") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SIP, "SIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FRAMERELAY, "FRAMERELAY") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RS232, "RS232") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PARA, "PARA") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ARCNET, "ARCNET") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ARCNET_PLUS, "ARCNET_PLUS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM, "ATM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MIO_X25, "MIO_X25") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SONET, "SONET") + FF_LOCALIP_FLAG_CASE(IF_TYPE_X25_PLE, "X25_PLE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88022_LLC, "ISO88022_LLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_LOCALTALK, "LOCALTALK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SMDS_DXI, "SMDS_DXI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FRAMERELAY_SERVICE, "FRAMERELAY_SERVICE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_V35, "V35") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HSSI, "HSSI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HIPPI, "HIPPI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MODEM, "MODEM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_AAL5, "AAL5") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SONET_PATH, "SONET_PATH") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SONET_VT, "SONET_VT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SMDS_ICIP, "SMDS_ICIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_VIRTUAL, "PROP_VIRTUAL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_MULTIPLEXOR, "PROP_MULTIPLEXOR") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE80212, "IEEE80212") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FIBRECHANNEL, "FIBRECHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HIPPIINTERFACE, "HIPPIINTERFACE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FRAMERELAY_INTERCONNECT, "FRAMERELAY_INTERCONNECT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_AFLANE_8023, "AFLANE_8023") + FF_LOCALIP_FLAG_CASE(IF_TYPE_AFLANE_8025, "AFLANE_8025") + FF_LOCALIP_FLAG_CASE(IF_TYPE_CCTEMUL, "CCTEMUL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FASTETHER, "FASTETHER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISDN, "ISDN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_V11, "V11") + FF_LOCALIP_FLAG_CASE(IF_TYPE_V36, "V36") + FF_LOCALIP_FLAG_CASE(IF_TYPE_G703_64K, "G703_64K") + FF_LOCALIP_FLAG_CASE(IF_TYPE_G703_2MB, "G703_2MB") + FF_LOCALIP_FLAG_CASE(IF_TYPE_QLLC, "QLLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FASTETHER_FX, "FASTETHER_FX") + FF_LOCALIP_FLAG_CASE(IF_TYPE_CHANNEL, "CHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE80211, "IEEE80211") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IBM370PARCHAN, "IBM370PARCHAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ESCON, "ESCON") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DLSW, "DLSW") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISDN_S, "ISDN_S") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISDN_U, "ISDN_U") + FF_LOCALIP_FLAG_CASE(IF_TYPE_LAP_D, "LAP_D") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IPSWITCH, "IPSWITCH") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RSRB, "RSRB") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_LOGICAL, "ATM_LOGICAL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DS0, "DS0") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DS0_BUNDLE, "DS0_BUNDLE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_BSC, "BSC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ASYNC, "ASYNC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_CNR, "CNR") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88025R_DTR, "ISO88025R_DTR") + FF_LOCALIP_FLAG_CASE(IF_TYPE_EPLRS, "EPLRS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ARAP, "ARAP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_CNLS, "PROP_CNLS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HOSTPAD, "HOSTPAD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_TERMPAD, "TERMPAD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FRAMERELAY_MPI, "FRAMERELAY_MPI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_X213, "X213") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ADSL, "ADSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RADSL, "RADSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SDSL, "SDSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VDSL, "VDSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88025_CRFPRINT, "ISO88025_CRFPRINT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MYRINET, "MYRINET") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICE_EM, "VOICE_EM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICE_FXO, "VOICE_FXO") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICE_FXS, "VOICE_FXS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICE_ENCAP, "VOICE_ENCAP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICE_OVERIP, "VOICE_OVERIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_DXI, "ATM_DXI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_FUNI, "ATM_FUNI") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_IMA, "ATM_IMA") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PPPMULTILINKBUNDLE, "PPPMULTILINKBUNDLE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IPOVER_CDLC, "IPOVER_CDLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IPOVER_CLAW, "IPOVER_CLAW") + FF_LOCALIP_FLAG_CASE(IF_TYPE_STACKTOSTACK, "STACKTOSTACK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VIRTUALIPADDRESS, "VIRTUALIPADDRESS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MPC, "MPC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IPOVER_ATM, "IPOVER_ATM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISO88025_FIBER, "ISO88025_FIBER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_TDLC, "TDLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_GIGABITETHERNET, "GIGABITETHERNET") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HDLC, "HDLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_LAP_F, "LAP_F") + FF_LOCALIP_FLAG_CASE(IF_TYPE_V37, "V37") + FF_LOCALIP_FLAG_CASE(IF_TYPE_X25_MLP, "X25_MLP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_X25_HUNTGROUP, "X25_HUNTGROUP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_TRANSPHDLC, "TRANSPHDLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_INTERLEAVE, "INTERLEAVE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FAST, "FAST") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IP, "IP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DOCSCABLE_MACLAYER, "DOCSCABLE_MACLAYER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DOCSCABLE_DOWNSTREAM, "DOCSCABLE_DOWNSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DOCSCABLE_UPSTREAM, "DOCSCABLE_UPSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_A12MPPSWITCH, "A12MPPSWITCH") + FF_LOCALIP_FLAG_CASE(IF_TYPE_TUNNEL, "TUNNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_COFFEE, "COFFEE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_CES, "CES") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_SUBINTERFACE, "ATM_SUBINTERFACE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_L2_VLAN, "L2_VLAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_L3_IPVLAN, "L3_IPVLAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_L3_IPXVLAN, "L3_IPXVLAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DIGITALPOWERLINE, "DIGITALPOWERLINE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MEDIAMAILOVERIP, "MEDIAMAILOVERIP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DTM, "DTM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DCN, "DCN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IPFORWARD, "IPFORWARD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MSDSL, "MSDSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE1394, "IEEE1394") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IF_GSN, "IF_GSN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DVBRCC_MACLAYER, "DVBRCC_MACLAYER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DVBRCC_DOWNSTREAM, "DVBRCC_DOWNSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DVBRCC_UPSTREAM, "DVBRCC_UPSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_VIRTUAL, "ATM_VIRTUAL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MPLS_TUNNEL, "MPLS_TUNNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SRP, "SRP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICEOVERATM, "VOICEOVERATM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_VOICEOVERFRAMERELAY, "VOICEOVERFRAMERELAY") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IDSL, "IDSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_COMPOSITELINK, "COMPOSITELINK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SS7_SIGLINK, "SS7_SIGLINK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_WIRELESS_P2P, "PROP_WIRELESS_P2P") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FR_FORWARD, "FR_FORWARD") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RFC1483, "RFC1483") + FF_LOCALIP_FLAG_CASE(IF_TYPE_USB, "USB") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE8023AD_LAG, "IEEE8023AD_LAG") + FF_LOCALIP_FLAG_CASE(IF_TYPE_BGP_POLICY_ACCOUNTING, "BGP_POLICY_ACCOUNTING") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FRF16_MFR_BUNDLE, "FRF16_MFR_BUNDLE") + FF_LOCALIP_FLAG_CASE(IF_TYPE_H323_GATEKEEPER, "H323_GATEKEEPER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_H323_PROXY, "H323_PROXY") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MPLS, "MPLS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MF_SIGLINK, "MF_SIGLINK") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HDSL2, "HDSL2") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SHDSL, "SHDSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DS1_FDL, "DS1_FDL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_POS, "POS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DVB_ASI_IN, "DVB_ASI_IN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DVB_ASI_OUT, "DVB_ASI_OUT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PLC, "PLC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_NFAS, "NFAS") + FF_LOCALIP_FLAG_CASE(IF_TYPE_TR008, "TR008") + FF_LOCALIP_FLAG_CASE(IF_TYPE_GR303_RDT, "GR303_RDT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_GR303_IDT, "GR303_IDT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ISUP, "ISUP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_DOCS_WIRELESS_MACLAYER, "PROP_DOCS_WIRELESS_MACLAYER") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_DOCS_WIRELESS_DOWNSTREAM, "PROP_DOCS_WIRELESS_DOWNSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_DOCS_WIRELESS_UPSTREAM, "PROP_DOCS_WIRELESS_UPSTREAM") + FF_LOCALIP_FLAG_CASE(IF_TYPE_HIPERLAN2, "HIPERLAN2") + FF_LOCALIP_FLAG_CASE(IF_TYPE_PROP_BWA_P2MP, "PROP_BWA_P2MP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_SONET_OVERHEAD_CHANNEL, "SONET_OVERHEAD_CHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_DIGITAL_WRAPPER_OVERHEAD_CHANNEL, "DIGITAL_WRAPPER_OVERHEAD_CHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_AAL2, "AAL2") + FF_LOCALIP_FLAG_CASE(IF_TYPE_RADIO_MAC, "RADIO_MAC") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_RADIO, "ATM_RADIO") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IMT, "IMT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_MVL, "MVL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_REACH_DSL, "REACH_DSL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_FR_DLCI_ENDPT, "FR_DLCI_ENDPT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_ATM_VCI_ENDPT, "ATM_VCI_ENDPT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_OPTICAL_CHANNEL, "OPTICAL_CHANNEL") + FF_LOCALIP_FLAG_CASE(IF_TYPE_OPTICAL_TRANSPORT, "OPTICAL_TRANSPORT") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE80216_WMAN, "IEEE80216_WMAN") + FF_LOCALIP_FLAG_CASE(IF_TYPE_WWANPP, "WWANPP") + FF_LOCALIP_FLAG_CASE(IF_TYPE_WWANPP2, "WWANPP2") + FF_LOCALIP_FLAG_CASE(IF_TYPE_IEEE802154, "IEEE802154") + FF_LOCALIP_FLAG_CASE(IF_TYPE_XBOX_WIRELESS, "XBOX_WIRELESS") + } + +#undef FF_LOCALIP_FLAG_CASE +} + +static bool isDefaultRouteInterface(const FFLocalIpOptions* options, NET_IFINDEX ifIndex) { + // Both route lookups are cached, so this is cheap even inside the per interface loop + return ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == ifIndex) || + ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == ifIndex); +} const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { FF_DEBUG("Starting local IP detection with showType=0x%X, namePrefix='%.*s'", @@ -30,89 +272,102 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { (int) options->namePrefix.length, options->namePrefix.chars); - FF_AUTO_FREE IP_ADAPTER_ADDRESSES* adapter_addresses = nullptr; - - // Multiple attempts in case interfaces change while - // we are in the middle of querying them. - DWORD adapter_addresses_buffer_size = 0; - for (int attempts = 0;; ++attempts) { - FF_DEBUG("Attempt %d to get adapter addresses, buffer size: %lu", attempts + 1, adapter_addresses_buffer_size); - - if (adapter_addresses_buffer_size) { - adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); - assert(adapter_addresses); + ADDRESS_FAMILY family = options->showType & FF_LOCALIP_TYPE_IPV4_BIT + ? options->showType & FF_LOCALIP_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET + : AF_INET6; + bool queryV4 = (options->showType & FF_LOCALIP_TYPE_IPV4_BIT) != 0; + bool queryV6 = (options->showType & FF_LOCALIP_TYPE_IPV6_BIT) != 0; + + // Every interface is queried with GetIfEntry2() rather than by walking a table of all of them: + // GetIfTable2() costs about 0.95 ms on a machine with a few dozen adapters however few of them + // are reported, because it marshals every registered one, while GetIfEntry2() costs about 24 us + // per interface. With 52 adapters registered and 8 interfaces carrying an address the per + // interface API is 3.6 times faster overall; the table only wins past roughly 40 interfaces with + // an address, and with FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT (the default) at most one interface + // can pass the filter below anyway. + bool defaultRouteOnly = (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) != 0; + if (defaultRouteOnly) { + if (queryV4) { + FF_DEBUG("Querying the IPv4 default route interface only: ifIndex=%u", (unsigned) ffNetifGetDefaultRouteV4()->ifIndex); } - - DWORD family = options->showType & FF_LOCALIP_TYPE_IPV4_BIT - ? options->showType & FF_LOCALIP_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET - : AF_INET6; - FF_DEBUG("Calling GetAdaptersAddresses with family=%u, flags=0x%X", (unsigned) family, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER); - - DWORD error = GetAdaptersAddresses( - family, - GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, - nullptr, - adapter_addresses, - &adapter_addresses_buffer_size); - - if (error == ERROR_SUCCESS) { - FF_DEBUG("GetAdaptersAddresses succeeded on attempt %d", attempts + 1); - break; - } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { - FF_DEBUG("Buffer overflow, need %lu bytes, retrying", adapter_addresses_buffer_size); - continue; - } else { - FF_DEBUG("GetAdaptersAddresses failed with error %lu after %d attempts", error, attempts + 1); - return "GetAdaptersAddresses() failed"; + if (queryV6) { + FF_DEBUG("Querying the IPv6 default route interface only: ifIndex=%u", (unsigned) ffNetifGetDefaultRouteV6()->ifIndex); } } + PMIB_UNICASTIPADDRESS_TABLE table = nullptr; + const char* errorMessage = nullptr; + + // The unicast address table is the only API that reports per interface addresses and it cannot + // be filtered by interface, so it is queried once and only the rows of the interfaces which pass + // the filter below are read + if (!NETIO_SUCCESS(GetUnicastIpAddressTable(family, &table))) { + FF_DEBUG("GetUnicastIpAddressTable failed"); + errorMessage = "GetUnicastIpAddressTable() failed"; + goto cleanup; + } + [[maybe_unused]] int adapterCount = 0, processedCount = 0; - // Iterate through all of the adapters - for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { + // Rows of the same interface are not adjacent (IPv4 rows come first), + // so each interface is handled at its first row + for (ULONG i = 0; i < table->NumEntries; ++i) { + NET_IFINDEX ifIndex = table->Table[i].InterfaceIndex; + + bool seen = false; + for (ULONG j = 0; j < i && !seen; ++j) { + seen = table->Table[j].InterfaceIndex == ifIndex; + } + if (seen) { + continue; + } + adapterCount++; + // Checked first to avoid querying interfaces we won't show + if (defaultRouteOnly && !isDefaultRouteInterface(options, ifIndex)) { + FF_DEBUG("Skipping interface %u (not default route interface)", (unsigned) ifIndex); + continue; + } + + MIB_IF_ROW2 ifRow = { .InterfaceIndex = ifIndex }; + if (!NETIO_SUCCESS(GetIfEntry2(&ifRow))) { + FF_DEBUG("GetIfEntry2(%u) failed", (unsigned) ifIndex); + continue; + } + FF_DEBUG("Processing adapter %d: IfIndex=%u, IfType=%u, OperStatus=%u", adapterCount, - (unsigned) adapter->IfIndex, - (unsigned) adapter->IfType, - (unsigned) adapter->OperStatus); + (unsigned) ifIndex, + (unsigned) ifRow.Type, + (unsigned) ifRow.OperStatus); - if (adapter->OperStatus != IfOperStatusUp) { - FF_DEBUG("Skipping adapter %u (not operational, status=%d)", (unsigned) adapter->IfIndex, adapter->OperStatus); + if (ifRow.OperStatus != IfOperStatusUp) { + FF_DEBUG("Skipping adapter %u (not operational, status=%d)", (unsigned) ifIndex, ifRow.OperStatus); continue; } - bool isLoop = adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK; - FF_DEBUG("Adapter %u: isLoopback=%s", (unsigned) adapter->IfIndex, isLoop ? "true" : "false"); + bool isLoop = ifRow.Type == IF_TYPE_SOFTWARE_LOOPBACK; + FF_DEBUG("Adapter %u: isLoopback=%s", (unsigned) ifIndex, isLoop ? "true" : "false"); if (isLoop && !(options->showType & FF_LOCALIP_TYPE_LOOP_BIT)) { - FF_DEBUG("Skipping loopback adapter %u (loopback not requested)", (unsigned) adapter->IfIndex); + FF_DEBUG("Skipping loopback adapter %u (loopback not requested)", (unsigned) ifIndex); continue; } - FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(adapter->FriendlyName); - FF_DEBUG("Adapter %u name: '%s'", (unsigned) adapter->IfIndex, name.chars); + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(ifRow.Alias); + FF_DEBUG("Adapter %u name: '%s'", (unsigned) ifIndex, name.chars); if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { FF_DEBUG("Skipping adapter %u (name doesn't match prefix '%.*s')", - (unsigned) adapter->IfIndex, + (unsigned) ifIndex, (int) options->namePrefix.length, options->namePrefix.chars); continue; } - if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) { - if (!((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex) && - !((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex)) { - FF_DEBUG("Skipping interface %u (not default route interface)", (unsigned) adapter->IfIndex); - continue; - } - } - processedCount++; - FF_DEBUG("Creating result item for adapter %u ('%s')", (unsigned) adapter->IfIndex, name.chars); + FF_DEBUG("Creating result item for adapter %u ('%s')", (unsigned) ifIndex, name.chars); FFLocalIpResult* item = FF_LIST_ADD(FFLocalIpResult, *results); ffStrbufInitMove(&item->name, &name); @@ -124,16 +379,26 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { item->speed = -1; item->mtu = -1; + if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) { + fillInterfaceFlags(&item->flags, &ifRow); + FF_DEBUG("Adapter %u flags: '%s'", (unsigned) ifIndex, item->flags.chars); + } + uint32_t typesToAdd = options->showType & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT); - FF_DEBUG("Types to add for adapter %u: 0x%X", (unsigned) adapter->IfIndex, typesToAdd); + FF_DEBUG("Types to add for adapter %u: 0x%X", (unsigned) ifIndex, typesToAdd); [[maybe_unused]] int ipv4Count = 0, ipv6Count = 0; - for (IP_ADAPTER_UNICAST_ADDRESS* ifa = adapter->FirstUnicastAddress; ifa; ifa = ifa->Next) { + for (ULONG j = i; j < table->NumEntries; ++j) { + MIB_UNICASTIPADDRESS_ROW* ifa = &table->Table[j]; + if (ifa->InterfaceIndex != ifIndex) { + continue; + } + FF_DEBUG("Processing unicast address: prefix origin=%d, suffix origin=%d, family=%d, DadState=%d", ifa->PrefixOrigin, ifa->SuffixOrigin, - ifa->Address.lpSockaddr->sa_family, + ifa->Address.si_family, ifa->DadState); if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) { @@ -150,19 +415,19 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { // MIB_UNICASTIPADDRESS_ROW::SkipAsSource } - if (ifa->Address.lpSockaddr->sa_family == AF_INET) { + if (ifa->Address.si_family == AF_INET) { if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) { FF_DEBUG("Skipping IPv4 address (not requested in typesToAdd=0x%X)", typesToAdd); continue; } - bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex); - if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) { + bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == ifIndex); + if (defaultRouteOnly && !isDefaultRoute) { FF_DEBUG("Skipping IPv4 address (not on default route interface)"); continue; } - SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr; + SOCKADDR_IN* ipv4 = &ifa->Address.Ipv4; char addressBuffer[INET_ADDRSTRLEN + 10]; char* end = RtlIpv4AddressToStringA(&ipv4->sin_addr, addressBuffer); @@ -185,13 +450,13 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { if (typesToAdd == 0) { break; } - } else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) { + } else if (ifa->Address.si_family == AF_INET6) { if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) { FF_DEBUG("Skipping IPv6 address (not requested in typesToAdd=0x%X)", typesToAdd); continue; } - SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr; + SOCKADDR_IN6* ipv6 = &ifa->Address.Ipv6; FFLocalIpIpv6Type ipv6Type = FF_LOCALIP_IPV6_TYPE_NONE; if (IN6_IS_ADDR_GLOBAL(&ipv6->sin6_addr)) { @@ -209,8 +474,8 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { continue; } - bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex); - if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) { + bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == ifIndex); + if (defaultRouteOnly && !isDefaultRoute) { FF_DEBUG("Skipping IPv6 address (not on default route interface)"); continue; } @@ -240,24 +505,20 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { } } - FF_DEBUG("Adapter %u: collected %d IPv4 and %d IPv6 addresses", (unsigned) adapter->IfIndex, ipv4Count, ipv6Count); + FF_DEBUG("Adapter %u: collected %d IPv4 and %d IPv6 addresses", (unsigned) ifIndex, ipv4Count, ipv6Count); if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) { - item->speed = (int32_t) (adapter->ReceiveLinkSpeed / 1000000); - FF_DEBUG("Adapter %u speed: %d Mbps (raw: %llu)", (unsigned) adapter->IfIndex, item->speed, adapter->ReceiveLinkSpeed); + item->speed = (int32_t) (ifRow.ReceiveLinkSpeed / 1000000); + FF_DEBUG("Adapter %u speed: %d Mbps (raw: %llu)", (unsigned) ifIndex, item->speed, ifRow.ReceiveLinkSpeed); } if (options->showType & FF_LOCALIP_TYPE_MTU_BIT) { - item->mtu = (int32_t) adapter->Mtu; - FF_DEBUG("Adapter %u MTU: %d", (unsigned) adapter->IfIndex, item->mtu); + item->mtu = (int32_t) ifRow.Mtu; + FF_DEBUG("Adapter %u MTU: %d", (unsigned) ifIndex, item->mtu); } - if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) { - ffLocalIpFillNIFlags(&item->flags, adapter->Flags, niFlagOptions); - FF_DEBUG("Adapter %u flags: 0x%lX -> '%s'", (unsigned) adapter->IfIndex, adapter->Flags, item->flags.chars); - } - if (options->showType & FF_LOCALIP_TYPE_MAC_BIT && adapter->PhysicalAddressLength == 6) { - uint8_t* ptr = adapter->PhysicalAddress; + if (options->showType & FF_LOCALIP_TYPE_MAC_BIT && ifRow.PhysicalAddressLength == 6) { + const uint8_t* ptr = ifRow.PhysicalAddress; ffStrbufSetF(&item->mac, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); - FF_DEBUG("Adapter %u MAC: %s", (unsigned) adapter->IfIndex, item->mac.chars); + FF_DEBUG("Adapter %u MAC: %s", (unsigned) ifIndex, item->mac.chars); } } @@ -266,5 +527,9 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { processedCount, results->length); - return nullptr; +cleanup: + if (table) { + FreeMibTable(table); + } + return errorMessage; } diff --git a/src/detection/media/media_android.c b/src/detection/media/media_android.c index 53596e760d..e698bd2050 100644 --- a/src/detection/media/media_android.c +++ b/src/detection/media/media_android.c @@ -1,6 +1,9 @@ #include "media.h" #include "common/android/api.h" +#include "common/android/applabel.h" #include "common/android/binder.h" +#include "common/android/dex.h" +#include "common/android/package.h" #include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" @@ -30,8 +33,9 @@ // service and reaches onTransact, and is then answered with a SecurityException carrying the server's // own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell // UID gets the session list from the same transaction code. Nothing in this file tests the uid: the -// service is asked and its answer is believed, so a device whose policy is looser, or one where a -// notification listener is registered, works without a change here. +// service is asked and its answer is believed, so a device whose policy is looser works without a +// change here. What the answer does depend on is the ComponentName in the request, which is not a +// filter and not decoration either -- see the request build in androidDetectMediaSession(). // // Only one of the two routes is ever asked, and which one it is depends on the display server. The // binder route is Android's, and the session it reads is the one the phone is playing only while @@ -70,23 +74,44 @@ #define FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR "android.media.session.ISessionManager" #define FF_MEDIA_ANDROID_CONTROLLER_DESCRIPTOR "android.media.session.ISessionController" -// Transaction codes as this device's own framework.jar declares them, read out of its dex rather than -// transcribed from AOSP -- a vendor ROM adds and drops methods as it likes, and a wrong code reaches -// a different method or none at all. getSessions is the second declared method of ISessionManager; -// the numbers on ISessionController are sparse because most of that interface is setters this module -// never calls. -#define FF_MEDIA_ANDROID_GET_SESSIONS 2u -#define FF_MEDIA_ANDROID_GET_PACKAGE_NAME 5u -#define FF_MEDIA_ANDROID_GET_METADATA 32u -#define FF_MEDIA_ANDROID_GET_PLAYBACK_STATE 33u +// The jar and the stub classes the transaction codes are read out of. Both interfaces have always +// been part of framework.jar -- neither is one of the frameworks that moved into an APEX -- so there +// is one path and no fallback. +#define FF_MEDIA_ANDROID_JAR "/system/framework/framework.jar" +#define FF_MEDIA_ANDROID_SESSIONS_STUB "Landroid/media/session/ISessionManager$Stub;" +#define FF_MEDIA_ANDROID_CONTROLLER_STUB "Landroid/media/session/ISessionController$Stub;" + +// The methods this module calls, by the name of the constant the dex carries for each. Spelling the +// names out rather than the numbers is the whole point: the numbering is a property of the `.aidl` +// the ROM was built from, and a vendor that inserts a method ahead of one shifts it, after which a +// transcribed code reaches a different method or none. `getSessions` is 2 from Android 12 on but 3 on +// Android 11, where `notifySession2Created` holds the second slot -- which is exactly the drift a +// hardcoded number cannot survive. See common/android/dex.h. +#define FF_MEDIA_ANDROID_GET_SESSIONS "TRANSACTION_getSessions" +#define FF_MEDIA_ANDROID_GET_PACKAGE_NAME "TRANSACTION_getPackageName" +#define FF_MEDIA_ANDROID_GET_METADATA "TRANSACTION_getMetadata" +#define FF_MEDIA_ANDROID_GET_PLAYBACK_STATE "TRANSACTION_getPlaybackState" // The largest reply any of these getters produced is a metadata Bundle at ~1.6 KB, and the artwork // never lands here -- it arrives as a descriptor. 16 KB leaves room for a Bundle of unusually long // strings; a reply that did not fit would be reported rather than misread. #define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024) -// An interface token plus, for getSessions, two int32s. -#define FF_MEDIA_ANDROID_REQUEST_SIZE 128 +// ffAndroidGetOwnPackage() writes at most this many bytes, terminator included. +#define FF_MEDIA_ANDROID_PACKAGE_SIZE 128 + +// The request is the interface token, the ComponentName and the user id. A ComponentName is a +// presence marker in front of the package name and the class name, so the two string16s are what +// took this past the 128 it used to be. That size had room for a seven-character package name -- +// `com.termux` with an empty class is already 136 bytes -- so the size is derived from the longest +// name that can be written rather than rounded up by hand, because a request that does not fit is +// dropped and reported as truncated: that fails the whole detection instead of degrading to a call +// without a name. The empty class name costs a 4-byte length, a NUL terminator and the padding, so 8. +// clang-format off +#define FF_MEDIA_ANDROID_REQUEST_SIZE \ + (12 + ((4 + 2 * sizeof(FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR) + 3) & ~3u) \ + + 4 + ((4 + 2 * FF_MEDIA_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8u + 4u) +// clang-format on // Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a // larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2% @@ -447,6 +472,45 @@ static bool androidCompressCover(FFNativeFD fd, const void* pixels, uint32_t wid // The session // --------------------------------------------------------------------------------------------- +// Every transaction code this module sends, resolved from the device's own jar. They are resolved +// together, in one walk, because that is the cheap way to get them: the walk is dominated by the +// dex's type table, and the two stub classes sit in the same entry of every framework.jar seen so far +// (classes2.dex on Android 11 and 13, classes3.dex on Android 16), so one pass answers all four. +// Asking one at a time would repeat that pass four times -- see common/android/dex.h. +typedef struct FFMediaAndroidCodes { + int32_t getSessions; + int32_t getPackageName; + int32_t getMetadata; + int32_t getPlaybackState; +} FFMediaAndroidCodes; + +static const char* androidResolveCodes(FFMediaAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_MEDIA_ANDROID_SESSIONS_STUB, FF_MEDIA_ANDROID_GET_SESSIONS, &codes->getSessions }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_PACKAGE_NAME, &codes->getPackageName }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_METADATA, &codes->getMetadata }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, &codes->getPlaybackState }, + }; + + const char* error = ffDexStaticInts(FF_MEDIA_ANDROID_JAR, requests, ARRAY_SIZE(requests)); + if (error != nullptr) { + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_MEDIA_ANDROID_JAR, error); + return error; + } + + // All four are codes the module cannot answer without. The reader leaves a code it could not find + // at the sentinel instead of judging what that costs the caller, so the judgement is made here. + if (codes->getSessions == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getPackageName == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getMetadata == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getPlaybackState == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("Unresolved transaction codes: getSessions=%d, getPackageName=%d, getMetadata=%d, getPlaybackState=%d", + (int) codes->getSessions, (int) codes->getPackageName, (int) codes->getMetadata, (int) codes->getPlaybackState); + return "The session service does not declare every method this module calls"; + } + return nullptr; +} + // Every getter on ISessionController takes no argument, so a call is the interface token and nothing // else; `flags` is passed through because one of them needs TF_ACCEPT_FDS. [[gnu::nonnull(1, 5)]] static const char* androidCall(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, FFBinderReply* reply) { @@ -591,6 +655,7 @@ static const char* androidReadMetadataEntry(FFMediaAndroidReader* reader, size_t static const char* androidReadMetadataBundle(FFMediaAndroidReader* reader, FFMediaResult* result, const FFBinderReply* reply, FFMediaAndroidCover* cover) { int32_t bundleLength = 0; if (!androidReadI32(reader, &bundleLength) || bundleLength <= 0) { + FF_DEBUG("The metadata bundle length is %d, which cannot hold a bundle", (int) bundleLength); return "The metadata bundle is empty"; } const size_t bundleEnd = reader->position + (size_t) bundleLength; @@ -600,10 +665,12 @@ static const char* androidReadMetadataBundle(FFMediaAndroidReader* reader, FFMed int32_t magic = 0; if (!androidReadI32(reader, &magic) || (uint32_t) magic != FF_MEDIA_ANDROID_BUNDLE_MAGIC) { + FF_DEBUG("The metadata magic is 0x%08x instead of 0x%08x", (unsigned) magic, FF_MEDIA_ANDROID_BUNDLE_MAGIC); return "The metadata is not a bundle"; } int32_t count = 0; if (!androidReadI32(reader, &count)) { + FF_DEBUG("The metadata bundle is truncated before its entry count"); return "The metadata bundle has no entry count"; } @@ -730,9 +797,9 @@ static int64_t androidElapsedRealtime(void) { return nullptr; } -static void androidReadPackageName(FFBinder* binder, uint32_t token, FFMediaResult* result) { +static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PACKAGE_NAME, 0, &reply); + const char* error = androidCall(binder, token, transaction, 0, &reply); if (error != nullptr) { FF_DEBUG("getPackageName failed: %s", error); return; @@ -743,22 +810,28 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, FFMediaResu return; } - // `player` wants the name the app shows, and there is no way to get one from here: `cmd package - // dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and - // resolving it would mean reading the app's resource table. The package name is what the - // notification and the settings screen fall back to as well, so both fields get it -- the same - // fallback media_linux.c uses when MPRIS answers without an Identity. + // `player` wants the name the app shows rather than the name it is installed under, and the + // package name is only the fallback. The label comes out of the APK's own resource table, which + // the package manager points at with `sourceDir` and `labelRes`: the table is *stored* rather + // than deflated -- the platform expects to mmap it -- so resolving an id out of it needs no + // decompressor. What keeps the package name are the apps whose label is a literal string in + // their manifest instead of a resource reference, since their reply carries no id at all; that + // is also what the notification and the settings screen fall back to. See doc/app-label.md. ffStrbufSet(&result->player, &result->playerId); + const char* labelError = ffAndroidGetAppLabel(result->playerId.chars, &result->player); + if (labelError != nullptr) { + FF_DEBUG("The label of \"%s\" could not be resolved: %s", result->playerId.chars, labelError); + } } -static void androidReadMetadata(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) { +static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); // TF_ACCEPT_FDS is not a hint and not optional here: the artwork arrives as a descriptor, and a // request that did not ask for one is answered with BR_FAILED_REPLY -- which reads like a service // that is not running rather than like a missing flag. It is passed unconditionally, because the // alternative is a failure that depends on whether the player happened to publish artwork. - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_METADATA, TF_ACCEPT_FDS, &reply); + const char* error = androidCall(binder, token, transaction, TF_ACCEPT_FDS, &reply); FFMediaAndroidCover cover = { .fd = -1 }; if (error != nullptr) { @@ -782,14 +855,14 @@ static void androidReadMetadata(FFBinder* binder, uint32_t token, FFMediaResult* } } -static void androidReadSession(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) { - androidReadPackageName(binder, token, result); +static void androidReadSession(FFBinder* binder, uint32_t token, const FFMediaAndroidCodes* codes, FFMediaResult* result, bool saveCover) { + androidReadPackageName(binder, token, (uint32_t) codes->getPackageName, result); // The length has to be known before the position is placed, so the metadata comes first. - androidReadMetadata(binder, token, result, saveCover); + androidReadMetadata(binder, token, (uint32_t) codes->getMetadata, result, saveCover); FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply); + const char* error = androidCall(binder, token, (uint32_t) codes->getPlaybackState, 0, &reply); if (error == nullptr && ffBinderReadI32(reply.data, reply.size, 4) != 0) { FFMediaAndroidReader reader = { .data = reply.data, .size = reply.size, .position = 8 }; error = androidReadPlaybackState(&reader, result); @@ -808,10 +881,21 @@ static void androidReadSession(FFBinder* binder, uint32_t token, FFMediaResult* // is the service's decision to make and it is not the only way in -- a notification listener is let // through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the // answer. That costs one transaction, and it keeps the module working wherever the check happens to -// pass, instead of hard-coding today's policy into the client. +// pass, instead of hard-coding today's policy into the client. What the request has to carry for the +// check to be consulted at all is the ComponentName: see the request build below. static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) { + // Resolved before the binder is opened, so that a jar which does not declare the methods says so + // rather than the module failing later with a message about the transport. The reader's own + // reason is what is returned: a jar that cannot be read, a class it does not define and a field + // the class does not declare are three different things. + FFMediaAndroidCodes codes = {}; + const char* error = androidResolveCodes(&codes); + if (error != nullptr) { + return error; + } + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); + error = ffBinderOpen(&binder); if (error != nullptr) { return error; } @@ -822,12 +906,20 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove FF_DEBUG("Looking up \"%s\" failed: %s", FF_MEDIA_ANDROID_SERVICE, error); return error; } + FF_DEBUG("The \"%s\" service is handle %u, getSessions is transaction %d", FF_MEDIA_ANDROID_SERVICE, service.handle, codes.getSessions); // A phone can have several sessions at once -- a music player and a video one, say -- and only one // of them is the answer. Which one is decided first, by state alone, so that the metadata and the // artwork are fetched exactly once, for the winner. uint32_t tokens[FF_BINDER_MAX_HANDLES]; uint32_t tokenCount = 0; + // The descriptors the reply below delivers, held outside its block because the reply itself does + // not outlive it: pointing `acquired` into `reply.fds` leaves it reading whatever the next call + // puts on that stack, which closes a descriptor that is not ours and leaks the one that is. + // Declared above the cleanup variable for the same reason `tokens[]` is -- a local declared + // after one has its lifetime ended before that cleanup runs, which is what a sanitizer sees. + int32_t fds[FF_BINDER_MAX_FDS] = {}; + uint32_t fdCount = 0; // Raised as soon as ffBinderTransact() has replied, because from that point on it is holding a // strong reference to every handle it collected and the kernel has installed every descriptor it // delivered into our own fd table. Any early return past that line has to give them back, so one @@ -842,11 +934,42 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request)); ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR); - ffBinderParcelPutI32(&parcel, 0); // ComponentName: null asks for every session + + // The ComponentName is not a filter and not decoration: it decides which branch of the + // service's permission check runs, and with a null one the branch this module needs is never + // reached. MediaSessionService.enforceMediaPermissions() admits the shell and root, a holder of + // android.permission.MEDIA_CONTENT_CONTROL, and a UID that owns an enabled notification + // listener -- but the last one is reached through isEnabledNotificationListener(compName, uid), + // which answers false for a null compName by construction. A null therefore asks for every + // session only on behalf of a UID that was going to be admitted anyway. + // + // Measured on the test device, one binary run by three UIDs: the shell gets the session list + // for every shape below; the app UID that owns an enabled listener gets -1 EX_SECURITY for a + // null ComponentName and the session list for a non-null one; an app UID that owns none gets + // -1 EX_SECURITY for both. Two more facts + // come out of the same run: the class inside the ComponentName is never validated -- a name no + // package declares and an empty string are answered exactly like a real one -- and the package + // has to be one the calling uid owns, because the service's own enforcePackageName() rejects + // anything else with EX_ILLEGAL_ARGUMENT ("packageName does not belong to the calling uid") + // before the permission check is reached. + // + // So the caller declares its own package and the class is left empty rather than invented: + // this is a standalone binary with no component of its own to name, and naming one the device + // does not have would be a claim that cannot be backed. A build whose package cannot be + // derived sends the null request, which is what this did before. + char package[FF_MEDIA_ANDROID_PACKAGE_SIZE]; + if (ffAndroidGetOwnPackage(package, sizeof(package))) { + ffBinderParcelPutI32(&parcel, 1); + ffBinderParcelPutString16(&parcel, package); + ffBinderParcelPutString16(&parcel, ""); + } else { + FF_DEBUG("Cannot determine the package of this process, so the request declares no component"); + ffBinderParcelPutI32(&parcel, 0); + } ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - error = ffBinderTransact(&binder, service.handle, FF_MEDIA_ANDROID_GET_SESSIONS, 0, &parcel, &reply); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getSessions, 0, &parcel, &reply); replied = true; if (error != nullptr) { return error; @@ -858,11 +981,13 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove if (exception != 0) { // -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The // number goes to the debug log rather than into the message: what tells the two cases apart - // for the user is which UID is asking. + // for the user is which UID is asking. On an app UID the missing privilege is named, + // because it is one the user can grant -- the request below already declares the caller, + // so whatever is left is the listener the UID does not own. FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : ""); return ffAndroidIsRootOrShell(instance.state.platform.uid) ? "The media session service refused the request" - : "Reading the media session needs the shell UID or root: an app UID is refused by the media_session service"; + : "Reading the media session from an app UID needs an enabled notification listener, which this UID does not have"; } // The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order @@ -880,11 +1005,14 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove // Every descriptor the reply delivered is ours to close. The art of a session is read through // one of them, and reading it happens over the fd number rather than over the file, so closing // it on the way out does not cut the read short. - acquired.fds = reply.fds; - acquired.fdCount = reply.fdCount; + fdCount = reply.fdCount; + memcpy(fds, reply.fds, sizeof(fds)); + acquired.fds = fds; + acquired.fdCount = fdCount; } if (tokenCount == 0) { + FF_DEBUG("The media_session service returned no session tokens"); return "No active media session"; } @@ -892,7 +1020,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove uint32_t bestScore = 0; for (uint32_t i = 0; i < tokenCount; i++) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - if (androidCall(&binder, tokens[i], FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply) != nullptr) { + if (androidCall(&binder, tokens[i], (uint32_t) codes.getPlaybackState, 0, &reply) != nullptr) { continue; } // The presence marker first: a session with no state yet answers a null parcelable, and its @@ -905,7 +1033,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove } } - androidReadSession(&binder, tokens[best], media, saveCover); + androidReadSession(&binder, tokens[best], &codes, media, saveCover); // The tokens were acquired by ffBinderTransact() and are given back by the cleanup attribute above. return nullptr; diff --git a/src/detection/media/media_apple.m b/src/detection/media/media_apple.m index 32f2c61667..a7acea4676 100644 --- a/src/detection/media/media_apple.m +++ b/src/detection/media/media_apple.m @@ -110,106 +110,9 @@ static uint32_t getTrueElapsedTime(CFDictionaryRef info) { return error; } -#if !FF_MODULE_DISABLE_MEDIA -// Called from the Perl helper process through `DynaLoader::dl_install_xsub`. That mechanism installs a -// plain C function as an XSUB, so Perl calls it with the `CV*` in the first parameter slot and no Perl -// argument ever reaches a parameter — a `bool saveCover` parameter read as a pointer and was always -// truthy. The flag has to travel in the environment instead; see `getMediaByAuthorizedProcess`. -[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(void) { - const char* saveCoverEnv = getenv("FF_MEDIA_REMOTE_SAVE_COVER"); - bool saveCover = saveCoverEnv && saveCoverEnv[0] == '1'; - - FFMediaResult media = { - .status = ffStrbufCreate(), - .song = ffStrbufCreate(), - .artist = ffStrbufCreate(), - .album = ffStrbufCreate(), - .playerId = ffStrbufCreate(), - .player = ffStrbufCreate(), - .cover = ffStrbufCreate(), - }; - if (getMediaByMediaRemote(&media, saveCover) != nullptr) { - return 1; - } - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppend(&media.status, &media.song); - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppend(&media.status, &media.artist); - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppend(&media.status, &media.album); - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppend(&media.status, &media.playerId); - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppend(&media.status, &media.player); - ffStrbufAppendC(&media.status, '\n'); - if (saveCover) { - ffStrbufAppend(&media.status, &media.cover); - } - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppendUInt(&media.status, media.position); - ffStrbufAppendC(&media.status, '\n'); - ffStrbufAppendUInt(&media.status, media.length); - write(STDOUT_FILENO, media.status.chars, media.status.length); - ffStrbufDestroy(&media.status); - ffStrbufDestroy(&media.song); - ffStrbufDestroy(&media.artist); - ffStrbufDestroy(&media.album); - ffStrbufDestroy(&media.playerId); - ffStrbufDestroy(&media.player); - ffStrbufDestroy(&media.cover); - return 0; -} -#endif - -static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) { - // #1737 - // `DynaLoader::dl_install_xsub` hands the C function the `CV*` as its first parameter, so the flag - // cannot be passed as a Perl argument — `ffPrintMediaByMediaRemote` reads it from the environment, - // which the child process inherits. - FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("$ENV{'FF_MEDIA_REMOTE_SAVE_COVER'}='%c';\ -require DynaLoader;\ -my $so = DynaLoader::dl_load_file('%s', 0);\ -my $sym = DynaLoader::dl_find_symbol($so, 'ffPrintMediaByMediaRemote');\ -DynaLoader::dl_install_xsub('fn', $sym, __FILE__);\ -exit(fn())", - saveCover ? '1' : '0', - instance.state.platform.exePath.chars); - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); - - const char* error = ffProcessAppendStdOut( - &buffer, - (char* const[]) { "/usr/bin/perl", // Must be signed by Apple. Homebrew version doesn't work - "-e", - script.chars, - nil }); - if (error) { - return error; - } - if (buffer.length == 0) { - return "No media found"; - } - - // status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS - FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover }; - char* line = nullptr; - size_t len = 0; - for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) { - ffStrbufSetS(strList[i], line); - } - uint32_t* const numList[] = { &result->position, &result->length }; - for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) { - *numList[i] = (uint32_t) strtoul(line, nullptr, 10); - } - return nullptr; -} - void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { - const char* error; - if (@available(macOS 15.4, *)) { - error = getMediaByAuthorizedProcess(media, saveCover); - } else { - error = getMediaByMediaRemote(media, saveCover); - } + const char* error = getMediaByMediaRemote(media, saveCover); + if (error) { ffStrbufAppendS(&media->error, error); } else { diff --git a/src/detection/media/media_windows.cpp b/src/detection/media/media_windows.cpp index 3f24f3c8b5..6f0d2a1fd0 100644 --- a/src/detection/media/media_windows.cpp +++ b/src/detection/media/media_windows.cpp @@ -1,5 +1,6 @@ extern "C" { #include "media.h" +#include "common/debug.h" #include "common/time.h" #include "common/windows/unicode.h" #include "common/windows/com.h" @@ -410,6 +411,7 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) { #else static const char* getMedia(FFMediaResult* media, bool saveCover) { FF_UNUSED(media, saveCover); + FF_DEBUG("Fastfetch is not compiled with WinRT support"); return "Fastfetch is not compiled with WinRT support"; } #endif // FF_HAVE_WINRT diff --git a/src/detection/memory/memory_apple.c b/src/detection/memory/memory_apple.c index 443ef20620..365b45e965 100644 --- a/src/detection/memory/memory_apple.c +++ b/src/detection/memory/memory_apple.c @@ -1,6 +1,7 @@ #include "memory.h" #include "common/debug.h" +#include #include #include #include @@ -11,10 +12,12 @@ const char* ffDetectMemory(FFMemoryResult* ram) { #if FF_APPLE_MEMSIZE_USABLE if (sysctlbyname("hw.memsize_usable", &ram->bytesTotal, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctlbyname(\"hw.memsize_usable\") failed: %s", strerror(errno)); return "Failed to read hw.memsize_usable"; } #else if (sysctl((int[]) { CTL_HW, HW_MEMSIZE }, 2, &ram->bytesTotal, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl(HW_MEMSIZE) failed: %s", strerror(errno)); return "Failed to read hw.memsize"; } #endif @@ -22,6 +25,7 @@ const char* ffDetectMemory(FFMemoryResult* ram) { mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; vm_statistics64_data_t vmstat; if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t) (&vmstat), &count) != KERN_SUCCESS) { + FF_DEBUG("host_statistics64() failed"); return "Failed to read host_statistics64"; } diff --git a/src/detection/memory/memory_bsd.c b/src/detection/memory/memory_bsd.c index 1985dfcd64..2a5e927316 100644 --- a/src/detection/memory/memory_bsd.c +++ b/src/detection/memory/memory_bsd.c @@ -1,9 +1,14 @@ #include "memory.h" +#include "common/debug.h" #include "common/sysctl.h" +#include +#include + const char* ffDetectMemory(FFMemoryResult* ram) { size_t length = sizeof(ram->bytesTotal); if (sysctl((int[]) { CTL_HW, HW_PHYSMEM }, 2, &ram->bytesTotal, &length, nullptr, 0)) { + FF_DEBUG("sysctl(hw.physmem) failed: %s", strerror(errno)); return "Failed to read hw.physmem"; } diff --git a/src/detection/memory/memory_haiku.c b/src/detection/memory/memory_haiku.c index bf54070473..705cb43166 100644 --- a/src/detection/memory/memory_haiku.c +++ b/src/detection/memory/memory_haiku.c @@ -1,10 +1,13 @@ #include "memory.h" +#include "common/debug.h" #include const char* ffDetectMemory(FFMemoryResult* ram) { system_info info; - if (get_system_info(&info) != B_OK) { + status_t status = get_system_info(&info); + if (status != B_OK) { + FF_DEBUG("get_system_info() failed: %d", (int) status); return "Error getting system info"; } diff --git a/src/detection/memory/memory_linux.c b/src/detection/memory/memory_linux.c index 0a2ebd3fb1..b525aef045 100644 --- a/src/detection/memory/memory_linux.c +++ b/src/detection/memory/memory_linux.c @@ -1,10 +1,12 @@ #include "memory.h" +#include "common/debug.h" #include "common/io.h" const char* ffDetectMemory(FFMemoryResult* ram) { char buf[PROC_FILE_BUFFSIZ]; ssize_t nRead = ffReadFileData("/proc/meminfo", ARRAY_SIZE(buf) - 1, buf); if (nRead < 0) { + FF_DEBUG("ffReadFileData(\"/proc/meminfo\", ARRAY_SIZE(buf)-1, buf) failed: %s", strerror(errno)); return "ffReadFileData(\"/proc/meminfo\", ARRAY_SIZE(buf)-1, buf)"; } buf[nRead] = '\0'; @@ -21,6 +23,7 @@ const char* ffDetectMemory(FFMemoryResult* ram) { if ((token = memmem(buf, (size_t) nRead, "MemTotal:", strlen("MemTotal:"))) != nullptr) { memTotal = (uint64_t) strtoull(token + strlen("MemTotal:"), nullptr, 10); } else { + FF_DEBUG("MemTotal not found in /proc/meminfo"); return "MemTotal not found in /proc/meminfo"; } diff --git a/src/detection/memory/memory_nbsd.c b/src/detection/memory/memory_nbsd.c index fe43e7058f..feef3437f6 100644 --- a/src/detection/memory/memory_nbsd.c +++ b/src/detection/memory/memory_nbsd.c @@ -1,6 +1,9 @@ #include "memory.h" +#include "common/debug.h" #include "common/sysctl.h" +#include +#include #include #include @@ -8,6 +11,7 @@ const char* ffDetectMemory(FFMemoryResult* ram) { struct uvmexp_sysctl buf; size_t length = sizeof(buf); if (sysctl((int[]) { CTL_VM, VM_UVMEXP2 }, 2, &buf, &length, nullptr, 0) < 0) { + FF_DEBUG("sysctl(CTL_VM, VM_UVMEXP2) failed: %s", strerror(errno)); return "sysctl(CTL_VM, VM_UVMEXP2) failed"; } diff --git a/src/detection/memory/memory_obsd.c b/src/detection/memory/memory_obsd.c index 87b7953e62..b831ae7937 100644 --- a/src/detection/memory/memory_obsd.c +++ b/src/detection/memory/memory_obsd.c @@ -1,6 +1,9 @@ #include "memory.h" +#include "common/debug.h" #include "common/sysctl.h" +#include +#include #include #include @@ -8,6 +11,7 @@ const char* ffDetectMemory(FFMemoryResult* ram) { struct uvmexp buf; size_t length = sizeof(buf); if (sysctl((int[]) { CTL_VM, VM_UVMEXP }, 2, &buf, &length, nullptr, 0) < 0) { + FF_DEBUG("sysctl(CTL_VM, VM_UVMEXP) failed: %s", strerror(errno)); return "sysctl(CTL_VM, VM_UVMEXP) failed"; } diff --git a/src/detection/memory/memory_windows.c b/src/detection/memory/memory_windows.c index 2beeb4b2be..0059a3d86d 100644 --- a/src/detection/memory/memory_windows.c +++ b/src/detection/memory/memory_windows.c @@ -1,4 +1,5 @@ #include "memory.h" +#include "common/debug.h" #include "common/windows/nt.h" @@ -6,8 +7,11 @@ const char* ffDetectMemory(FFMemoryResult* ram) { // Note: GlobalMemoryStatusEx() internally uses SystemMemoryUsageInformation in Win 10 SYSTEM_BASIC_PERFORMANCE_INFORMATION sbpi; - if (!NT_SUCCESS(NtQuerySystemInformation(SystemBasicPerformanceInformation, &sbpi, sizeof(sbpi), NULL))) + NTSTATUS status = NtQuerySystemInformation(SystemBasicPerformanceInformation, &sbpi, sizeof(sbpi), NULL); + if (!NT_SUCCESS(status)) { + FF_DEBUG("Failed to query memory information: %s", ffDebugNtStatus(status)); return "Failed to query memory information"; + } uint64_t phyPages = #if _WIN64 diff --git a/src/detection/mouse/mouse_apple.c b/src/detection/mouse/mouse_apple.c index 7f0b96cd0b..1413d3140a 100644 --- a/src/detection/mouse/mouse_apple.c +++ b/src/detection/mouse/mouse_apple.c @@ -1,5 +1,6 @@ #include "mouse.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include @@ -20,6 +21,7 @@ static void enumSet(IOHIDDeviceRef value, FFlist* results) { const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { FF_CFTYPE_AUTO_RELEASE IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + FF_DEBUG("IOHIDManagerOpen() failed"); return "IOHIDManagerOpen() failed"; } diff --git a/src/detection/mouse/mouse_haiku.cpp b/src/detection/mouse/mouse_haiku.cpp index 70a4533c36..bb7033fb24 100644 --- a/src/detection/mouse/mouse_haiku.cpp +++ b/src/detection/mouse/mouse_haiku.cpp @@ -1,5 +1,6 @@ extern "C" { #include "mouse.h" +#include "common/debug.h" } #include @@ -8,7 +9,9 @@ extern "C" { const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { BList list; - if (get_input_devices(&list) != B_OK) { + status_t inputDevicesStatus = get_input_devices(&list); + if (inputDevicesStatus != B_OK) { + FF_DEBUG("get_input_devices() failed: status_t 0x%08x", (uint32_t) inputDevicesStatus); return "get_input_devices() failed"; } diff --git a/src/detection/mouse/mouse_linux.c b/src/detection/mouse/mouse_linux.c index 4c66fc513e..ddcc39812a 100644 --- a/src/detection/mouse/mouse_linux.c +++ b/src/detection/mouse/mouse_linux.c @@ -1,10 +1,12 @@ #include "mouse.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/input/"); if (dirp == nullptr) { + FF_DEBUG("opendir(\"/sys/class/input/\") failed: %s", strerror(errno)); return "opendir(\"/sys/class/input/\") == nullptr"; } diff --git a/src/detection/mouse/mouse_windows.c b/src/detection/mouse/mouse_windows.c index d600281828..3eb16c83a5 100644 --- a/src/detection/mouse/mouse_windows.c +++ b/src/detection/mouse/mouse_windows.c @@ -1,6 +1,7 @@ #define INITGUID #include "mouse.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" #include "common/windows/unicode.h" @@ -13,13 +14,16 @@ const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { UINT nDevices = 0; if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + FF_DEBUG("GetRawInputDeviceList(nullptr) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(nullptr) failed"; } if (nDevices == 0) { + FF_DEBUG("No HID devices found"); return "No HID devices found"; } FF_AUTO_FREE RAWINPUTDEVICELIST* pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + FF_DEBUG("GetRawInputDeviceList(pRawInputDeviceList) failed: %s", ffDebugWin32Error(GetLastError())); return "GetRawInputDeviceList(pRawInputDeviceList) failed"; } diff --git a/src/detection/netio/netio_apple.c b/src/detection/netio/netio_apple.c index 1ab2c5774e..530aee8209 100644 --- a/src/detection/netio/netio_apple.c +++ b/src/detection/netio/netio_apple.c @@ -1,8 +1,10 @@ #include "netio.h" +#include "common/debug.h" #include "common/netif.h" #include "common/mallocHelper.h" +#include #include #include #include @@ -12,6 +14,7 @@ const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { size_t bufSize = 0; if (sysctl(mib, ARRAY_SIZE(mib), nullptr, &bufSize, 0, 0) < 0) { + FF_DEBUG("sysctl(mib, ARRAY_SIZE(mib), nullptr, &bufSize, 0, 0) failed: %s", strerror(errno)); return "sysctl(mib, ARRAY_SIZE(mib), nullptr, &bufSize, 0, 0) failed"; } @@ -19,6 +22,7 @@ const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { FF_AUTO_FREE struct ifmibdata* buf = (struct ifmibdata*) malloc(bufSize); if (sysctl(mib, ARRAY_SIZE(mib), buf, &bufSize, 0, 0) < 0) { + FF_DEBUG("sysctl(mib, ARRAY_SIZE(mib), buf, &bufSize, 0, 0) failed: %s", strerror(errno)); return "sysctl(mib, ARRAY_SIZE(mib), buf, &bufSize, 0, 0) failed"; } diff --git a/src/detection/netio/netio_bsd.c b/src/detection/netio/netio_bsd.c index 2641dd2493..1c938e713d 100644 --- a/src/detection/netio/netio_bsd.c +++ b/src/detection/netio/netio_bsd.c @@ -1,8 +1,10 @@ #include "netio.h" +#include "common/debug.h" #include "common/netif.h" #include "common/mallocHelper.h" +#include #include #include #include @@ -15,11 +17,13 @@ const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { size_t bufSize = 0; if (sysctl((int[]) { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, (options->defaultRouteOnly ? (int) defaultRouteIfIndex : 0) }, 6, nullptr, &bufSize, 0, 0) < 0) { + FF_DEBUG("sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, nullptr, &bufSize, 0, 0) failed: %s", strerror(errno)); return "sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, nullptr, &bufSize, 0, 0) failed"; } FF_AUTO_FREE struct if_msghdr* buf = (struct if_msghdr*) malloc(bufSize); if (sysctl((int[]) { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, (options->defaultRouteOnly ? (int) defaultRouteIfIndex : 0) }, 6, buf, &bufSize, 0, 0) < 0) { + FF_DEBUG("sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, buf, &bufSize, 0, 0) failed: %s", strerror(errno)); return "sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, buf, &bufSize, 0, 0) failed"; } diff --git a/src/detection/netio/netio_linux.c b/src/detection/netio/netio_linux.c index 4e5c8d61b7..2daf23bd0d 100644 --- a/src/detection/netio/netio_linux.c +++ b/src/detection/netio/netio_linux.c @@ -1,5 +1,6 @@ #include "netio.h" +#include "common/debug.h" #include "common/io.h" #include "common/netif.h" #include "common/strutil.h" @@ -58,6 +59,7 @@ static void getData(FFstrbuf* buffer, const char* ifName, bool isDefaultRoute, i const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/net"); if (!dirp) { + FF_DEBUG("opendir(\"/sys/class/net\") failed: %s", strerror(errno)); return "opendir(\"/sys/class/net\") == nullptr"; } diff --git a/src/detection/netio/netio_sunos.c b/src/detection/netio/netio_sunos.c index 2b9a020387..df580646b6 100644 --- a/src/detection/netio/netio_sunos.c +++ b/src/detection/netio/netio_sunos.c @@ -1,4 +1,5 @@ #include "netio.h" +#include "common/debug.h" #include "common/netif.h" #include "common/strutil.h" @@ -14,6 +15,7 @@ static inline void kstatFreeWrap(kstat_ctl_t** pkc) { const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { [[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); if (!kc) { + FF_DEBUG("kstat_open() failed"); return "kstat_open() failed"; } diff --git a/src/detection/netio/netio_windows.c b/src/detection/netio/netio_windows.c index 4c3f87b3ea..39cdd803fc 100644 --- a/src/detection/netio/netio_windows.c +++ b/src/detection/netio/netio_windows.c @@ -1,71 +1,82 @@ #include "netio.h" +#include "common/debug.h" #include "common/netif.h" -#include "common/mallocHelper.h" #include "common/windows/unicode.h" #include #include -const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { - FF_AUTO_FREE IP_ADAPTER_ADDRESSES* adapter_addresses = nullptr; - - // Multiple attempts in case interfaces change while - // we are in the middle of querying them. - DWORD adapter_addresses_buffer_size = 0; - for (int attempts = 0;; ++attempts) { - if (adapter_addresses_buffer_size) { - adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); - assert(adapter_addresses); - } +static void addInterface(FFlist* result, const FFNetIOOptions* options, NET_IFINDEX ifIndex, bool isDefaultRoute) { + MIB_IF_ROW2 ifRow = { .InterfaceIndex = ifIndex }; + if (!NETIO_SUCCESS(GetIfEntry2(&ifRow))) { + return; + } - DWORD error = GetAdaptersAddresses( - AF_UNSPEC, - GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, - nullptr, - adapter_addresses, - &adapter_addresses_buffer_size); + // localip reports the interfaces that are operational only, so the same filter is applied here + if (ifRow.OperStatus != IfOperStatusUp) { + return; + } - if (error == ERROR_SUCCESS) { - break; - } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { - continue; - } else { - return "GetAdaptersAddresses() failed"; - } + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(ifRow.Alias); + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + return; } + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + *counters = (FFNetIOResult) { + .name = ffStrbufCreateMove(&name), + .txBytes = ifRow.OutOctets, + .rxBytes = ifRow.InOctets, + .txPackets = (ifRow.OutUcastPkts + ifRow.OutNUcastPkts), + .rxPackets = (ifRow.InUcastPkts + ifRow.InNUcastPkts), + .rxErrors = ifRow.InErrors, + .txErrors = ifRow.OutErrors, + .rxDrops = ifRow.InDiscards, + .txDrops = ifRow.OutDiscards, + .defaultRoute = isDefaultRoute, + }; +} + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; - // Iterate through all of the adapters - for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { - bool isDefaultRoute = adapter->IfIndex == defaultRouteIfIndex; - if (options->defaultRouteOnly && !isDefaultRoute) { - continue; - } + // With `defaultRouteOnly` (the default) the interface carrying the default route is the only one + // that can be reported, so it is queried with the single entry API instead of walking a table of + // every interface: GetIfEntry2() on one interface costs about 12 us. + if (options->defaultRouteOnly) { + addInterface(result, options, (NET_IFINDEX) defaultRouteIfIndex, true); + return nullptr; + } - FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(adapter->FriendlyName); - if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + // The interface set is taken from the unicast address table, the same source localip uses, so + // both modules report the same interfaces. Walking the interface table instead would also report + // the NDIS filter layers of every adapter (Npcap, QoS, WFP, ...) and the adapters that carry no + // address at all, and those cannot be told apart from real adapters by any MIB_IF_ROW2 field. + // GetUnicastIpAddressTable() costs about 72 us, GetIfTable2() about 860 us. + PMIB_UNICASTIPADDRESS_TABLE addressTable = nullptr; + DWORD ret = GetUnicastIpAddressTable(AF_UNSPEC, &addressTable); + if (!NETIO_SUCCESS(ret)) { + FF_DEBUG("GetUnicastIpAddressTable() failed: %s", ffDebugWin32Error(ret)); + return "GetUnicastIpAddressTable() failed"; + } + + for (ULONG i = 0; i < addressTable->NumEntries; ++i) { + NET_IFINDEX ifIndex = addressTable->Table[i].InterfaceIndex; + + // Rows of the same interface are not adjacent (IPv4 rows come first), + // so each interface is handled at its first row + bool seen = false; + for (ULONG j = 0; j < i && !seen; ++j) { + seen = addressTable->Table[j].InterfaceIndex == ifIndex; + } + if (seen) { continue; } - MIB_IF_ROW2 ifRow = { .InterfaceIndex = adapter->IfIndex }; - if (GetIfEntry2(&ifRow) == NO_ERROR) { - FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); - *counters = (FFNetIOResult) { - .name = ffStrbufCreateMove(&name), - .txBytes = ifRow.OutOctets, - .rxBytes = ifRow.InOctets, - .txPackets = (ifRow.OutUcastPkts + ifRow.OutNUcastPkts), - .rxPackets = (ifRow.InUcastPkts + ifRow.InNUcastPkts), - .rxErrors = ifRow.InErrors, - .txErrors = ifRow.OutErrors, - .rxDrops = ifRow.InDiscards, - .txDrops = ifRow.OutDiscards, - .defaultRoute = isDefaultRoute, - }; - } + addInterface(result, options, ifIndex, ifIndex == defaultRouteIfIndex); } + FreeMibTable(addressTable); return nullptr; } diff --git a/src/detection/opengl/opengl_apple.c b/src/detection/opengl/opengl_apple.c index d29ae284a4..0f994175d1 100644 --- a/src/detection/opengl/opengl_apple.c +++ b/src/detection/opengl/opengl_apple.c @@ -1,6 +1,7 @@ #include "fastfetch.h" #include "opengl.h" +#include "common/debug.h" #define GL_SILENCE_DEPRECATION #include @@ -9,7 +10,9 @@ void ffOpenGLHandleResult(FFOpenGLResult* result, typeof(&glGetString) ffglGetString); static const char* cglHandleContext(FFOpenGLResult* result, CGLContextObj context) { - if (CGLSetCurrentContext(context) != kCGLNoError) { + CGLError setContextError = CGLSetCurrentContext(context); + if (setContextError != kCGLNoError) { + FF_DEBUG("CGLSetCurrentContext() failed: CGLError %d", (int) setContextError); return "CGLSetCurrentContext() failed"; } @@ -25,7 +28,9 @@ static const char* cglHandleContext(FFOpenGLResult* result, CGLContextObj contex static const char* cglHandlePixelFormat(FFOpenGLResult* result, CGLPixelFormatObj pixelFormat) { CGLContextObj context; - if (CGLCreateContext(pixelFormat, nullptr, &context) != kCGLNoError) { + CGLError createContextError = CGLCreateContext(pixelFormat, nullptr, &context); + if (createContextError != kCGLNoError) { + FF_DEBUG("CGLCreateContext() failed: CGLError %d", (int) createContextError); return "CGLCreateContext() failed"; } @@ -41,7 +46,9 @@ const char* cglDetectOpenGL(FFOpenGLResult* result) { }; GLint num; - if (CGLChoosePixelFormat(attrs, &pixelFormat, &num) != kCGLNoError) { + CGLError choosePixelFormatError = CGLChoosePixelFormat(attrs, &pixelFormat, &num); + if (choosePixelFormatError != kCGLNoError) { + FF_DEBUG("CGLChoosePixelFormat() failed: CGLError %d", (int) choosePixelFormatError); return "CGLChoosePixelFormat() failed"; } @@ -61,6 +68,7 @@ const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { return "fastfetch was compiled without egl support"; #endif } else { + FF_DEBUG("Unsupported OpenGL library requested: %d", (int) options->library); return "Unsupported OpenGL library"; } } diff --git a/src/detection/opengl/opengl_haiku.cpp b/src/detection/opengl/opengl_haiku.cpp index 47d55500eb..4fd47707ff 100644 --- a/src/detection/opengl/opengl_haiku.cpp +++ b/src/detection/opengl/opengl_haiku.cpp @@ -2,6 +2,7 @@ extern "C" { #include "opengl.h" +#include "common/debug.h" #include "common/io.h" #if FF_HAVE_EGL const char* ffOpenGLDetectByEGL(FFOpenGLResult* result); @@ -16,6 +17,7 @@ static const char* oglDetectOpenGL(FFOpenGLResult* result) { BGLView glView(BRect(), "ff_ogl_view", B_FOLLOW_NONE, B_WILL_DRAW, BGL_RGB); auto ffglGetString = (decltype(&glGetString)) glView.GetGLProcAddress("glGetString"); if (!ffglGetString) { + FF_DEBUG("glView.GetGLProcAddress(glGetString) returned null"); return "glView.GetGLProcAddress() failed"; } ffOpenGLHandleResult(result, ffglGetString); @@ -33,6 +35,7 @@ const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { return "fastfetch was compiled without egl support"; #endif } else { + FF_DEBUG("Unsupported OpenGL library requested: %d", (int) options->library); return "Unsupported OpenGL library"; } } diff --git a/src/detection/opengl/opengl_linux.c b/src/detection/opengl/opengl_linux.c index d63843b90c..a8dd680d7e 100644 --- a/src/detection/opengl/opengl_linux.c +++ b/src/detection/opengl/opengl_linux.c @@ -1,12 +1,16 @@ #include "fastfetch.h" #include "opengl.h" +#include "common/debug.h" #include "common/io.h" #include #if __ANDROID__ && !defined(FF_HAVE_EGL) - // On Android, installing OpenGL headers is enough (mesa-dev) - #if __has_include() + // On Android the EGL headers ship with the NDK, but the desktop GL ones only come from mesa-dev, + // which a plain NDK build does not have. The block below includes , so that is the + // header to probe: probing turns this on for every NDK build and then fails to + // compile. Termux installs mesa-dev, so it still gets the EGL path. + #if __has_include() && __has_include() #define FF_HAVE_EGL 1 #endif #endif @@ -50,6 +54,7 @@ typedef struct GLXData { static const char* glxHandleContext(FFOpenGLResult* result, GLXData* data) { if (data->ffglXMakeCurrent(data->display, data->glxPixmap, data->context) != True) { + FF_DEBUG("glXMakeCurrent() returned False"); return "glXMakeCurrent returned False"; } ffOpenGLHandleResult(result, data->ffglGetString); @@ -67,6 +72,7 @@ static const char* glxHandleContext(FFOpenGLResult* result, GLXData* data) { static const char* glxHandleGLXPixmap(FFOpenGLResult* result, GLXData* data) { data->context = data->ffglXCreateContext(data->display, data->visualInfo, nullptr, True); if (data->context == nullptr) { + FF_DEBUG("glXCreateContext() returned nullptr"); return "glXCreateContext returned nullptr"; } @@ -78,6 +84,7 @@ static const char* glxHandleGLXPixmap(FFOpenGLResult* result, GLXData* data) { static const char* glxHandlePixmap(FFOpenGLResult* result, GLXData* data) { data->glxPixmap = data->ffglXCreateGLXPixmap(data->display, data->visualInfo, data->pixmap); if (data->glxPixmap == None) { + FF_DEBUG("glXCreateGLXPixmap() returned None"); return "glXCreateGLXPixmap returned None"; } @@ -89,6 +96,7 @@ static const char* glxHandlePixmap(FFOpenGLResult* result, GLXData* data) { static const char* glxHandleVisualInfo(FFOpenGLResult* result, GLXData* data) { data->pixmap = data->ffXCreatePixmap(data->display, DefaultRootWindow(data->display), FF_OPENGL_BUFFER_WIDTH, FF_OPENGL_BUFFER_HEIGHT, (unsigned int) data->visualInfo->depth); if (data->pixmap == None) { + FF_DEBUG("XCreatePixmap() returned None"); return "XCreatePixmap returned None"; } @@ -100,6 +108,7 @@ static const char* glxHandleVisualInfo(FFOpenGLResult* result, GLXData* data) { static const char* glxHandleDisplay(FFOpenGLResult* result, GLXData* data) { data->visualInfo = data->ffglXChooseVisual(data->display, DefaultScreen(data->display), (int[]) { None }); if (data->visualInfo == nullptr) { + FF_DEBUG("glXChooseVisual() returned nullptr"); return "glXChooseVisual returned nullptr"; } @@ -111,11 +120,13 @@ static const char* glxHandleDisplay(FFOpenGLResult* result, GLXData* data) { static const char* glxHandleData(FFOpenGLResult* result, GLXData* data) { data->ffglGetString = (typeof(data->ffglGetString)) data->ffglXGetProcAddress((const GLubyte*) "glGetString"); if (data->ffglGetString == nullptr) { + FF_DEBUG("glXGetProcAddress(glGetString) returned nullptr"); return "glXGetProcAddress(glGetString) returned nullptr"; } data->display = data->ffXOpenDisplay(nullptr); if (data->display == nullptr) { + FF_DEBUG("XOpenDisplay() returned nullptr"); return "XOpenDisplay returned nullptr"; } diff --git a/src/detection/opengl/opengl_windows.c b/src/detection/opengl/opengl_windows.c index 5b2262cbb7..61c991cbec 100644 --- a/src/detection/opengl/opengl_windows.c +++ b/src/detection/opengl/opengl_windows.c @@ -1,4 +1,5 @@ #include "opengl.h" +#include "common/debug.h" #include "common/library.h" #include "common/printing.h" #include "common/windows/nt.h" @@ -17,11 +18,13 @@ void ffOpenGLHandleResult(FFOpenGLResult* result, typeof(&glGetString) ffglGetSt static const char* wglHandleContext(WGLData* wglData, FFOpenGLResult* result, HDC hdc, HGLRC context) { if (wglData->ffwglMakeCurrent(hdc, context) == FALSE) { + FF_DEBUG("wglMakeCurrent() failed: %s", ffDebugWin32Error(GetLastError())); return "wglMakeCurrent() failed"; } ffOpenGLHandleResult(result, wglData->ffglGetString); ffStrbufSetStatic(&result->library, "WGL 1.0"); if (wglData->ffwglMakeCurrent(nullptr, nullptr) == FALSE) { + FF_DEBUG("wglMakeCurrent(nullptr, nullptr) failed: %s", ffDebugWin32Error(GetLastError())); return "wglMakeCurrent(nullptr, nullptr) failed"; } return nullptr; @@ -31,6 +34,7 @@ static const char* wglHandlePixelFormat(WGLData* wglData, FFOpenGLResult* result HDC hdc = GetDC(hWnd); if (hdc == nullptr) { + FF_DEBUG("GetDC() failed: %s", ffDebugWin32Error(GetLastError())); return "GetDC() failed"; } @@ -45,17 +49,20 @@ static const char* wglHandlePixelFormat(WGLData* wglData, FFOpenGLResult* result }; int pixelFormat = ChoosePixelFormat(hdc, &pfd); if (pixelFormat == 0) { + FF_DEBUG("ChoosePixelFormat() failed: %s", ffDebugWin32Error(GetLastError())); ReleaseDC(hWnd, hdc); return "ChoosePixelFormat() failed"; } if (SetPixelFormat(hdc, pixelFormat, &pfd) == FALSE) { + FF_DEBUG("SetPixelFormat() failed: %s", ffDebugWin32Error(GetLastError())); ReleaseDC(hWnd, hdc); return "SetPixelFormat() failed"; } HGLRC context = wglData->ffwglCreateContext(hdc); if (context == nullptr) { + FF_DEBUG("wglCreateContext() failed: %s", ffDebugWin32Error(GetLastError())); ReleaseDC(hWnd, hdc); return "wglCreateContext() failed"; } @@ -88,11 +95,13 @@ static const char* wglDetectOpenGL(FFOpenGLResult* result) { .style = CS_OWNDC, }; if (!RegisterClassW(&wc)) { + FF_DEBUG("RegisterClassW() failed: %s", ffDebugWin32Error(GetLastError())); return "RegisterClassW() failed"; } HWND hWnd = CreateWindowW(wc.lpszClassName, L"ogl_version_check", 0, 0, 0, FF_OPENGL_BUFFER_WIDTH, FF_OPENGL_BUFFER_HEIGHT, nullptr, nullptr, hInstance, nullptr); if (!hWnd) { + FF_DEBUG("CreateWindowW() failed: %s", ffDebugWin32Error(GetLastError())); return "CreateWindowW() failed"; } @@ -112,9 +121,11 @@ const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { const char* ffOpenGLDetectByEGL(FFOpenGLResult * result); return ffOpenGLDetectByEGL(result); #else + FF_DEBUG("fastfetch was compiled without egl support"); return "fastfetch was compiled without egl support"; #endif } else { + FF_DEBUG("Unsupported OpenGL library"); return "Unsupported OpenGL library"; } } diff --git a/src/detection/packages/packages.c b/src/detection/packages/packages.c index 312c8aad59..5f82d50ecd 100644 --- a/src/detection/packages/packages.c +++ b/src/detection/packages/packages.c @@ -70,7 +70,147 @@ bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t n return ffWriteFileBuffer(cacheDir->chars, cacheContent); } -#ifndef _WIN32 +#if __linux__ +#include + +struct linux_dirent64 { + uint64_t d_ino; /* 64-bit inode number */ + int64_t d_off; /* Not an offset; see getdents() */ + unsigned short d_reclen; /* Size of this dirent */ + unsigned char d_type; /* File type */ + char d_name[]; /* Filename (null-terminated) */ +}; + +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) { + FF_AUTO_CLOSE_FD int fd = open(dirname, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) { + return 0; + } + + alignas(struct linux_dirent64) uint8_t bytes[64 * 1024]; + + uint32_t num_elements = 0; + const size_t nameOffset = offsetof(struct linux_dirent64, d_name); + + for (;;) { + long bytesRead = syscall(SYS_getdents64, fd, bytes, sizeof(bytes)); + if (bytesRead < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (bytesRead == 0) { + break; + } + + size_t remaining = (size_t) bytesRead; + struct linux_dirent64* entry = (struct linux_dirent64*) bytes; + + while (remaining >= nameOffset + 1) { + if (entry->d_reclen < nameOffset + 1 || entry->d_reclen > remaining) { + remaining = 0; + break; + } + + bool ok = false; + if (entry->d_name[0] != '.') { + if (__builtin_expect(entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK, true)) { + ok = entry->d_type == (isdir ? DT_DIR : DT_REG); + } else { + struct stat stbuf; + if (fstatat(fd, entry->d_name, &stbuf, 0) == 0) { + ok = isdir ? S_ISDIR(stbuf.st_mode) : S_ISREG(stbuf.st_mode); + } + } + } + + num_elements += ok; + size_t recordLength = entry->d_reclen; + remaining -= recordLength; + entry = (struct linux_dirent64*) ((uint8_t*) entry + recordLength); + } + + if (remaining != 0) { + break; + } + } + + return num_elements; +} + +#elif __APPLE__ + +#pragma clang diagnostic ignored "-Wdeprecated-declarations" // syscall + +#include + +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) { + FF_AUTO_CLOSE_FD int fd = open(dirname, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) { + return 0; + } + + alignas(struct dirent) uint8_t bytes[64 * 1024]; + off_t seek = 0; + uint32_t num_elements = 0; + const size_t nameOffset = offsetof(struct dirent, d_name); + + for (;;) { + // getdirentries64 stores its EOF indicator in the final four bytes. + uint32_t* eofFlag = (uint32_t*) (bytes + sizeof(bytes) - sizeof(uint32_t)); + *eofFlag = 0; + + long bytesRead = syscall(SYS_getdirentries64, fd, bytes, sizeof(bytes), &seek); + if (bytesRead < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (bytesRead == 0 || (size_t) bytesRead > sizeof(bytes)) { + break; + } + + size_t remaining = (size_t) bytesRead; + struct dirent* entry = (struct dirent*) bytes; + while (remaining >= nameOffset + 1) { + assert(((uintptr_t) entry) % alignof(struct dirent) == 0); + + if (entry->d_reclen < nameOffset + 1 || entry->d_reclen > remaining || + entry->d_namlen >= entry->d_reclen - nameOffset) { + remaining = 0; + break; + } + + const char* name = (const char*) entry + nameOffset; + if (entry->d_ino != 0 && name[0] != '.') { + bool ok = false; + if (__builtin_expect(entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK, true)) { + ok = entry->d_type == (isdir ? DT_DIR : DT_REG); + } else { + struct stat stbuf; + if (fstatat(fd, name, &stbuf, 0) == 0) { + ok = isdir ? S_ISDIR(stbuf.st_mode) : S_ISREG(stbuf.st_mode); + } + } + + num_elements += ok; + } + + remaining -= entry->d_reclen; + entry = (struct dirent*) ((uint8_t*) entry + entry->d_reclen); + } + + if ((size_t) bytesRead <= sizeof(bytes) - sizeof(uint32_t) && *eofFlag == 1) { + break; + } + } + + return num_elements; +} + +#elif !_WIN32 uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir(dirname); if (dirp == nullptr) { diff --git a/src/detection/packages/packages_apple.c b/src/detection/packages/packages_apple.c index 43de97df8f..17bc857477 100644 --- a/src/detection/packages/packages_apple.c +++ b/src/detection/packages/packages_apple.c @@ -46,6 +46,14 @@ void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) result->macports = getMacPortsPackages(&baseDir); } + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + const char* prefix = getenv("PKG_DBDIR"); + if (ffStrSet(prefix)) { + result->pkgsrc = ffPackagesGetNumElements(prefix, true); + } else { + result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true); + } + } if (FF_PACKAGES_IS_ENABLED(options, NIX)) { ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default"); diff --git a/src/detection/packages/packages_windows.c b/src/detection/packages/packages_windows.c index 3325c15a23..2c9e1ed54c 100644 --- a/src/detection/packages/packages_windows.c +++ b/src/detection/packages/packages_windows.c @@ -2,6 +2,7 @@ #include "common/processing.h" #include "common/strutil.h" #include "common/path.h" +#include "common/windows/folders.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" #include "common/mallocHelper.h" @@ -11,7 +12,6 @@ #include #include "common/windows/nt.h" #include -#include static uint32_t getNumElements(const char* searchPath, DWORD type, const wchar_t* ignore) { FF_AUTO_CLOSE_FD HANDLE dfd = CreateFileA(searchPath, FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); @@ -120,11 +120,9 @@ static void detectScoop(FFPackagesResult* result) { ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "global_path")); } if (scoopPath.length == 0) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramData, KF_FLAG_DEFAULT, nullptr, &pPath))) { - ffStrbufSetWS(&scoopPath, pPath); - CoTaskMemFree(pPath); - } + // Leaving `scoopPath` empty when the lookup fails makes `getNumElements` report 0, which + // is what the shell API failing used to do as well. + ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_DATA, &scoopPath); ffStrbufAppendS(&scoopPath, "/scoop"); } ffStrbufAppendS(&scoopPath, "/apps/"); diff --git a/src/detection/physicaldisk/physicaldisk_apple.c b/src/detection/physicaldisk/physicaldisk_apple.c index 47098c9441..789ab624d5 100644 --- a/src/detection/physicaldisk/physicaldisk_apple.c +++ b/src/detection/physicaldisk/physicaldisk_apple.c @@ -1,5 +1,6 @@ #include "physicaldisk.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include #include @@ -25,11 +26,13 @@ static const char* detectSsdTemp(io_service_t entryPhysical, double* temp) { [[gnu::cleanup(wrapIoDestroyPlugInInterface)]] IOCFPlugInInterface** pluginInf = nullptr; int32_t score; if (IOCreatePlugInInterfaceForService(entryPhysical, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, &pluginInf, &score) != kIOReturnSuccess) { + FF_DEBUG("IOCreatePlugInInterfaceForService(kIONVMeSMARTUserClientTypeID) failed"); return "IOCreatePlugInInterfaceForService() failed"; } IONVMeSMARTInterface** smartInf = nullptr; if ((*pluginInf)->QueryInterface(pluginInf, CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), (LPVOID) &smartInf) != kIOReturnSuccess) { + FF_DEBUG("QueryInterface(kIONVMeSMARTInterfaceID) failed"); return "QueryInterface() failed"; } @@ -44,6 +47,7 @@ static const char* detectSsdTemp(io_service_t entryPhysical, double* temp) { (*pluginInf)->Release(smartInf); return error; #else + FF_DEBUG("SSD temperature detection needs macOS 10.15 or newer"); return "No support for old MacOS version"; #endif } @@ -51,6 +55,7 @@ static const char* detectSsdTemp(io_service_t entryPhysical, double* temp) { const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = 0; if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(kIOBlockStorageDriverClass), &iterator) != KERN_SUCCESS) { + FF_DEBUG("IOServiceGetMatchingServices(kIOBlockStorageDriverClass) failed"); return "IOServiceGetMatchingServices() failed"; } diff --git a/src/detection/physicaldisk/physicaldisk_bsd.c b/src/detection/physicaldisk/physicaldisk_bsd.c index d17905388b..5d59fd9d91 100644 --- a/src/detection/physicaldisk/physicaldisk_bsd.c +++ b/src/detection/physicaldisk/physicaldisk_bsd.c @@ -2,6 +2,7 @@ #if __has_include() + #include "common/debug.h" #include "common/strutil.h" #include @@ -14,10 +15,12 @@ const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { struct gmesh geomTree; if (geom_gettree(&geomTree) < 0) { + FF_DEBUG("geom_gettree() failed"); return "geom_gettree() failed"; } if (geom_stats_open() < 0) { + FF_DEBUG("geom_stats_open() failed"); return "geom_stats_open() failed"; } diff --git a/src/detection/physicaldisk/physicaldisk_haiku.c b/src/detection/physicaldisk/physicaldisk_haiku.c index d2b83fed29..ab5599658c 100644 --- a/src/detection/physicaldisk/physicaldisk_haiku.c +++ b/src/detection/physicaldisk/physicaldisk_haiku.c @@ -1,5 +1,6 @@ #include "physicaldisk.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -11,6 +12,7 @@ static const char* searchRawDeviceFile(FFstrbuf* path, const char* diskType, FFlist* result, FFPhysicalDiskOptions* options) { FF_AUTO_CLOSE_DIR DIR* dir = opendir(path->chars); if (!dir) { + FF_DEBUG("opendir(\"%s\") failed: %s", path->chars, strerror(errno)); return "detectDiskType: opendir() failed"; } uint32_t baseLen = path->length; @@ -83,6 +85,7 @@ static const char* searchRawDeviceFile(FFstrbuf* path, const char* diskType, FFl const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { FF_AUTO_CLOSE_DIR DIR* dir = opendir("/dev/disk"); if (!dir) { + FF_DEBUG("opendir(\"/dev/disk\") failed: %s", strerror(errno)); return "opendir(/dev/disk) failed"; } diff --git a/src/detection/physicaldisk/physicaldisk_linux.c b/src/detection/physicaldisk/physicaldisk_linux.c index 525e391bf5..7a9fd5f798 100644 --- a/src/detection/physicaldisk/physicaldisk_linux.c +++ b/src/detection/physicaldisk/physicaldisk_linux.c @@ -1,4 +1,5 @@ #include "physicaldisk.h" +#include "common/debug.h" #include "common/io.h" #include "common/properties.h" #include "common/strutil.h" @@ -178,6 +179,7 @@ static void parsePhysicalDisk(int dfd, const char* devName, FFPhysicalDiskOption const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { FF_AUTO_CLOSE_DIR DIR* sysBlockDirp = opendir("/sys/block/"); if (sysBlockDirp == nullptr) { + FF_DEBUG("opendir(\"/sys/block/\") failed: %s", strerror(errno)); return "opendir(\"/sys/block/\") == nullptr"; } diff --git a/src/detection/physicaldisk/physicaldisk_sunos.c b/src/detection/physicaldisk/physicaldisk_sunos.c index b65fba000f..9ac2a30efd 100644 --- a/src/detection/physicaldisk/physicaldisk_sunos.c +++ b/src/detection/physicaldisk/physicaldisk_sunos.c @@ -1,4 +1,5 @@ #include "physicaldisk.h" +#include "common/debug.h" #include "common/strutil.h" #include "sys/scsi/generic/inquiry.h" @@ -98,6 +99,7 @@ static int walkDevTree(di_node_t node, di_minor_t minor, struct FFWalkTreeBundle const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { di_node_t rootNode = di_init("/", DINFOCPYALL); if (rootNode == DI_NODE_NIL) { + FF_DEBUG("di_init() failed"); return "di_init() failed"; } di_walk_minor(rootNode, DDI_NT_BLOCK, DI_WALK_CLDFIRST, &(struct FFWalkTreeBundle) { options, result }, (void*) walkDevTree); diff --git a/src/detection/physicaldisk/physicaldisk_windows.c b/src/detection/physicaldisk/physicaldisk_windows.c index 56ae1d314d..3d6dccc2f0 100644 --- a/src/detection/physicaldisk/physicaldisk_windows.c +++ b/src/detection/physicaldisk/physicaldisk_windows.c @@ -12,6 +12,7 @@ static const char* detectPhysicalDisk(const char* physicalType, const wchar_t* szDevice, FFlist* result, FFPhysicalDiskOptions* options) { FF_AUTO_CLOSE_FD HANDLE hDevice = CreateFileW(szDevice, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); if (hDevice == INVALID_HANDLE_VALUE) { + FF_DEBUG("CreateFileW() failed: %s", ffDebugWin32Error(GetLastError())); return "CreateFileW() failed"; } diff --git a/src/detection/physicalmemory/physicalmemory_linux.c b/src/detection/physicalmemory/physicalmemory_linux.c index c3e3fae960..6abd1021bd 100644 --- a/src/detection/physicalmemory/physicalmemory_linux.c +++ b/src/detection/physicalmemory/physicalmemory_linux.c @@ -1,4 +1,5 @@ #include "physicalmemory.h" +#include "common/debug.h" #include "common/endian.h" #include "common/smbios.h" @@ -68,11 +69,13 @@ static_assert(offsetof(FFSmbiosMemoryDevice, RcdRevisionNumber) == 0x62, const char* ffDetectPhysicalMemory(FFPhysicalMemoryOptions* options, FFlist* result) { const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); if (!smbiosTable) { + FF_DEBUG("Failed to get SMBIOS data"); return "Failed to get SMBIOS data"; } const FFSmbiosMemoryDevice* data = (const FFSmbiosMemoryDevice*) (*smbiosTable)[FF_SMBIOS_TYPE_MEMORY_DEVICE]; if (!data) { + FF_DEBUG("Memory device is not found in SMBIOS data"); return "Memory device is not found in SMBIOS data"; } diff --git a/src/detection/poweradapter/poweradapter_linux.c b/src/detection/poweradapter/poweradapter_linux.c index fd0abcc1dc..4a3375dc2e 100644 --- a/src/detection/poweradapter/poweradapter_linux.c +++ b/src/detection/poweradapter/poweradapter_linux.c @@ -1,10 +1,12 @@ #include "poweradapter.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" #include #include #include +#include static void parsePowerAdapter(int dfd, const char* id, FFlist* results) { FF_STRBUF_AUTO_DESTROY tmpBuffer = ffStrbufCreate(); @@ -72,6 +74,7 @@ static void parsePowerAdapter(int dfd, const char* id, FFlist* results) { const char* ffDetectPowerAdapter(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"; } diff --git a/src/detection/processes/processes_apple.c b/src/detection/processes/processes_apple.c index 727aeac02d..5cbc32d659 100644 --- a/src/detection/processes/processes_apple.c +++ b/src/detection/processes/processes_apple.c @@ -1,6 +1,10 @@ #include "processes.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include + #include #import @@ -9,6 +13,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu size_t length; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, nullptr}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, nullptr}) failed"; } @@ -17,6 +22,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu FF_AUTO_FREE struct kinfo_proc* processes = malloc(length); if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, processes}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, processes}) failed"; } diff --git a/src/detection/processes/processes_bsd.c b/src/detection/processes/processes_bsd.c index 32ee53736c..1f70eea377 100644 --- a/src/detection/processes/processes_bsd.c +++ b/src/detection/processes/processes_bsd.c @@ -1,6 +1,9 @@ #include "processes.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include #include #include #include @@ -9,6 +12,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC }; size_t length; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; } @@ -16,6 +20,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu length += length / 8 + sizeof(struct kinfo_proc); FF_AUTO_FREE struct kinfo_proc* procs = malloc(length); if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; } diff --git a/src/detection/processes/processes_haiku.c b/src/detection/processes/processes_haiku.c index feecc34e58..3f0496a2b0 100644 --- a/src/detection/processes/processes_haiku.c +++ b/src/detection/processes/processes_haiku.c @@ -1,10 +1,13 @@ #include "processes.h" +#include "common/debug.h" #include const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { system_info info; - if (get_system_info(&info) != B_OK) { + status_t status = get_system_info(&info); + if (status != B_OK) { + FF_DEBUG("Error getting system info: %d", (int) status); return "Error getting system info"; } diff --git a/src/detection/processes/processes_linux.c b/src/detection/processes/processes_linux.c index 68f819481b..650ab6c75b 100644 --- a/src/detection/processes/processes_linux.c +++ b/src/detection/processes/processes_linux.c @@ -1,14 +1,18 @@ #include "processes.h" +#include "common/debug.h" #include "common/io.h" #include "common/memrchr.h" #include "common/strutil.h" +#include + #define PF_KTHREAD 0x00200000 // defined in kernel include/linux/sched.h const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { FF_AUTO_CLOSE_DIR DIR* dir = opendir("/proc"); if (dir == nullptr) { + FF_DEBUG("opendir(/proc) failed: %s", strerror(errno)); return "opendir(\"/proc\") failed"; } diff --git a/src/detection/processes/processes_nbsd.c b/src/detection/processes/processes_nbsd.c index cb91db096f..7a7bc5e4d5 100644 --- a/src/detection/processes/processes_nbsd.c +++ b/src/detection/processes/processes_nbsd.c @@ -1,6 +1,9 @@ #include "processes.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include #include const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { @@ -8,11 +11,13 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu size_t length = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, nullptr}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, nullptr}) failed"; } FF_AUTO_FREE struct kinfo_proc2* procs = malloc(length); if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, procs}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, procs}) failed"; } diff --git a/src/detection/processes/processes_obsd.c b/src/detection/processes/processes_obsd.c index e07d45b1b6..37fb9ab6f6 100644 --- a/src/detection/processes/processes_obsd.c +++ b/src/detection/processes/processes_obsd.c @@ -1,7 +1,10 @@ #include "processes.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include #include #include @@ -10,12 +13,14 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu size_t length = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed"; } FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, procs) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, procs) failed"; } diff --git a/src/detection/processes/processes_sunos.c b/src/detection/processes/processes_sunos.c index a815390bb5..e50419fa97 100644 --- a/src/detection/processes/processes_sunos.c +++ b/src/detection/processes/processes_sunos.c @@ -1,5 +1,6 @@ #include "processes.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -8,6 +9,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { FF_AUTO_CLOSE_DIR DIR* dir = opendir("/proc"); if (dir == nullptr) { + FF_DEBUG("opendir(\"/proc\") failed: %s", strerror(errno)); return "opendir(\"/proc\") failed"; } diff --git a/src/detection/processes/processes_windows.c b/src/detection/processes/processes_windows.c index b5d65b3017..09c8741352 100644 --- a/src/detection/processes/processes_windows.c +++ b/src/detection/processes/processes_windows.c @@ -1,4 +1,5 @@ #include "processes.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include "common/windows/nt.h" @@ -34,6 +35,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; } else { + FF_DEBUG("NtQuerySystemInformation(SystemProcessInformation) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessInformation) failed"; } } diff --git a/src/detection/publicip/publicip.c b/src/detection/publicip/publicip.c index cd850efdc1..807d34be71 100644 --- a/src/detection/publicip/publicip.c +++ b/src/detection/publicip/publicip.c @@ -31,7 +31,6 @@ void ffPreparePublicIp(FFPublicIPOptions* options) { state->ipv6 = options->ipv6; if (options->url.length == 0) { - state->compression = true; state->tfo = true; *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", 80, "/json", nullptr); } else { diff --git a/src/detection/sound/sound_android.c b/src/detection/sound/sound_android.c new file mode 100644 index 0000000000..de1ca1a87f --- /dev/null +++ b/src/detection/sound/sound_android.c @@ -0,0 +1,684 @@ +#include "sound.h" + +#include "detection/displayserver/displayserver.h" +#include "common/android/binder.h" +#include "common/android/dex.h" +#include "common/debug.h" +#include "common/strutil.h" + +#include + +// Android has no desktop sound stack. PulseAudio is not part of the platform, but a Termux user can +// run one, and that is what a session sounds through whenever the display server is a real desktop +// rather than SurfaceFlinger -- so this file answers for SurfaceFlinger alone and hands the rest to +// sound_linux.c, which speaks PulseAudio. ALSA and OSS are not there either: /dev/snd, /proc/asound +// and /sys/class/sound are all EACCES from an app's SELinux domain, `dumpsys audio` needs +// android.permission.DUMP, and `settings get system volume_music` needs INTERACT_ACROSS_USERS. +// `cmd audio` is worse than useless: `cmd` hands the arguments to the service over binder and the +// command body runs inside system_server, so from an app it hangs or answers ENOSYS. +// +// What is left is the service those all sit on: `audio`, i.e. android.media.IAudioService, which +// lives in system_server. This file speaks to it over /dev/binder, the same way the battery, media +// and wifi modules do. Two things about it are worth stating up front, because they are what makes +// this module work at all: +// +// * Nothing here needs a permission. The methods used below are the ones `AudioManager` itself +// calls on behalf of an ordinary app, and the service checks no permission for them. The one +// that does the real work is deliberately named for it -- see getDevicesForAttributesUnprotected +// below. The protected twin, `getDevicesForAttributes`, is refused with +// "Missing MODIFY_AUDIO_ROUTING or QUERY_AUDIO_STATE permissions", and +// `getLastAudibleStreamVolume` with "requires android.permission.QUERY_AUDIO_STATE"; neither is +// used. So unlike the wifi module, this one works on a device where nothing has been granted. +// +// * Every transaction code is resolved from the device's own jar at run time, never written down. +// `IAudioService$Stub` declares 283 methods in AOSP and 324 on the test device -- a vendor fork +// inserts methods freely, and an insertion ahead of the one being called shifts it. Reading the +// constants out of the dex is the only thing that survives that; see common/android/dex.h. The +// class is in the *second* dex of framework.jar, which is why ffDexStaticInt walks the entries. +// +// The reply is not uniform, because the two kinds of service are not: IAudioService is Java, so a +// failure arrives as a parcel exception code (-1 for a SecurityException, -4 for a null argument), +// while the native services answer with a TF_STATUS_CODE carrying a raw status_t. Only the former +// can happen here, but the check is cheap and the two are easy to confuse. + +#define FF_SOUND_ANDROID_SERVICE "audio" +#define FF_SOUND_ANDROID_DESCRIPTOR "android.media.IAudioService" + +// IAudioService 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 stub is in classes2.dex there. +#define FF_SOUND_ANDROID_JAR "/system/framework/framework.jar" +#define FF_SOUND_ANDROID_STUB "Landroid/media/IAudioService$Stub;" + +// The methods this module calls, by the name of the constant the dex carries for each. Spelling the +// names out rather than the numbers is the whole point: the lookup turns them into whatever this +// build uses, and a build that does not declare one says so instead of reaching another method. +#define FF_SOUND_ANDROID_GET_STREAM_VOLUME "TRANSACTION_getStreamVolume" +#define FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME "TRANSACTION_getStreamMinVolume" +#define FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME "TRANSACTION_getStreamMaxVolume" +#define FF_SOUND_ANDROID_IS_STREAM_MUTE "TRANSACTION_isStreamMute" +#define FF_SOUND_ANDROID_IS_MUSIC_ACTIVE "TRANSACTION_isMusicActive" +#define FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES "TRANSACTION_getDevicesForAttributesUnprotected" +#define FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME "TRANSACTION_getBtActiveDeviceName" + +// `AudioManager.STREAM_MUSIC`. The one stream a phone's volume rocker moves, and the one the module +// reports; the other eleven are internal (SYSTEM_ENFORCED, DTMF, ...) or belong to a call. +typedef enum FFSoundAndroidStream : int32_t { + FF_SOUND_ANDROID_STREAM_MUSIC = 3, +} FFSoundAndroidStream; + +// `AudioAttributes.USAGE_MEDIA`. Asking the policy what it routes for media is what makes the answer +// "where the phone's sound is going"; other usages answer differently on purpose (a call goes to the +// earpiece, an emergency call to the telephony port). +typedef enum FFSoundAndroidUsage : int32_t { + FF_SOUND_ANDROID_USAGE_MEDIA = 1, +} FFSoundAndroidUsage; + +// `AudioAttributes.SOURCE_*`, and the one value that matters here. A request whose source is +// anything but AUDIO_SOURCE_INVALID takes the *capture* branch of +// AudioPolicyManager::getDevicesForAttributes and answers with microphones, whatever the usage says: +// +// if (attr.source != AUDIO_SOURCE_INVALID) return getInputDevicesForAttributes(attr, devices); +// +// AUDIO_SOURCE_DEFAULT is 0, so leaving the field at zero -- which is what a zeroed parcel does -- +// silently reports the built-in microphone as the output device. It has to be written out as -1. +#define FF_SOUND_ANDROID_SOURCE_INVALID (-1) + +// `AudioAttributes.ATTR_PARCEL_IS_NULL_BUNDLE`. The tags and the bundle are the only variable-length +// parts of the request; an empty tag list and no bundle is the smallest well-formed one. +#define FF_SOUND_ANDROID_ATTR_NULL_BUNDLE (-1977) + +// The interface token is 12 bytes of header plus a padded string16, and the largest request here adds +// one AudioAttributes to it. The replies are one int, or one list of AudioDeviceAttributes. The +// device reply is sized well past the 48 bytes a single device takes, because a reply that does not +// fit is reported as a buffer error and would hide the exception message the service sent instead. +#define FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE 96 +#define FF_SOUND_ANDROID_SCALAR_REPLY_SIZE 32 +#define FF_SOUND_ANDROID_DEVICE_PARCEL_SIZE 128 +#define FF_SOUND_ANDROID_DEVICE_REPLY_SIZE 1024 + +// The reply to the Bluetooth device name request is one string16, and a device name is chosen by +// whoever owns the headset: 248 bytes is the longest a Bluetooth name can be, so 512 bytes of reply +// hold the count, the code units, the terminator and the padding with room to spare. A reply that +// does not fit is reported as a buffer error, so sizing it generously is what keeps a long name +// from looking like a failure. +#define FF_SOUND_ANDROID_NAME_REPLY_SIZE 512 + +// `AudioDeviceInfo.TYPE_*`, as the test device's own dex declares them (read out of +// `Landroid/media/AudioDeviceInfo;` -- most recently on Android 17, which had added four). The +// value is what `AudioDeviceAttributes.type` carries in the reply; the names are the AOSP constant +// names without the prefix, which is what goes into `identifier`, and a spelled out name, which is +// what the module prints. An unrecognised value is reported rather than guessed at: a vendor is +// free to add types, and a wrong name is worse than "Unknown Device". +typedef enum FFSoundAndroidDeviceType : int32_t { + FF_SOUND_ANDROID_TYPE_UNKNOWN = 0, + FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE = 1, + FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER = 2, + FF_SOUND_ANDROID_TYPE_WIRED_HEADSET = 3, + FF_SOUND_ANDROID_TYPE_WIRED_HEADPHONES = 4, + FF_SOUND_ANDROID_TYPE_LINE_ANALOG = 5, + FF_SOUND_ANDROID_TYPE_LINE_DIGITAL = 6, + FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO = 7, + FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP = 8, + FF_SOUND_ANDROID_TYPE_HDMI = 9, + FF_SOUND_ANDROID_TYPE_HDMI_ARC = 10, + FF_SOUND_ANDROID_TYPE_USB_DEVICE = 11, + FF_SOUND_ANDROID_TYPE_USB_ACCESSORY = 12, + FF_SOUND_ANDROID_TYPE_DOCK = 13, + FF_SOUND_ANDROID_TYPE_FM = 14, + FF_SOUND_ANDROID_TYPE_BUILTIN_MIC = 15, + FF_SOUND_ANDROID_TYPE_FM_TUNER = 16, + FF_SOUND_ANDROID_TYPE_TV_TUNER = 17, + FF_SOUND_ANDROID_TYPE_TELEPHONY = 18, + FF_SOUND_ANDROID_TYPE_AUX_LINE = 19, + FF_SOUND_ANDROID_TYPE_IP = 20, + FF_SOUND_ANDROID_TYPE_BUS = 21, + FF_SOUND_ANDROID_TYPE_USB_HEADSET = 22, + FF_SOUND_ANDROID_TYPE_HEARING_AID = 23, + FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER_SAFE = 24, + FF_SOUND_ANDROID_TYPE_REMOTE_SUBMIX = 25, + FF_SOUND_ANDROID_TYPE_BLE_HEADSET = 26, + FF_SOUND_ANDROID_TYPE_BLE_SPEAKER = 27, + FF_SOUND_ANDROID_TYPE_ECHO_REFERENCE = 28, + FF_SOUND_ANDROID_TYPE_HDMI_EARC = 29, + FF_SOUND_ANDROID_TYPE_BLE_BROADCAST = 30, + FF_SOUND_ANDROID_TYPE_DOCK_ANALOG = 31, + FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP = 32, + FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID = 33, + FF_SOUND_ANDROID_TYPE_BLE_CENTRAL = 34, + FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST = 35, + // 36 to 79 have no constant in the Android 17 release, and 80 is the highest it declares. + FF_SOUND_ANDROID_TYPE_PROXY = 80, +} FFSoundAndroidDeviceType; + +typedef struct FFSoundAndroidDeviceName { + FFSoundAndroidDeviceType type; + const char* identifier; + const char* name; +} FFSoundAndroidDeviceName; + +static const FFSoundAndroidDeviceName FF_SOUND_ANDROID_DEVICE_NAMES[] = { + { FF_SOUND_ANDROID_TYPE_UNKNOWN, "UNKNOWN", "Unknown Device" }, + { FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE, "BUILTIN_EARPIECE", "Built-in Earpiece" }, + { FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER, "BUILTIN_SPEAKER", "Built-in Speaker" }, + { FF_SOUND_ANDROID_TYPE_WIRED_HEADSET, "WIRED_HEADSET", "Wired Headset" }, + { FF_SOUND_ANDROID_TYPE_WIRED_HEADPHONES, "WIRED_HEADPHONES", "Wired Headphones" }, + { FF_SOUND_ANDROID_TYPE_LINE_ANALOG, "LINE_ANALOG", "Line (Analog)" }, + { FF_SOUND_ANDROID_TYPE_LINE_DIGITAL, "LINE_DIGITAL", "Line (Digital)" }, + { FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO, "BLUETOOTH_SCO", "Bluetooth SCO" }, + { FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP, "BLUETOOTH_A2DP", "Bluetooth A2DP" }, + { FF_SOUND_ANDROID_TYPE_HDMI, "HDMI", "HDMI" }, + { FF_SOUND_ANDROID_TYPE_HDMI_ARC, "HDMI_ARC", "HDMI ARC" }, + { FF_SOUND_ANDROID_TYPE_USB_DEVICE, "USB_DEVICE", "USB Device" }, + { FF_SOUND_ANDROID_TYPE_USB_ACCESSORY, "USB_ACCESSORY", "USB Accessory" }, + { FF_SOUND_ANDROID_TYPE_DOCK, "DOCK", "Dock" }, + { FF_SOUND_ANDROID_TYPE_FM, "FM", "FM" }, + { FF_SOUND_ANDROID_TYPE_BUILTIN_MIC, "BUILTIN_MIC", "Built-in Microphone" }, + { FF_SOUND_ANDROID_TYPE_FM_TUNER, "FM_TUNER", "FM Tuner" }, + { FF_SOUND_ANDROID_TYPE_TV_TUNER, "TV_TUNER", "TV Tuner" }, + { FF_SOUND_ANDROID_TYPE_TELEPHONY, "TELEPHONY", "Telephony" }, + { FF_SOUND_ANDROID_TYPE_AUX_LINE, "AUX_LINE", "Aux Line" }, + { FF_SOUND_ANDROID_TYPE_IP, "IP", "IP" }, + { FF_SOUND_ANDROID_TYPE_BUS, "BUS", "Bus" }, + { FF_SOUND_ANDROID_TYPE_USB_HEADSET, "USB_HEADSET", "USB Headset" }, + { FF_SOUND_ANDROID_TYPE_HEARING_AID, "HEARING_AID", "Hearing Aid" }, + { FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER_SAFE, "BUILTIN_SPEAKER_SAFE", "Built-in Speaker (Safe)" }, + { FF_SOUND_ANDROID_TYPE_REMOTE_SUBMIX, "REMOTE_SUBMIX", "Remote Submix" }, + { FF_SOUND_ANDROID_TYPE_BLE_HEADSET, "BLE_HEADSET", "Bluetooth LE Headset" }, + { FF_SOUND_ANDROID_TYPE_BLE_SPEAKER, "BLE_SPEAKER", "Bluetooth LE Speaker" }, + { FF_SOUND_ANDROID_TYPE_ECHO_REFERENCE, "ECHO_REFERENCE", "Echo Reference" }, + { FF_SOUND_ANDROID_TYPE_HDMI_EARC, "HDMI_EARC", "HDMI eARC" }, + { FF_SOUND_ANDROID_TYPE_BLE_BROADCAST, "BLE_BROADCAST", "Bluetooth LE Broadcast" }, + { FF_SOUND_ANDROID_TYPE_DOCK_ANALOG, "DOCK_ANALOG", "Dock (Analog)" }, + { FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP, "MULTICHANNEL_GROUP", "Multichannel Group" }, + { FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID, "BLE_HEARING_AID", "Bluetooth LE Hearing Aid" }, + { FF_SOUND_ANDROID_TYPE_BLE_CENTRAL, "BLE_CENTRAL", "Bluetooth LE Central" }, + { FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST, "BLE_CENTRAL_BROADCAST", "Bluetooth LE Central Broadcast" }, + { FF_SOUND_ANDROID_TYPE_PROXY, "PROXY", "Proxy" }, +}; + +static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) { + for (size_t i = 0; i < ARRAY_SIZE(FF_SOUND_ANDROID_DEVICE_NAMES); ++i) { + if ((int32_t) FF_SOUND_ANDROID_DEVICE_NAMES[i].type == type) { + return &FF_SOUND_ANDROID_DEVICE_NAMES[i]; + } + } + return nullptr; +} + +// The types that stand for a Bluetooth device. They are the ones whose own name is worth asking the +// service for: everything else is described well enough by its type, and the request is one more +// round trip to system_server. A hearing aid is on the list because it is a Bluetooth LE link with +// a name of its own, the same as a headset. The other three types Android 17 added stay off it: +// BLE_CENTRAL and BLE_CENTRAL_BROADCAST name an audio path inside this device while it operates in +// a Bluetooth peripheral mode rather than a device on the far end of a link, and PROXY is a logical +// endpoint rather than a link at all. +static bool soundAndroidIsBluetooth(int32_t type) { + switch ((FFSoundAndroidDeviceType) type) { + case FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO: + case FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP: + case FF_SOUND_ANDROID_TYPE_HEARING_AID: + case FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID: + case FF_SOUND_ANDROID_TYPE_BLE_HEADSET: + case FF_SOUND_ANDROID_TYPE_BLE_SPEAKER: + case FF_SOUND_ANDROID_TYPE_BLE_BROADCAST: + return true; + default: + return false; + } +} + +// Every transaction code this module sends, resolved from the device's own jar in one walk. They are +// asked for together because the walk is dominated by the dex's type table and all seven sit in the +// same class of the same entry -- see common/android/dex.h. +typedef struct FFSoundAndroidCodes { + int32_t getStreamVolume; + int32_t getStreamMinVolume; + int32_t getStreamMaxVolume; + int32_t isStreamMute; + int32_t getDevicesForAttributes; + int32_t isMusicActive; + int32_t getBtActiveDeviceName; +} FFSoundAndroidCodes; + +// The five codes the module cannot answer without, against the two that only refine the answer. A +// build that does not declare one of these five has nothing to report, which is what the module did +// when each code was resolved on its own. +static bool soundAndroidCodesIncomplete(const FFSoundAndroidCodes* codes) { + return codes->getStreamVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getStreamMinVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getStreamMaxVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->isStreamMute == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getDevicesForAttributes == FF_DEX_STATIC_INT_UNRESOLVED; +} + +// Resolves all seven in one call, and then makes the judgement the reader leaves to its caller. The +// two that only refine the answer are allowed to come back as the sentinel, because a build may +// declare one of them and not the other -- Mi 10 has `getDevicesForAttributesUnprotected` and no +// `getBtActiveDeviceName` -- and a code the module can live without must not take it down with it. +static const char* soundAndroidResolveCodes(FFSoundAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_VOLUME, &codes->getStreamVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, &codes->getStreamMinVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, &codes->getStreamMaxVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_IS_STREAM_MUTE, &codes->isStreamMute }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, &codes->getDevicesForAttributes }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, &codes->isMusicActive }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, &codes->getBtActiveDeviceName }, + }; + + const char* error = ffDexStaticInts(FF_SOUND_ANDROID_JAR, requests, ARRAY_SIZE(requests)); + if (error != nullptr) { + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_SOUND_ANDROID_JAR, error); + return error; + } + if (soundAndroidCodesIncomplete(codes)) { + FF_DEBUG("The audio service does not declare every method this module calls"); + return "The audio service does not declare every method this module calls"; + } + return nullptr; +} + +// Calls a method that takes no argument or one int, and answers with one int. `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. `transactionField` is carried only to name the method in the debug output, which a +// release build compiles out -- hence the attribute. +static const char* soundAndroidCallInt(FFBinder* binder, uint32_t handle, [[maybe_unused]] const char* transactionField, int32_t transaction, bool hasArgument, int32_t argument, int32_t* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("The audio service does not declare that method"); + return "The audio service does not declare that method"; + } + + uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR); + if (hasArgument) { + ffBinderParcelPutI32(&parcel, argument); + } + + uint8_t replyBuffer[FF_SOUND_ANDROID_SCALAR_REPLY_SIZE]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", transactionField, transaction); + return "The audio service does not answer that request"; + } + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("\"%s\" is transaction %d and raised exception %d", transactionField, transaction, exception); + return "The audio service raised an exception"; + } + *result = ffBinderReadI32(reply.data, reply.size, 4); + FF_DEBUG("\"%s\" is transaction %d and answered %d", transactionField, transaction, *result); + return nullptr; +} + +// Walks past a string16 and reports how long it was. The bytes themselves are not decoded: the only +// strings in the reply are the device address and name, both of which are empty for every built-in +// device and a partly anonymised MAC for a Bluetooth one -- `AudioService` blanks the address of +// anything it considers identifying before answering an unprivileged caller, so neither is an +// identifier worth carrying. +static bool soundAndroidSkipString16(const uint8_t* data, size_t size, size_t* offset) { + const int32_t length = ffBinderReadI32(data, size, *offset); + *offset += sizeof(int32_t); + if (length < 0) { + return true; + } + // The code units plus the NUL terminator, padded to four bytes. + const size_t payload = ((size_t) length * 2 + 2 + 3) & ~(size_t) 3; + if (*offset + payload > size) { + return false; + } + *offset += payload; + return true; +} + +// Reads a string16 out of a reply and appends it to `result`. +// +// The code units are UTF-16LE, and a device name is the first string here that is not guaranteed to +// be ASCII -- a headset can be named in any script, and `vivo TWS A4` is only the one that happened +// to be paired -- so they are converted rather than copied. A count of -1 is how the same field +// spells null, which is what the service answers when there is no name to give; that is not a +// failure, and the caller falls back to the type name. +static bool soundAndroidReadString16(const uint8_t* data, size_t size, size_t offset, FFstrbuf* result) { + if (offset + sizeof(int32_t) > size) { + return false; + } + const int32_t length = ffBinderReadI32(data, size, offset); + offset += sizeof(int32_t); + if (length < 0) { + return true; + } + // The code units plus the NUL terminator. The padding behind them is not part of the string, so + // there is nothing to step over here. + if ((size_t) length * 2 + 2 > size - offset) { + return false; + } + + for (int32_t i = 0; i < length; ++i) { + uint32_t codePoint = (uint32_t) data[offset + (size_t) i * 2] | ((uint32_t) data[offset + (size_t) i * 2 + 1] << 8); + if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < length) { + // Anything outside the basic plane arrives as a surrogate pair, and the low half is what + // turns it back into one code point. A high surrogate without one is malformed and is + // passed through as it stands rather than guessed at. + const uint32_t low = (uint32_t) data[offset + (size_t) (i + 1) * 2] | ((uint32_t) data[offset + (size_t) (i + 1) * 2 + 1] << 8); + if (low >= 0xDC00 && low <= 0xDFFF) { + codePoint = 0x10000u + ((codePoint - 0xD800u) << 10) + (low - 0xDC00u); + ++i; + } + } + ffStrbufAppendUtf32CodePoint(result, codePoint); + } + return true; +} + +// Calls `getBtActiveDeviceName()`, which takes no argument and answers a string16. It is the only +// call here that carries a human readable device name. +// +// The policy reply has a `name` field of its own, but on the test device `AudioService` leaves it +// empty for every device -- a Bluetooth one included, whose address it blanks to +// `XX:XX:XX:XX:D8:61` while the name goes with it -- so the type is all that reply can describe a +// sink by. This call is what turns "Bluetooth A2DP" into the name of the headset. +// +// What it must not be used for is deciding *which* device is the output. It answers about the +// Bluetooth route as a whole and reports it even when the policy has selected the speaker, so it is +// only ever consulted after the device list has already picked a Bluetooth sink, and only its name +// is taken from it. +static const char* soundAndroidCallBtName(FFBinder* binder, uint32_t handle, int32_t transaction, FFstrbuf* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("The audio service does not declare getBtActiveDeviceName"); + return "The audio service does not declare getBtActiveDeviceName"; + } + + uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR); + + uint8_t replyBuffer[FF_SOUND_ANDROID_NAME_REPLY_SIZE]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction); + return "The audio service does not answer that request"; + } + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("\"%s\" is transaction %d and raised exception %d", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction, exception); + return "The audio service raised an exception"; + } + if (!soundAndroidReadString16(reply.data, reply.size, sizeof(int32_t), result)) { + FF_DEBUG("The audio service answered a truncated device name"); + return "The audio service answered a truncated device name"; + } + FF_DEBUG("\"%s\" is transaction %d and answered \"%s\"", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction, + result->chars != nullptr ? result->chars : ""); + return nullptr; +} + +// Reads one `AudioDeviceAttributes` out of the reply and advances `offset` past it. +// +// The layout is the one `AudioDeviceAttributes.writeToParcel` writes, and it was checked byte by +// byte against the device's replies: +// +// int32 role, int32 type, string16 address, string16 name, int32 nativeType, +// List profiles, List descriptors +// +// `role` is 1 for a source (an input) and 2 for a sink (an output). `type` is the +// AudioDeviceInfo.TYPE_* above; `nativeType` repeats it as the older AUDIO_DEVICE_* bit value and is +// not used. Neither profile list has carried an entry on any device seen so far, so they are walked +// only far enough to confirm that. A device that does carry profiles is still reported -- everything +// this module reads comes before them -- but nothing after it in the list can be reached, because +// walking a profile means decoding one, and the caller stops there rather than reading the next +// entry out of the middle of one. +static bool soundAndroidReadDevice(const uint8_t* data, size_t size, size_t* offset, int32_t* type, int32_t* role, bool* ended) { + *ended = false; + if (*offset + 3 * sizeof(int32_t) > size) { + return false; + } + *role = ffBinderReadI32(data, size, *offset); + *offset += sizeof(int32_t); + *type = ffBinderReadI32(data, size, *offset); + *offset += sizeof(int32_t); + if (!soundAndroidSkipString16(data, size, offset) || !soundAndroidSkipString16(data, size, offset)) { + return false; + } + *offset += sizeof(int32_t); // nativeType, which follows the two strings + for (int list = 0; list < 2; ++list) { + const int32_t count = ffBinderReadI32(data, size, *offset); + *offset += sizeof(int32_t); + if (count > 0) { + // A null list is a -1 and occupies nothing beyond the count that was just read, so only + // a list with entries in it ends the walk. + *ended = true; + return true; + } + } + return true; +} + +// Turns the three volume readings into the percentage the module prints. The service answers with +// the index it keeps for the stream, which is not a percentage and is not even on a fixed scale: +// `getStreamVolume` divides by ten before returning, so the numbers here are the ones an app sees, +// and the maximum differs per stream -- 150 for music on the test device, 15 for the system stream, +// 10 for a call. Music's minimum is 0, but that is a property of the stream rather than of the API: +// a call's minimum is 1. So the percentage comes out of the range the service reports, and a stream +// whose range is empty or inverted is reported as unknown rather than as 100%. +static uint8_t soundAndroidPercent(int32_t volume, int32_t minimum, int32_t maximum, int32_t mute) { + if (mute != 0) { + // A muted stream is reported as 0 rather than as its index: the service keeps the index and + // the mute flag separately, and it is the mute flag that decides what comes out of the + // speaker. This is also what the PulseAudio backend does with a muted sink. + return 0; + } + if (maximum <= minimum) { + return FF_SOUND_VOLUME_UNKNOWN; + } + if (volume <= minimum) { + return 0; + } + if (volume >= maximum) { + return 100; + } + return (uint8_t) (((volume - minimum) * 100 + (maximum - minimum) / 2) / (maximum - minimum)); +} + +static const char* detectNative(FFSoundOptions* options, FFlist* devices) { + // Resolved before anything else so that a build without the methods says so, rather than opening + // binder and failing later with a message about the transport. All seven codes are asked for in + // one call and judged here: the five the module cannot answer without decide whether it runs at + // all, and the two it only uses when the build declares them are left as sentinels for their + // call sites to cope with. + FFSoundAndroidCodes codes = {}; + const char* error = soundAndroidResolveCodes(&codes); + if (error != nullptr) { + return error; + } + + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + error = ffBinderOpen(&binder); + if (error != nullptr) { + return error; + } + + // Released on every path out, including the early returns below. + [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder }; + error = ffBinderLookupService(&binder, FF_SOUND_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle); + if (error != nullptr) { + return error; + } + FF_DEBUG("The \"%s\" service is handle %u, getDevicesForAttributesUnprotected is transaction %d", + FF_SOUND_ANDROID_SERVICE, service.handle, codes.getDevicesForAttributes); + + uint8_t parcelBuffer[FF_SOUND_ANDROID_DEVICE_PARCEL_SIZE]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR); + // The non-null marker `Parcel.writeTypedObject` puts in front of a Parcelable. The service reads + // the argument with `readTypedObject`, which reads that int first and hands back a null object + // when it is not 1 -- so leaving it out does not shift the fields, it loses the argument, and + // the service answers with an exception rather than a device list. + ffBinderParcelPutI32(&parcel, 1); + // `in AudioAttributes`, whose writeToParcel is: usage, contentType, source, flags, the parcel + // flags, the tag array and the bundle marker. A null bundle is the only form built here. + ffBinderParcelPutI32(&parcel, (int32_t) FF_SOUND_ANDROID_USAGE_MEDIA); + ffBinderParcelPutI32(&parcel, 0); // contentType, unused by the policy when a usage is set + ffBinderParcelPutI32(&parcel, FF_SOUND_ANDROID_SOURCE_INVALID); + ffBinderParcelPutI32(&parcel, 0); // flags + ffBinderParcelPutI32(&parcel, 0); // parcel flags: the tags are not flattened + ffBinderParcelPutI32(&parcel, 0); // an empty tag array + ffBinderParcelPutI32(&parcel, FF_SOUND_ANDROID_ATTR_NULL_BUNDLE); + + uint8_t replyBuffer[FF_SOUND_ANDROID_DEVICE_REPLY_SIZE]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getDevicesForAttributes, 0, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("The reply carries status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0)); + return "The audio service rejected the request"; + } + + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("getDevicesForAttributesUnprotected raised exception %d", exception); + return "The audio service raised an exception"; + } + + // The service answers with the devices the policy has selected for that usage, so there can be + // more than one -- an alarm or a ringtone plays on the speaker and on a connected headset at the + // same time -- and the first is the one the stream's volume belongs to. + const int32_t count = ffBinderReadI32(reply.data, reply.size, 4); + size_t offset = 2 * sizeof(int32_t); + const FFSoundAndroidDeviceName* name = nullptr; + int32_t deviceType = 0; + for (int32_t index = 0; index < count; ++index) { + const int32_t present = ffBinderReadI32(reply.data, reply.size, offset); + offset += sizeof(int32_t); + if (present != 1) { + continue; + } + + int32_t type = 0; + int32_t role = 0; + bool ended = false; + if (!soundAndroidReadDevice(reply.data, reply.size, &offset, &type, &role, &ended)) { + FF_DEBUG("The device list ended after %d of %d entries", index, count); + break; + } + + if (role == 2 && name == nullptr) { + // ROLE_SINK. A source would be a microphone, which the policy only answers when the + // request asks for one, so this is a guard rather than a case that happens. + name = soundAndroidDeviceName(type); + deviceType = type; + FF_DEBUG("Media output device %d of %d is type %d, which is \"%s\"", + index + 1, count, type, name != nullptr ? name->identifier : "an unrecognised type"); + } + + if (ended) { + // A profile list with entries in it: the next entry cannot be reached without walking + // one, and the first device is the one that matters. + FF_DEBUG("Device %d carries audio profiles, so the rest of the list is not read", index); + break; + } + } + + if (name == nullptr) { + FF_DEBUG("The audio service selected no output device"); + return "The audio service selected no output device"; + } + + int32_t volume = 0; + int32_t minimum = 0; + int32_t maximum = 0; + int32_t mute = 0; + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_VOLUME, codes.getStreamVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &volume); + if (error == nullptr) { + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, codes.getStreamMinVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &minimum); + } + if (error == nullptr) { + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, codes.getStreamMaxVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &maximum); + } + if (error == nullptr) { + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_STREAM_MUTE, codes.isStreamMute, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &mute); + } + if (error != nullptr) { + return error; + } + + uint8_t percent = soundAndroidPercent(volume, minimum, maximum, mute); + FF_DEBUG("Stream %d is %d of %d..%d, muted %d, which is %s", + (int) FF_SOUND_ANDROID_STREAM_MUSIC, volume, minimum, maximum, mute, + percent != FF_SOUND_VOLUME_UNKNOWN ? "a percentage" : "unknown"); + + // `isMusicActive` is what tells "this device is the output" apart from "this device is playing". + // It answers about the media stream as a whole and not about one device, so it only ever adds + // the ACTIVE bit to the device the policy already selected. A build that does not declare the + // method, or a service that refuses it, leaves the bit clear rather than failing the module. + int32_t active = 0; + const char* activeError = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, codes.isMusicActive, true, 0, &active); + if (activeError != nullptr) { + FF_DEBUG("Whether anything is playing is not known: %s", activeError); + active = 0; + } + + const FFSoundType type = (FFSoundType) (FF_SOUND_TYPE_MAIN | (active != 0 ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE)); + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !(type & FF_SOUND_TYPE_MAIN)) { + return nullptr; + } + if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !(type & FF_SOUND_TYPE_ACTIVE)) { + return nullptr; + } + + // A Bluetooth sink is reported by the name of the device behind it when the service can be + // asked, and by its type otherwise. Falling back is not an error path: a build that does not + // declare the method, a service that refuses it, and a service that has no name to give all end + // in the type name, which is what this module printed before it asked at all. + FF_STRBUF_AUTO_DESTROY bluetoothName = ffStrbufCreate(); + if (soundAndroidIsBluetooth(deviceType)) { + const char* nameError = soundAndroidCallBtName(&binder, service.handle, codes.getBtActiveDeviceName, &bluetoothName); + if (nameError != nullptr || bluetoothName.length == 0) { + FF_DEBUG("The Bluetooth device is not named: %s", + nameError != nullptr ? nameError : "the service answered an empty name"); + } + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + // The identifier stays the type: it is the machine readable half of the pair, and the Bluetooth + // address the reply carries is not usable as one -- `AudioService` blanks it to + // `XX:XX:XX:XX:D8:61` for an unprivileged caller. + ffStrbufInitS(&device->identifier, name->identifier); + if (bluetoothName.length > 0) { + ffStrbufInitMove(&device->name, &bluetoothName); + } else { + ffStrbufInitS(&device->name, name->name); + } + ffStrbufInitStatic(&device->platformApi, "AudioService"); + device->volume = percent; + device->type = type; + FF_DEBUG("Reporting \"%s\" (%s) at %u%%%s", device->name.chars, device->identifier.chars, device->volume, + active != 0 ? ", active" : ""); + return nullptr; +} + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { + // The binder route below asks the policy about Android's own audio, which is the audio that is + // audible only while SurfaceFlinger is the display server. A Termux:X11 or Wayland session runs + // a real desktop on top of the device, and the sound there belongs to that desktop, not to + // Android -- so that case is handed to the Linux implementation, which speaks PulseAudio. Sound + // is only reported for one of the two, never merged. + const FFDisplayServerResult* wm = ffConnectDisplayServer(); + if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) { + FF_DEBUG("The display server is \"%s\", so the Linux implementation answers", wm->wmProtocolName.chars); + const char* ffDetectSoundLinux(FFSoundOptions* options, FFlist* devices); + return ffDetectSoundLinux(options, devices); + } + + return detectNative(options, devices); +} diff --git a/src/detection/sound/sound_apple.c b/src/detection/sound/sound_apple.c index 36ebb2a8b7..37f71e4152 100644 --- a/src/detection/sound/sound_apple.c +++ b/src/detection/sound/sound_apple.c @@ -1,5 +1,6 @@ #include "sound.h" #include "common/apple/cf_helpers.h" +#include "common/debug.h" #include #include @@ -11,7 +12,9 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FFSoundDevice */) { AudioDeviceID mainDeviceId; UInt32 dataSize = sizeof(mainDeviceId); - if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, nullptr, &dataSize, &mainDeviceId) != kAudioHardwareNoError) { + OSStatus defaultOutputStatus = AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, nullptr, &dataSize, &mainDeviceId); + if (defaultOutputStatus != kAudioHardwareNoError) { + FF_DEBUG("AudioObjectGetPropertyData(kAudioHardwarePropertyDefaultOutputDevice) failed: OSStatus %d", (int) defaultOutputStatus); return "AudioObjectGetPropertyData(kAudioHardwarePropertyDefaultOutputDevice) failed"; } @@ -21,7 +24,9 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FF dataSize = sizeof(mainDeviceId); } else { dataSize = sizeof(deviceIds); - if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, nullptr, &dataSize, &deviceIds) != kAudioHardwareNoError) { + OSStatus deviceListStatus = AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, nullptr, &dataSize, &deviceIds); + if (deviceListStatus != kAudioHardwareNoError) { + FF_DEBUG("AudioObjectGetPropertyData(kAudioHardwarePropertyDevices) failed: OSStatus %d", (int) deviceListStatus); return "AudioObjectGetPropertyData(kAudioHardwarePropertyDevices) failed"; } } diff --git a/src/detection/sound/sound_bsd.c b/src/detection/sound/sound_bsd.c index 118b33b091..b5e5a373e7 100644 --- a/src/detection/sound/sound_bsd.c +++ b/src/detection/sound/sound_bsd.c @@ -1,4 +1,5 @@ #include "sound.h" +#include "common/debug.h" #include "common/io.h" #include "common/sysctl.h" @@ -10,6 +11,7 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { #ifndef __NetBSD__ int defaultDev = ffSysctlGetInt("hw.snd.default_unit", -1); if (defaultDev == -1) { + FF_DEBUG("sysctl(hw.snd.default_unit) failed"); return "sysctl(hw.snd.default_unit) failed"; } #else @@ -18,10 +20,12 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { char mixerp[12]; ssize_t plen = readlink("/dev/mixer", mixerp, ARRAY_SIZE(mixerp)); if (plen < 6) { + FF_DEBUG("readlink(/dev/mixer) failed: %s", strerror(errno)); return "readlink(/dev/mixer) failed"; } defaultDev = mixerp[plen - 1] - '0'; if (defaultDev < 0 || defaultDev > 9) { + FF_DEBUG("Invalid mixer device"); return "Invalid mixer device"; } } @@ -45,6 +49,7 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { if (idev == 0) { if (ioctl(fd, SNDCTL_SYSINFO, &info) != 0) { + FF_DEBUG("ioctl(SNDCTL_SYSINFO) failed: %s", strerror(errno)); return "ioctl(SNDCTL_SYSINFO) failed"; } } diff --git a/src/detection/sound/sound_linux.c b/src/detection/sound/sound_linux.c index 7deb1b5f4c..cddcef6e19 100644 --- a/src/detection/sound/sound_linux.c +++ b/src/detection/sound/sound_linux.c @@ -1,164 +1,1727 @@ #include "sound.h" +#include "common/debug.h" +#include "common/endian.h" +#include "common/io.h" +#include "common/strutil.h" -#ifdef FF_HAVE_PULSE - #include "common/library.h" - #include +#include +#include +#include +#include +#include +#include +#include +#include +#include -struct DetectionInfoBundle { - FFstrbuf serverName; - FFstrbuf defaultDeviceId; - FFlist* result; - FFSoundOptions* options; +// Linux sound comes from one of two places: the sound server a desktop runs, and the hardware +// underneath it. +// +// The server is PulseAudio -- PipeWire speaks its protocol too -- and it is asked first, because +// it is the only one of the two that knows what the desktop's volume is: the mixer the card +// exposes and the slider on screen are not the same thing. When no server answers, the cards are +// read from ALSA instead; that part lives in its own section at the end of this file. +// +// --------------------------------------------------------------------------- +// the server: PulseAudio's native protocol ("protocol-native") +// --------------------------------------------------------------------------- +// +// Instead of linking or dlopen-ing libpulse, fastfetch talks to the daemon +// directly: the daemon speaks a small length prefixed binary protocol on its +// Unix socket (TCP works as well) and the read only introspection commands used +// here need no event loop, no shared memory and no subscription. +// +// A frame is a 20 byte big endian header (payload length, channel, offset high, +// offset low, flags) followed by a "tagstruct" payload: a stream of one byte +// type tags, each followed by its value. All integers are big endian. The header +// of control packets always uses the channel 0xFFFFFFFF with a zero offset and +// flag word. +// +// The command numbers, the tag characters and the field orders below are the ones in pulseaudio's +// own sources -- pulsecore/native-common.h, pulsecore/tagstruct.{c,h} and +// pulsecore/protocol-native.c -- and the place each of them was read from is named where it is used. + +typedef enum FFSoundPulseCommand : uint32_t { + FF_SOUND_PA_COMMAND_ERROR = 0, + FF_SOUND_PA_COMMAND_REPLY = 2, + FF_SOUND_PA_COMMAND_AUTH = 8, + FF_SOUND_PA_COMMAND_SET_CLIENT_NAME = 9, + FF_SOUND_PA_COMMAND_GET_SERVER_INFO = 20, + FF_SOUND_PA_COMMAND_GET_SINK_INFO_LIST = 22, +} FFSoundPulseCommand; + +typedef enum FFSoundPulseTag : uint8_t { + FF_SOUND_PA_TAG_STRING = 't', + FF_SOUND_PA_TAG_STRING_NULL = 'N', + FF_SOUND_PA_TAG_U32 = 'L', + FF_SOUND_PA_TAG_U8 = 'B', + FF_SOUND_PA_TAG_U64 = 'R', + FF_SOUND_PA_TAG_S64 = 'r', + FF_SOUND_PA_TAG_SAMPLE_SPEC = 'a', + FF_SOUND_PA_TAG_ARBITRARY = 'x', + FF_SOUND_PA_TAG_BOOLEAN_TRUE = '1', + FF_SOUND_PA_TAG_BOOLEAN_FALSE = '0', + FF_SOUND_PA_TAG_TIMEVAL = 'T', + FF_SOUND_PA_TAG_USEC = 'U', + FF_SOUND_PA_TAG_CHANNEL_MAP = 'm', + FF_SOUND_PA_TAG_CVOLUME = 'v', + FF_SOUND_PA_TAG_PROPLIST = 'P', + FF_SOUND_PA_TAG_VOLUME = 'V', + FF_SOUND_PA_TAG_FORMAT_INFO = 'f', +} FFSoundPulseTag; + +typedef enum FFSoundPulseError { + FF_SOUND_PA_ERROR_ACCESS = 1, + FF_SOUND_PA_ERROR_VERSION = 17, +} FFSoundPulseError; + +enum { + FF_SOUND_PA_VERSION = 35, // PA_PROTOCOL_VERSION + FF_SOUND_PA_VERSION_MASK = 0xFFFF, // PA_PROTOCOL_VERSION_MASK + FF_SOUND_PA_COOKIE_LENGTH = 256, // PA_NATIVE_COOKIE_LENGTH + FF_SOUND_PA_FRAME_HEADER_SIZE = 20, + FF_SOUND_PA_FRAME_LENGTH_MAX = 1024 * 1024 * 16, // FRAME_SIZE_MAX_ALLOW + FF_SOUND_PA_CHANNELS_MAX = 32, // PA_CHANNELS_MAX + FF_SOUND_PA_VOLUME_NORM = 0x10000, // PA_VOLUME_NORM, i.e. 100% + FF_SOUND_PA_PORT_AVAILABLE_NO = 2, // PA_PORT_AVAILABLE_NO + FF_SOUND_PA_RECV_TIMEOUT_MS = 2000, }; -static void paSinkInfoCallback([[maybe_unused]] pa_context* c, const pa_sink_info* i, int eol, void* userdata) { - if (eol > 0 || !i) { +#define FF_SOUND_PA_DEFAULT_PORT "4713" // PA_NATIVE_DEFAULT_PORT +#define FF_SOUND_PA_SOCKET_NAME "native" + +#ifdef MSG_NOSIGNAL + #define FF_SOUND_PA_SEND_FLAGS MSG_NOSIGNAL +#else + #define FF_SOUND_PA_SEND_FLAGS 0 +#endif + +#if defined(__linux__) || defined(__ANDROID__) + #define FF_SOUND_PA_HAS_CREDENTIALS 1 +#else + #define FF_SOUND_PA_HAS_CREDENTIALS 0 +#endif + +typedef struct FFSoundPulseConn { + int fd; + uint32_t version; // protocol version negotiated with the server + uint32_t nextTag; + FFstrbuf frame; // payload of the last received frame +} FFSoundPulseConn; + +typedef struct FFSoundPulseReader { + const char* data; + uint32_t length; + uint32_t offset; + bool error; +} FFSoundPulseReader; + +// Everything this module needs to know about one sink. The fields point into the +// frame buffer of the connection. +typedef struct FFSoundPulseSinkInfo { + const char* name; // the sink name, reported as the device identifier + const char* description; // the sink's own description field, reported as the device name + uint32_t volume; // cvolume of the first channel + bool mute; + bool main; // the sink equals the server's default sink + bool active; // the sink has an active port and that port is available +} FFSoundPulseSinkInfo; + +// --------------------------------------------------------------------------- +// tagstruct +// --------------------------------------------------------------------------- + +static void putTagU32(FFstrbuf* buffer, uint32_t value) { + ffStrbufAppendC(buffer, FF_SOUND_PA_TAG_U32); + value = FF_READ_BE(value); + ffStrbufAppendNS(buffer, sizeof(value), (const char*) &value); +} + +static void putTagString(FFstrbuf* buffer, const char* value) { + if (!value) { + ffStrbufAppendC(buffer, FF_SOUND_PA_TAG_STRING_NULL); return; } - struct DetectionInfoBundle* bundle = userdata; + ffStrbufAppendC(buffer, FF_SOUND_PA_TAG_STRING); + ffStrbufAppendS(buffer, value); + ffStrbufAppendC(buffer, '\0'); +} + +static void putTagArbitrary(FFstrbuf* buffer, const void* value, uint32_t length) { + ffStrbufAppendC(buffer, FF_SOUND_PA_TAG_ARBITRARY); + uint32_t lengthData = FF_READ_BE(length); + ffStrbufAppendNS(buffer, sizeof(lengthData), (const char*) &lengthData); + ffStrbufAppendNS(buffer, length, value); +} + +// A proplist is a series of "string key, u32 length, arbitrary value" triples +// terminated by a NULL string. The length is a tagged u32 like any other, only +// the length inside the arbitrary value is raw. Because the terminator uses the +// same tag as a NULL string, a proplist may only appear at the end of a packet -- +// which is the case for every packet we send. +static void putTagProplist(FFstrbuf* buffer, const char* key, const char* value) { + ffStrbufAppendC(buffer, FF_SOUND_PA_TAG_PROPLIST); + if (key) { + uint32_t length = (uint32_t) strlen(value); + putTagString(buffer, key); + putTagU32(buffer, length); + putTagArbitrary(buffer, value, length); + } + putTagString(buffer, nullptr); +} + +static bool readerEof(const FFSoundPulseReader* reader) { + return reader->offset >= reader->length; +} + +static bool skipBytes(FFSoundPulseReader* reader, uint32_t count) { + if ((uint64_t) reader->offset + count > reader->length) { + reader->error = true; + return false; + } + + reader->offset += count; + return true; +} + +static uint8_t readU8(FFSoundPulseReader* reader) { + if (!skipBytes(reader, 1)) { + return 0; + } + + return (uint8_t) reader->data[reader->offset - 1]; +} + +static uint32_t readU32(FFSoundPulseReader* reader) { + if (!skipBytes(reader, 4)) { + return 0; + } + + uint32_t value; + memcpy(&value, reader->data + reader->offset - 4, sizeof(value)); + return FF_READ_BE(value); +} + +static bool readU32Value(FFSoundPulseReader* reader, uint32_t* value) { + if (readU8(reader) != FF_SOUND_PA_TAG_U32) { + reader->error = true; + return false; + } + + *value = readU32(reader); + return !reader->error; +} + +static bool readU8Value(FFSoundPulseReader* reader, uint8_t* value) { + if (readU8(reader) != FF_SOUND_PA_TAG_U8) { + reader->error = true; + return false; + } + + *value = readU8(reader); + return !reader->error; +} + +// Skips a NUL terminated string whose tag has already been consumed. +static bool skipString(FFSoundPulseReader* reader) { + while (reader->offset < reader->length && reader->data[reader->offset]) { + reader->offset++; + } - bool isMain = ffStrbufEqualS(&bundle->defaultDeviceId, i->name); - if ((bundle->options->soundType & FF_SOUND_TYPE_MAIN) && !isMain) { + if (reader->offset >= reader->length) { + reader->error = true; + return false; + } + + reader->offset++; // trailing NUL + return true; +} + +// Reads a string tag and returns a pointer to the NUL terminated string inside +// the frame. A NULL string ('N') is a valid value, not an error. The pointer +// stays valid as long as the frame buffer is not reused. +static const char* readStringRef(FFSoundPulseReader* reader) { + uint8_t tag = readU8(reader); + if (reader->error) { + return nullptr; + } + + if (tag == FF_SOUND_PA_TAG_STRING_NULL) { + return nullptr; + } + + if (tag != FF_SOUND_PA_TAG_STRING) { + reader->error = true; + return nullptr; + } + + uint32_t start = reader->offset; + if (!skipString(reader)) { + return nullptr; + } + + return reader->data + start; +} + +static bool readString(FFSoundPulseReader* reader, FFstrbuf* value) { + const char* string = readStringRef(reader); + if (reader->error) { + return false; + } + + if (string) { + ffStrbufSetS(value, string); + } else { + ffStrbufClear(value); + } + + return true; +} + +static bool readBoolean(FFSoundPulseReader* reader, bool* value) { + uint8_t tag = readU8(reader); + if (tag == FF_SOUND_PA_TAG_BOOLEAN_TRUE) { + *value = true; + return true; + } + + if (tag == FF_SOUND_PA_TAG_BOOLEAN_FALSE) { + *value = false; + return true; + } + + reader->error = true; + return false; +} + +// Reads a cvolume tag and returns the volume of its first channel. The channel +// count is validated like pa_tagstruct_get_cvolume() does, otherwise a malformed +// packet would make us walk out of the payload. +static bool readCVolume(FFSoundPulseReader* reader, uint32_t* volume) { + if (readU8(reader) != FF_SOUND_PA_TAG_CVOLUME) { + reader->error = true; + return false; + } + + uint8_t channels = readU8(reader); + if (reader->error || channels > FF_SOUND_PA_CHANNELS_MAX) { + reader->error = true; + return false; + } + + *volume = 0; + for (uint8_t channel = 0; channel < channels; channel++) { + uint32_t value = readU32(reader); + if (reader->error) { + return false; + } + + if (channel == 0) { + *volume = value; + } + } + + return true; +} + +// Skips the value that follows the already consumed tag. It dispatches on the +// tag it actually reads, so it can not run out of sync even if the layout +// assumed for the surrounding packet is wrong. +static bool skipValue(FFSoundPulseReader* reader) { + uint8_t tag = readU8(reader); + if (reader->error) { + return false; + } + + switch (tag) { + case FF_SOUND_PA_TAG_STRING_NULL: + case FF_SOUND_PA_TAG_BOOLEAN_TRUE: + case FF_SOUND_PA_TAG_BOOLEAN_FALSE: + return true; + case FF_SOUND_PA_TAG_STRING: + return skipString(reader); // the tag is already consumed + case FF_SOUND_PA_TAG_U8: + return skipBytes(reader, 1); + case FF_SOUND_PA_TAG_U32: + case FF_SOUND_PA_TAG_VOLUME: + return skipBytes(reader, 4); + case FF_SOUND_PA_TAG_U64: + case FF_SOUND_PA_TAG_S64: + case FF_SOUND_PA_TAG_USEC: + case FF_SOUND_PA_TAG_TIMEVAL: + return skipBytes(reader, 8); + case FF_SOUND_PA_TAG_SAMPLE_SPEC: + return skipBytes(reader, 6); // format, channels, rate + case FF_SOUND_PA_TAG_ARBITRARY: { + uint32_t length = readU32(reader); + return !reader->error && skipBytes(reader, length); + } + case FF_SOUND_PA_TAG_CHANNEL_MAP: { + uint8_t channels = readU8(reader); + return !reader->error && skipBytes(reader, channels); + } + case FF_SOUND_PA_TAG_CVOLUME: { + uint8_t channels = readU8(reader); + return !reader->error && skipBytes(reader, (uint32_t) channels * 4); + } + case FF_SOUND_PA_TAG_PROPLIST: + while (!reader->error && !readerEof(reader) && reader->data[reader->offset] != FF_SOUND_PA_TAG_STRING_NULL) { + if (!skipValue(reader) || !skipValue(reader) || !skipValue(reader)) { // key, length, value + return false; + } + } + return skipValue(reader); // the NULL string terminating the list + case FF_SOUND_PA_TAG_FORMAT_INFO: + return skipValue(reader) && skipValue(reader); // encoding, proplist + default: + FF_DEBUG("Unknown pulseaudio tag 0x%02X", tag); + reader->error = true; + return false; + } +} + +// --------------------------------------------------------------------------- +// transport +// --------------------------------------------------------------------------- + +static bool writeAll(int fd, const char* data, uint32_t length) { + while (length > 0) { + ssize_t written = send(fd, data, length, FF_SOUND_PA_SEND_FLAGS); + if (written < 0) { + if (errno == EINTR) { + continue; + } + + FF_DEBUG("Failed to write to the pulseaudio socket: %s", strerror(errno)); + return false; + } + + data += written; + length -= (uint32_t) written; + } + + return true; +} + +static bool readAll(int fd, char* data, uint32_t length) { + while (length > 0) { + ssize_t received = recv(fd, data, length, 0); + if (received < 0) { + if (errno == EINTR) { + continue; + } + + FF_DEBUG("Failed to read from the pulseaudio socket: %s", strerror(errno)); + return false; + } + + if (received == 0) { // the server closed the connection + FF_DEBUG("The pulseaudio server closed the connection"); + return false; + } + + data += received; + length -= (uint32_t) received; + } + + return true; +} + +static void applyReceiveTimeout(int fd) { + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &(struct timeval){ .tv_sec = FF_SOUND_PA_RECV_TIMEOUT_MS / 1000, .tv_usec = (FF_SOUND_PA_RECV_TIMEOUT_MS % 1000) * 1000 }, sizeof(struct timeval)); +} + +static int connectUnixSocket(const char* path) { + struct sockaddr_un address = { .sun_family = AF_UNIX }; + size_t length = strlen(path); + if (length >= sizeof(address.sun_path)) { + FF_DEBUG("The pulseaudio socket path is too long: %s", path); + return -1; + } + + memcpy(address.sun_path, path, length + 1); + + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (connect(fd, (struct sockaddr*) &address, (socklen_t) (offsetof(struct sockaddr_un, sun_path) + length + 1)) < 0) { + FF_DEBUG("Failed to connect to %s: %s", path, strerror(errno)); + close(fd); + return -1; + } + + applyReceiveTimeout(fd); + return fd; +} + +static int connectTcpSocket(const char* address, int family) { + // "host", "host:port" or "[host]:port" + FF_STRBUF_AUTO_DESTROY host = ffStrbufCreate(); + const char* port = FF_SOUND_PA_DEFAULT_PORT; + + if (*address == '[') { + const char* end = strchr(address, ']'); + if (!end) { + return -1; + } + + ffStrbufSetNS(&host, (uint32_t) (end - address - 1), address + 1); + if (end[1] == ':') { + port = end + 2; + } + } else { + const char* colon = strrchr(address, ':'); + if (colon && strchr(address, ':') == colon) { // an unbracketed IPv6 address has several colons + ffStrbufSetNS(&host, (uint32_t) (colon - address), address); + port = colon + 1; + } else { + ffStrbufSetS(&host, address); + } + } + + if (host.length == 0) { + ffStrbufSetS(&host, "localhost"); + } + + if (*port == '\0') { + port = FF_SOUND_PA_DEFAULT_PORT; + } + + struct addrinfo hints = { .ai_family = family, .ai_socktype = SOCK_STREAM }; + struct addrinfo* result = nullptr; + if (getaddrinfo(host.chars, port, &hints, &result) != 0) { + FF_DEBUG("Failed to resolve the pulseaudio server address %s", address); + return -1; + } + + int fd = -1; + for (struct addrinfo* entry = result; entry; entry = entry->ai_next) { + fd = socket(entry->ai_family, entry->ai_socktype, entry->ai_protocol); + if (fd < 0) { + continue; + } + + if (connect(fd, entry->ai_addr, entry->ai_addrlen) == 0) { + break; + } + + close(fd); + fd = -1; + } + + freeaddrinfo(result); + + if (fd < 0) { + FF_DEBUG("Failed to connect to the pulseaudio server %s", address); + return -1; + } + + applyReceiveTimeout(fd); + return fd; +} + +// $PULSE_SERVER overrides the whole address and accepts "unix:", "tcp:", "tcp4:" +// and "tcp6:" prefixes as well as a plain socket path. +static int connectServer(const char* server, const FFstrbuf* defaultSocketPath) { + if (ffStrStartsWith(server, "unix:")) { + const char* path = server + strlen("unix:"); + return ffStrSet(path) ? connectUnixSocket(path) : connectUnixSocket(defaultSocketPath->chars); + } + + if (ffStrStartsWith(server, "tcp4:")) { + return connectTcpSocket(server + strlen("tcp4:"), AF_INET); + } + + if (ffStrStartsWith(server, "tcp6:")) { + return connectTcpSocket(server + strlen("tcp6:"), AF_INET6); + } + + if (ffStrStartsWith(server, "tcp:")) { + return connectTcpSocket(server + strlen("tcp:"), AF_UNSPEC); + } + + return connectUnixSocket(server); +} + +// $PULSE_RUNTIME_PATH is used as is, otherwise $XDG_RUNTIME_DIR/pulse. The socket +// is always named "native". The directory is deliberately not created: only the +// daemon owns it. +static void getDefaultSocketPath(FFstrbuf* path) { + const char* runtimePath = getenv("PULSE_RUNTIME_PATH"); + if (ffStrSet(runtimePath)) { + ffStrbufSetF(path, "%s/%s", runtimePath, FF_SOUND_PA_SOCKET_NAME); + return; + } + + const char* xdgRuntimeDir = getenv("XDG_RUNTIME_DIR"); + if (ffStrSet(xdgRuntimeDir)) { + ffStrbufSetF(path, "%s/pulse/%s", xdgRuntimeDir, FF_SOUND_PA_SOCKET_NAME); return; } - bool isActive = i->active_port && i->active_port->available != PA_PORT_AVAILABLE_NO; - if ((bundle->options->soundType & FF_SOUND_TYPE_ACTIVE) && !isActive) { + const char* home = getenv("HOME"); + if (ffStrSet(home)) { + ffStrbufSetF(path, "%s/.pulse/%s", home, FF_SOUND_PA_SOCKET_NAME); return; } - FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *bundle->result); - ffStrbufInitS(&device->identifier, i->name); - ffStrbufInitCopy(&device->platformApi, &bundle->serverName); + ffStrbufClear(path); +} + +// The daemon always compares 256 bytes, so the buffer is sent in full. When no +// cookie file is found the daemon still authorizes us by the credentials attached +// to the AUTH frame. +static void getCookie(char cookie[FF_SOUND_PA_COOKIE_LENGTH]) { + memset(cookie, 0, FF_SOUND_PA_COOKIE_LENGTH); + + FF_STRBUF_AUTO_DESTROY paths = ffStrbufCreate(); + const char* cookiePath = getenv("PULSE_COOKIE"); + if (ffStrSet(cookiePath)) { + ffStrbufSetS(&paths, cookiePath); + } else { + const char* configHome = getenv("XDG_CONFIG_HOME"); + const char* home = getenv("HOME"); + if (ffStrSet(configHome)) { + ffStrbufAppendF(&paths, "%s/pulse/cookie:", configHome); + } + if (ffStrSet(home)) { + ffStrbufAppendF(&paths, "%s/.config/pulse/cookie:", home); + ffStrbufAppendF(&paths, "%s/.pulse-cookie", home); + } + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + for (const char* path = paths.chars; ffStrSet(path);) { + const char* end = strchr(path, ':'); + FF_STRBUF_AUTO_DESTROY candidate = ffStrbufCreateNS(end ? (uint32_t) (end - path) : (uint32_t) strlen(path), path); + if (ffReadFileBuffer(candidate.chars, &buffer) && buffer.length > 0) { + memcpy(cookie, buffer.chars, buffer.length < FF_SOUND_PA_COOKIE_LENGTH ? buffer.length : FF_SOUND_PA_COOKIE_LENGTH); + FF_DEBUG("Using the pulseaudio cookie from %s", candidate.chars); + return; + } + + path = end ? end + 1 : nullptr; + } + + FF_DEBUG("No pulseaudio cookie found, relying on the credentials of the AUTH frame"); +} + +// --------------------------------------------------------------------------- +// frames +// --------------------------------------------------------------------------- + +// The daemon authorizes same user clients by the credentials of the last packet it +// read before AUTH, so they must not be omitted on Unix sockets. They are attached +// to the whole frame instead of the header alone: with two packets the daemon can +// read the second one without credentials and forget the ones it took from the +// first, which then fails AUTH with PA_ERR_ACCESS. Whether it does depends on how +// it happens to buffer the two writes, which makes that layout flaky. +static bool sendWithCredentials(int fd, const char* data, uint32_t length) { +#if FF_SOUND_PA_HAS_CREDENTIALS + struct iovec iov = { .iov_base = (void*) data, .iov_len = length }; + char control[CMSG_SPACE(sizeof(struct ucred))]; + struct msghdr message = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = control, + .msg_controllen = sizeof(control), + }; + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&message); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_CREDENTIALS; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct ucred)); + memcpy(CMSG_DATA(cmsg), &(struct ucred){ .pid = getpid(), .uid = getuid(), .gid = getgid() }, sizeof(struct ucred)); + + ssize_t sent; + do { + sent = sendmsg(fd, &message, FF_SOUND_PA_SEND_FLAGS); + } while (sent < 0 && errno == EINTR); + + if (sent < 0) { + FF_DEBUG("Failed to send the pulseaudio frame: %s", strerror(errno)); + return false; + } + + return writeAll(fd, data + sent, length - (uint32_t) sent); // the remainder carries no credentials +#else + return writeAll(fd, data, length); +#endif +} + +static bool sendFrame(FFSoundPulseConn* conn, const FFstrbuf* payload, bool withCredentials) { + FF_STRBUF_AUTO_DESTROY frame = ffStrbufCreate(); + ffStrbufAppendNC(&frame, FF_SOUND_PA_FRAME_HEADER_SIZE, '\0'); + frame.chars[0] = (char) (payload->length >> 24); + frame.chars[1] = (char) (payload->length >> 16); + frame.chars[2] = (char) (payload->length >> 8); + frame.chars[3] = (char) payload->length; + frame.chars[4] = frame.chars[5] = frame.chars[6] = frame.chars[7] = (char) 0xFF; // channel: control packet + ffStrbufAppend(&frame, payload); + + if (withCredentials) { + return sendWithCredentials(conn->fd, frame.chars, frame.length); + } + + return writeAll(conn->fd, frame.chars, frame.length); +} + +static bool recvFrame(FFSoundPulseConn* conn) { + alignas(uint32_t) char header[FF_SOUND_PA_FRAME_HEADER_SIZE]; + if (!readAll(conn->fd, header, sizeof(header))) { + return false; + } + + uint32_t length = FF_READ_BE(*(uint32_t*) header); + if (length == 0 || length > FF_SOUND_PA_FRAME_LENGTH_MAX) { + FF_DEBUG("Refusing a pulseaudio frame of %u bytes", length); + return false; + } + + ffStrbufEnsureFixedLengthFree(&conn->frame, length); + if (!readAll(conn->fd, conn->frame.chars, length)) { + return false; + } + + conn->frame.chars[length] = '\0'; + conn->frame.length = length; + return true; +} + +// Waits for the reply to `tag`. On success the reader is positioned after the +// command and tag fields. +static const char* recvReply(FFSoundPulseConn* conn, uint32_t tag, FFSoundPulseReader* reader) { + while (true) { + if (!recvFrame(conn)) { + FF_DEBUG("Failed to read the pulseaudio reply to tag %u", tag); + return "Failed to read a pulseaudio reply"; + } + + FFSoundPulseReader frame = { .data = conn->frame.chars, .length = conn->frame.length }; + uint32_t command = 0; + uint32_t replyTag = 0; + // Both the command and the tag are tagged u32s, so they must be read as + // values. Reading four raw bytes here would swallow the 'L' of the tag. + if (!readU32Value(&frame, &command) || !readU32Value(&frame, &replyTag)) { + FF_DEBUG("Malformed pulseaudio reply to tag %u: %u payload bytes", tag, frame.length); + return "Malformed pulseaudio reply"; + } + + if (replyTag != tag) { + // Server initiated commands use the tag (uint32_t) -1 and are only + // sent to subscribed clients. Ignore everything that is not ours. + continue; + } + + if (command == FF_SOUND_PA_COMMAND_ERROR) { + uint32_t error = 0; + if (!readU32Value(&frame, &error)) { + FF_DEBUG("Malformed pulseaudio error reply to tag %u: %u payload bytes", tag, frame.length); + return "Malformed pulseaudio error reply"; + } + + FF_DEBUG("The pulseaudio server returned the error %u", error); + if (error == FF_SOUND_PA_ERROR_ACCESS) { + FF_DEBUG("The pulseaudio server denied access to the AUTH request"); + return "Access to the pulseaudio server was denied"; + } + + if (error == FF_SOUND_PA_ERROR_VERSION) { + FF_DEBUG("The pulseaudio server rejected our protocol version %u", FF_SOUND_PA_VERSION); + return "The pulseaudio server is too old"; + } + + return "The pulseaudio server refused the request"; + } + + if (command != FF_SOUND_PA_COMMAND_REPLY) { + FF_DEBUG("Unexpected pulseaudio command %u in the reply to tag %u", command, tag); + return "Unexpected pulseaudio reply"; + } + + *reader = frame; + return nullptr; + } +} + +// --------------------------------------------------------------------------- +// handshake +// --------------------------------------------------------------------------- + +static const char* handshake(FFSoundPulseConn* conn, FFstrbuf* serverName, FFstrbuf* defaultSinkName) { + { + FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate(); + uint32_t tag = conn->nextTag++; + putTagU32(&payload, FF_SOUND_PA_COMMAND_AUTH); + putTagU32(&payload, tag); + putTagU32(&payload, FF_SOUND_PA_VERSION); // neither PA_PROTOCOL_FLAG_SHM nor PA_PROTOCOL_FLAG_MEMFD + + char cookie[FF_SOUND_PA_COOKIE_LENGTH]; + getCookie(cookie); + putTagArbitrary(&payload, cookie, sizeof(cookie)); + + if (!sendFrame(conn, &payload, true)) { + FF_DEBUG("Failed to send the pulseaudio AUTH request (tag %u)", tag); + return "Failed to send the pulseaudio AUTH request"; + } + + FFSoundPulseReader reader; + const char* error = recvReply(conn, tag, &reader); + if (error) { + return error; + } + + uint32_t version = 0; + if (!readU32Value(&reader, &version)) { + return "Malformed pulseaudio AUTH reply"; + } + + conn->version = version & FF_SOUND_PA_VERSION_MASK; + if (conn->version > FF_SOUND_PA_VERSION) { + conn->version = FF_SOUND_PA_VERSION; + } + + FF_DEBUG("Negotiated pulseaudio protocol version %u", conn->version); + } + + { + FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate(); + uint32_t tag = conn->nextTag++; + putTagU32(&payload, FF_SOUND_PA_COMMAND_SET_CLIENT_NAME); + putTagU32(&payload, tag); + + if (conn->version >= 13) { + putTagProplist(&payload, "application.name", "fastfetch"); + } else { + putTagString(&payload, "fastfetch"); + } + + if (!sendFrame(conn, &payload, false)) { + FF_DEBUG("Failed to send the pulseaudio SET_CLIENT_NAME request (tag %u)", tag); + return "Failed to send the pulseaudio SET_CLIENT_NAME request"; + } + + FFSoundPulseReader reader; + const char* error = recvReply(conn, tag, &reader); + if (error) { + return error; + } + } + + { + FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate(); + uint32_t tag = conn->nextTag++; + putTagU32(&payload, FF_SOUND_PA_COMMAND_GET_SERVER_INFO); + putTagU32(&payload, tag); + + if (!sendFrame(conn, &payload, false)) { + FF_DEBUG("Failed to send the pulseaudio GET_SERVER_INFO request (tag %u)", tag); + return "Failed to send the pulseaudio GET_SERVER_INFO request"; + } + + FFSoundPulseReader reader; + const char* error = recvReply(conn, tag, &reader); + if (error) { + return error; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY version = ffStrbufCreate(); + if (!readString(&reader, &name) || !readString(&reader, &version)) { + return "Malformed pulseaudio GET_SERVER_INFO reply"; + } + + if (!skipValue(&reader) || !skipValue(&reader)) { // user name, host name + return "Malformed pulseaudio GET_SERVER_INFO reply"; + } + + if (!skipValue(&reader)) { // sample spec + return "Malformed pulseaudio GET_SERVER_INFO reply"; + } + + if (!readString(&reader, defaultSinkName)) { // default sink name, may be a NULL string + return "Malformed pulseaudio GET_SERVER_INFO reply"; + } + + FF_DEBUG("Default pulseaudio sink: %s", defaultSinkName->chars); + + // pipewire-pulse reports itself as "PulseAudio (on PipeWire x.y.z)" + const char* on = strstr(name.chars, "(on "); + if (on) { + ffStrbufSetS(serverName, on + strlen("(on ")); + ffStrbufTrimRight(serverName, ')'); + } else { + ffStrbufSetF(serverName, "%s %s", name.chars, version.chars); + } + } + + return nullptr; +} + +// --------------------------------------------------------------------------- +// sinks +// --------------------------------------------------------------------------- + +// The port that is reported as active decides whether a sink counts as active. +// Since its name is sent after the port list, the list is scanned a second time +// to look up the availability of that port. +static bool isPortAvailable(const FFSoundPulseReader* reader, uint32_t version, uint32_t portCount, uint32_t portsOffset, const char* activePort) { + if (version < 24) { + return true; // ports carry no availability information before v24 + } + + FFSoundPulseReader scan = *reader; + scan.offset = portsOffset; + scan.error = false; + + for (uint32_t port = 0; port < portCount; port++) { + const char* name = readStringRef(&scan); + if (scan.error || !skipValue(&scan) || !skipValue(&scan)) { // description, priority + return true; + } + + uint32_t available = 0; + if (!readU32Value(&scan, &available)) { + return true; + } + + if (version >= 34) { // availability group, type + if (!skipValue(&scan) || !skipValue(&scan)) { + return true; + } + } + + if (name && ffStrEquals(name, activePort)) { + return available != FF_SOUND_PA_PORT_AVAILABLE_NO; + } + } + + return true; +} + +// Sink objects are sent in the order of sink_fill_tagstruct(). Everything that is +// not needed is skipped; the fields that are read are tag checked so that a +// layout mismatch is reported instead of silently shifting the whole list. +static bool readSinkInfo(FFSoundPulseReader* reader, const FFSoundPulseConn* conn, const FFstrbuf* defaultSinkName, FFSoundPulseSinkInfo* sink) { + uint32_t version = conn->version; + + if (!skipValue(reader)) { // index + return false; + } + + sink->name = readStringRef(reader); + if (reader->error || !sink->name) { + return false; + } + + sink->description = readStringRef(reader); + if (reader->error) { + return false; + } + + if (!skipValue(reader)) { // sample spec + return false; + } + + if (!skipValue(reader)) { // channel map + return false; + } + + if (!skipValue(reader)) { // owner module + return false; + } + + if (!readCVolume(reader, &sink->volume)) { + return false; + } + + if (!readBoolean(reader, &sink->mute)) { + return false; + } + + if (!skipValue(reader)) { // monitor source + return false; + } + + if (!skipValue(reader)) { // monitor source name + return false; + } + + if (!skipValue(reader)) { // latency + return false; + } + + if (!skipValue(reader)) { // driver + return false; + } + + if (!skipValue(reader)) { // flags + return false; + } + + if (version >= 13) { + if (!skipValue(reader)) { // proplist + return false; + } + + if (!skipValue(reader)) { // configured latency + return false; + } + } + + if (version >= 15) { + for (uint32_t field = 0; field < 4; field++) { // base volume, state, volume steps, card + if (!skipValue(reader)) { + return false; + } + } + } + + const char* activePort = nullptr; + uint32_t portCount = 0; + uint32_t portsOffset = 0; + + if (version >= 16) { + if (!readU32Value(reader, &portCount)) { + return false; + } + + portsOffset = reader->offset; + for (uint32_t port = 0; port < portCount; port++) { + if (!skipValue(reader)) { // name + return false; + } + + if (!skipValue(reader)) { // description + return false; + } + + if (!skipValue(reader)) { // priority + return false; + } + + if (version >= 24 && !skipValue(reader)) { // availability + return false; + } + + if (version >= 34) { + if (!skipValue(reader) || !skipValue(reader)) { // availability group, type + return false; + } + } + } + + activePort = readStringRef(reader); // the last field of the port section + if (reader->error) { + return false; + } + } + + if (version >= 21) { + uint8_t formats = 0; + if (!readU8Value(reader, &formats)) { // number of format infos + return false; + } + + for (uint8_t format = 0; format < formats; format++) { + if (!skipValue(reader)) { // format info + return false; + } + } + } + + sink->main = ffStrbufEqualS(defaultSinkName, sink->name); + sink->active = activePort && isPortAvailable(reader, version, portCount, portsOffset, activePort); + return true; +} + +static void appendSink(FFlist* devices, const FFSoundPulseSinkInfo* sink, const FFstrbuf* serverName) { + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInitS(&device->identifier, sink->name); ffStrbufTrimRightSpace(&device->identifier); - ffStrbufInitS(&device->name, i->description); + ffStrbufInitCopy(&device->platformApi, serverName); + ffStrbufInitS(&device->name, sink->description ? sink->description : ""); ffStrbufTrimRightSpace(&device->name); ffStrbufTrimLeft(&device->name, ' '); - device->volume = i->mute ? 0 : (uint8_t) ((i->volume.values[0] * 100 + PA_VOLUME_NORM / 2 /*round*/) / PA_VOLUME_NORM); - device->type = (isMain ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | (isActive ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE); + device->volume = sink->mute ? 0 : (uint8_t) (((uint64_t) sink->volume * 100 + FF_SOUND_PA_VOLUME_NORM / 2) / FF_SOUND_PA_VOLUME_NORM); + device->type = (sink->main ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | (sink->active ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE); } -static void paServerInfoCallback([[maybe_unused]] pa_context* c, const pa_server_info* i, void* userdata) { - if (!i) { - return; +static const char* fetchSinkList(FFSoundPulseConn* conn, const FFSoundOptions* options, FFlist* devices, const FFstrbuf* serverName, const FFstrbuf* defaultSinkName) { + FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate(); + uint32_t tag = conn->nextTag++; + putTagU32(&payload, FF_SOUND_PA_COMMAND_GET_SINK_INFO_LIST); + putTagU32(&payload, tag); + + if (!sendFrame(conn, &payload, false)) { + FF_DEBUG("Failed to send the pulseaudio GET_SINK_INFO_LIST request (tag %u)", tag); + return "Failed to send the pulseaudio GET_SINK_INFO_LIST request"; } - struct DetectionInfoBundle* bundle = userdata; + FFSoundPulseReader reader; + const char* error = recvReply(conn, tag, &reader); + if (error) { + return error; + } - const char* realServer = strstr(i->server_name, "(on "); - if (realServer) { - ffStrbufSetS(&bundle->serverName, realServer + strlen("(on ")); - ffStrbufTrimRight(&bundle->serverName, ')'); - } else { - ffStrbufSetF(&bundle->serverName, "%s %s", i->server_name, i->server_version); + // All sinks are concatenated in a single reply without a separator, a count + // or a terminator: parse until the payload is exhausted. + while (!reader.error && !readerEof(&reader)) { + FFSoundPulseSinkInfo sink = {}; + if (!readSinkInfo(&reader, conn, defaultSinkName, &sink)) { + FF_DEBUG("Failed to parse a pulseaudio sink at payload offset %u of %u", reader.offset, reader.length); + return "Failed to parse the pulseaudio sink list"; + } + + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !sink.main) { + continue; + } + + if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !sink.active) { + continue; + } + + appendSink(devices, &sink, serverName); } - ffStrbufSetS(&bundle->defaultDeviceId, i->default_sink_name); + if (reader.error) { + FF_DEBUG("The pulseaudio sink list ended at payload offset %u of %u", reader.offset, reader.length); + return "Failed to parse the pulseaudio sink list"; + } + + FF_DEBUG("Parsed %u pulseaudio sinks", devices->length); + return nullptr; } -static const char* detectSound(FFSoundOptions* options, FFlist* devices) { - FF_LIBRARY_LOAD_MESSAGE(pulse, "libpulse" FF_LIBRARY_EXTENSION, 0) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_new) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_get_api) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_iterate) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_free) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_new) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_connect) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_state) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_sink_info_list) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_server_info) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_unref) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_cancel) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_get_state) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_unref) +// --------------------------------------------------------------------------- +// the hardware: ALSA +// --------------------------------------------------------------------------- + +// A machine that runs no sound server still has ALSA, and this is what answers for it: the cards +// are enumerated from /proc and their volumes read from the control device of each card. +// +// Nothing is linked and nothing is loaded. The values are behind four ioctls of the kernel's own +// control interface, and both the structs and the request numbers are the kernel's uapi, which +// every libc ships as . The request numbers are `_IOWR`-style, so they carry the +// size of the struct they pass -- a layout that does not match the kernel's is refused as an +// unknown ioctl instead of being read as garbage. +// +// Android is left out: it has no ALSA of the kind this reads ( /dev/snd and /proc/asound are +// closed to an app's SELinux domain ) and a session that does have a desktop on top of the device +// is running a sound server, which is the branch below. A libc that does not carry the header says +// so here, and the fallback is simply not compiled in -- the same shape as the other optional +// backends in this tree. +#if defined(__linux__) && !defined(__ANDROID__) && __has_include() + #define FF_SOUND_ALSA 1 +#endif + +#ifdef FF_SOUND_ALSA + + #include + #include + // before , in this order: the uapi header spells its request + // numbers with _IOR and _IOWR and does not define either of them itself. + #include + #include + +// What a mixer control *is* is written down nowhere in the control interface. The kernel describes +// a control with a type (integer, boolean, ...) and an access bitmask, and that bitmask has no +// volume and no switch bit at all -- the uapi header defines READ, WRITE, VOLATILE, TLV_*, INACTIVE, +// LOCK, OWNER and USER, and nothing else. What carries it is the control's name, which a driver +// builds by appending a suffix to the name of what is being controlled: +// +// "Master Playback Volume" the output volume of the control named "Master" +// "Capture Volume" the input volume of a control that has no name of its own +// +// So the suffix is what is matched, on one condition: it has to be the whole of the name or the +// start of a word in it. Without that, "Capture Volume" would end in " Volume" and be read as the +// directionless " Volume" -- which is the volume of a card that has only one direction, and so an +// output. +// +// The set of suffixes is the one libasound's simple mixer layer carries; the entries sit one after +// another in that library's .rodata. The order below is the order they have to be tried in, the +// longest first, or "Master Playback Volume" would be taken for a " Volume". +// +// One deliberate difference from libasound: it reads "Capture Volume" as a directionless volume, +// which is why a desktop mixer shows a "Capture" slider. This module reports output devices, and a +// capture gain is not one of them -- so a card that carries nothing else has no output to report. +typedef struct FFSoundAlsaSuffix { + const char* suffix; + bool isSwitch; // ... the suffix names a switch rather than a volume + bool playback; // ... and one of the output direction +} FFSoundAlsaSuffix; + +static const FFSoundAlsaSuffix FF_SOUND_ALSA_SUFFIXES[] = { + { "Playback Volume", false, true }, + { "Playback Switch", true, true }, + { "Playback Enum", false, false }, + { "Playback Route", false, false }, + { "Capture Volume", false, false }, + { "Capture Switch", false, false }, + { "Capture Enum", false, false }, + { "Capture Route", false, false }, + { "Volume", false, true }, + { "Switch", true, true }, + { "Enum", false, false }, + { "Route", false, false }, +}; + +// Which control is "the volume" of a card. A desktop mixer shows one slider per output and the name +// on it is one of these. A card that has none of them -- a USB interface, a virtual card -- still +// has exactly one playback volume, and there the first one the kernel enumerates is taken. This list +// exists so that the choice does not have to come out of that order, which is the order the driver +// registered its controls in and not a ranking at all. +static const char* const FF_SOUND_ALSA_MAIN_CONTROLS[] = { "Master", "Master Mono", "PCM", "Speaker", "Headphone", "Headphones", "Front" }; + +enum { + // Ids asked for per ELEM_LIST call. The list is walked in batches rather than copied in one go, + // so that nothing has to be sized after a number the card reports; 64 ids is 4 KiB of stack, and + // the far side of the ioctl is a table lookup either way. + FF_SOUND_ALSA_ID_BATCH = 64, + FF_SOUND_ALSA_PRIORITY_NONE = UINT32_MAX, +}; + +// Whether the control name is that of a playback volume or switch, and how much of it the suffix +// takes up. Both suffixes that are accepted end in the same six character tail -- "Volume" or +// "Switch" -- which is the only thing that pairs a card's volume with its mute; nothing else in the +// control interface says the two belong together. +static bool stripAlsaNameSuffix(const char* name, uint32_t length, bool* isSwitch, uint32_t* baseLength) { + for (size_t index = 0; index < ARRAY_SIZE(FF_SOUND_ALSA_SUFFIXES); index++) { + const FFSoundAlsaSuffix* suffix = &FF_SOUND_ALSA_SUFFIXES[index]; + const uint32_t suffixLength = (uint32_t) strlen(suffix->suffix); + if (length < suffixLength || memcmp(name + length - suffixLength, suffix->suffix, suffixLength) != 0) { + continue; + } + + // A suffix is a word of the name: either all of it, or preceded by a space. Anything else + // is a coincidence of letters -- "Capture Volume" ends in " Volume" and is not one. + if (length > suffixLength && name[length - suffixLength - 1] != ' ') { + continue; + } + + if (!suffix->playback) { + return false; // a capture or a routing control is not the card's output + } + + // The space that separated the base name from the suffix belongs to neither, and a name + // that is nothing but the suffix has no base name at all. + uint32_t base = length - suffixLength; + while (base > 0 && name[base - 1] == ' ') { + base--; + } - pa_mainloop* mainloop = ffpa_mainloop_new(); - if (!mainloop) { - return "Failed to create pulseaudio mainloop"; + *isSwitch = suffix->isSwitch; + *baseLength = base; + return true; } - pa_mainloop_api* mainloopApi = ffpa_mainloop_get_api(mainloop); - if (!mainloopApi) { - ffpa_mainloop_free(mainloop); - return "Failed to get pulseaudio mainloop api"; + return false; +} + +// The rank of a control's base name, or FF_SOUND_ALSA_PRIORITY_NONE when the list does not know it. +static uint32_t getAlsaControlPriority(const char* name, uint32_t baseLength) { + for (size_t index = 0; index < ARRAY_SIZE(FF_SOUND_ALSA_MAIN_CONTROLS); index++) { + if (strlen(FF_SOUND_ALSA_MAIN_CONTROLS[index]) == baseLength && memcmp(name, FF_SOUND_ALSA_MAIN_CONTROLS[index], baseLength) == 0) { + return (uint32_t) index; + } } - pa_context* context = ffpa_context_new(mainloopApi, "fastfetch"); - if (!context) { - ffpa_mainloop_free(mainloop); - return "Failed to create pulseaudio context"; + return FF_SOUND_ALSA_PRIORITY_NONE; +} + +// Walks the controls of one card. ELEM_LIST holds no state -- every call carries the offset it wants +// and the buffer to fill -- so a second walk of the same card is simply a second one of these. +typedef struct FFSoundAlsaControls { + int fd; + uint32_t offset; + uint32_t index; + uint32_t batch; + bool done; + struct snd_ctl_elem_id ids[FF_SOUND_ALSA_ID_BATCH]; +} FFSoundAlsaControls; + +// The next mixer control of the card, or nullptr at the end. A list that cannot be read ends the +// walk rather than failing it: what to do about "this card says nothing" belongs to the caller, and +// everything below treats it exactly like a card that has no volume control. The other interfaces a +// card carries (PCM, timer, raw MIDI) are skipped here so that no caller has to know about them. +static const struct snd_ctl_elem_id* nextAlsaControl(FFSoundAlsaControls* controls) { + for (;;) { + if (controls->index < controls->batch) { + const struct snd_ctl_elem_id* id = &controls->ids[controls->index++]; + if (id->iface != SNDRV_CTL_ELEM_IFACE_MIXER) { + continue; + } + + return id; + } + + if (controls->done) { + return nullptr; + } + + struct snd_ctl_elem_list list = { + .offset = controls->offset, + .space = FF_SOUND_ALSA_ID_BATCH, + .pids = controls->ids, + }; + + if (ioctl(controls->fd, SNDRV_CTL_IOCTL_ELEM_LIST, &list) < 0) { + FF_DEBUG("Listing the controls of an ALSA card failed: %s", strerror(errno)); + controls->done = true; + return nullptr; + } + + controls->offset += list.used; + controls->index = 0; + controls->batch = list.used; + + if (list.used == 0) { + controls->done = true; + return nullptr; + } } +} - struct DetectionInfoBundle bundle = { - .serverName = ffStrbufCreate(), - .defaultDeviceId = ffStrbufCreate(), - .result = devices, - .options = options, - }; - const char* error = nullptr; +// A card's volume control and its name without the suffix -- which is what a desktop mixer puts on +// the slider, "Master" rather than "Master Playback Volume". +typedef struct FFSoundAlsaVolume { + struct snd_ctl_elem_id id; + char base[SNDRV_CTL_ELEM_ID_NAME_MAXLEN]; + uint32_t baseLength; +} FFSoundAlsaVolume; + +static bool findAlsaVolume(FFSoundAlsaControls* controls, FFSoundAlsaVolume* volume) { + uint32_t priority = FF_SOUND_ALSA_PRIORITY_NONE; + bool found = false; + + const struct snd_ctl_elem_id* id = nullptr; + while ((id = nextAlsaControl(controls)) != nullptr) { + // The name is an array of SNDRV_CTL_ELEM_ID_NAME_MAXLEN bytes with the terminator inside + // it, so its length is bounded rather than taken on trust. + const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN); + bool isSwitch = false; + uint32_t baseLength = 0; + if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || isSwitch) { + continue; + } - if (ffpa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) { - error = "Failed to connect to pulseaudio context"; - goto exit; + const uint32_t candidate = getAlsaControlPriority((const char*) id->name, baseLength); + if (found && candidate >= priority) { + continue; // a control the list knows beats one it does not, and the first of a rank wins + } + + volume->id = *id; + volume->baseLength = baseLength; + memcpy(volume->base, id->name, baseLength); + volume->base[baseLength] = '\0'; + priority = candidate; + found = true; } - pa_context_state_t state; - while ((state = ffpa_context_get_state(context)) != PA_CONTEXT_READY) { - if (!PA_CONTEXT_IS_GOOD(state)) { - error = "Failed to get pulseaudio context state"; - goto exit; + return found; +} + +// The switch that belongs to the volume control: same base name, same index, same device. It takes +// a second walk because which base name wins is only known once the first walk is over. A card with +// no switch at all leaves the caller with "not muted", which is the answer a control without a mute +// state deserves. +static bool findAlsaSwitch(FFSoundAlsaControls* controls, const FFSoundAlsaVolume* volume, struct snd_ctl_elem_id* result) { + const struct snd_ctl_elem_id* id = nullptr; + while ((id = nextAlsaControl(controls)) != nullptr) { + if (id->index != volume->id.index || id->device != volume->id.device || id->subdevice != volume->id.subdevice) { + continue; } - ffpa_mainloop_iterate(mainloop, 1, nullptr); + const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN); + bool isSwitch = false; + uint32_t baseLength = 0; + if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || !isSwitch) { + continue; + } + + if (baseLength != volume->baseLength || memcmp(id->name, volume->base, baseLength) != 0) { + continue; + } + + *result = *id; + return true; } - { - pa_operation* operation = ffpa_context_get_server_info(context, paServerInfoCallback, &bundle); - if (!operation) { - error = "Failed to get pulseaudio server info"; - goto exit; + return false; +} + +// The output of a card that has no volume control: any playback switch, the first one the kernel +// enumerates. HDMI and DP cards are typically this shape, with an "IEC958 Playback Switch" and no +// volume to go with it, so the switch is the only thing that says the card plays anything at all. +// Returns whether the card has a playback switch, so a caller can tell "no output" from "a switch". +static bool findAlsaPlaybackSwitch(FFSoundAlsaControls* controls, FFSoundAlsaVolume* output) { + const struct snd_ctl_elem_id* id = nullptr; + while ((id = nextAlsaControl(controls)) != nullptr) { + const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN); + bool isSwitch = false; + uint32_t baseLength = 0; + if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || !isSwitch) { + continue; } - while (ffpa_operation_get_state(operation) == PA_OPERATION_RUNNING) { - ffpa_mainloop_iterate(mainloop, 1, nullptr); + + output->id = *id; + output->baseLength = baseLength; + memcpy(output->base, id->name, baseLength); + output->base[baseLength] = '\0'; + return true; + } + + return false; +} + +// One value of a control, and the range that control's own scale puts it in. A switch is a boolean +// and shares the same union; a few DSP cards use the 64 bit integer type. +typedef struct FFSoundAlsaValue { + long long value; + long long minimum; + long long maximum; +} FFSoundAlsaValue; + +static bool readAlsaValue(int fd, const struct snd_ctl_elem_id* id, FFSoundAlsaValue* result) { + struct snd_ctl_elem_info info = { .id = *id }; + if (ioctl(fd, SNDRV_CTL_IOCTL_ELEM_INFO, &info) < 0) { + FF_DEBUG("Reading the type of an ALSA control failed: %s", strerror(errno)); + return false; + } + + struct snd_ctl_elem_value value = { .id = *id }; + if (ioctl(fd, SNDRV_CTL_IOCTL_ELEM_READ, &value) < 0) { + FF_DEBUG("Reading the value of an ALSA control failed: %s", strerror(errno)); + return false; + } + + // Only the first channel is read, the way the PulseAudio backend reports the first channel of a + // sink's volume. A card whose channels disagree is left to the mixer to sort out. + if (info.type == SNDRV_CTL_ELEM_TYPE_INTEGER64) { + result->value = value.value.integer64.value[0]; + result->minimum = info.value.integer64.min; + result->maximum = info.value.integer64.max; + return true; + } + + if (info.type != SNDRV_CTL_ELEM_TYPE_INTEGER && info.type != SNDRV_CTL_ELEM_TYPE_BOOLEAN) { + FF_DEBUG("An ALSA control carries the value type %u, which is not a volume", (unsigned) info.type); + return false; + } + + result->value = value.value.integer.value[0]; + result->minimum = info.value.integer.min; + result->maximum = info.value.integer.max; + return true; +} + +// The control's own scale as a percentage, which is the number `amixer` prints as `[n%]`. A range +// that is empty or inverted says nothing at all, and is reported as unknown rather than as 0% -- +// 0% is a state a control is really in, and it is the muted one. +static uint8_t getAlsaPercent(const FFSoundAlsaValue* value) { + if (value->maximum <= value->minimum) { + return FF_SOUND_VOLUME_UNKNOWN; + } + + if (value->value <= value->minimum) { + return 0; + } + + if (value->value >= value->maximum) { + return 100; + } + + return (uint8_t) (((value->value - value->minimum) * 100 + (value->maximum - value->minimum) / 2) / (value->maximum - value->minimum)); +} + +// Appends the output of one card. Returns whether the card has an output to report at all, which is +// not the same as whether a device was appended: `options` filters what is kept, while the main +// device of a machine is the first card that has an output whatever the filter then does with it. +// +// A card whose control device cannot be opened is reported without a volume rather than dropped. +// The devices are root:audio behind an ACL for the seat, so a session over ssh can name a card it +// may not read, and the name comes from /proc/asound/cards, which is world readable. +// +// The reported name is the card's own name from /proc/asound/cards followed by the control's base +// name -- "HDA Intel PCH Master" rather than a bare "Master" -- so the list says which card each +// volume belongs to. A card that *can* be read and has no playback control at all is dropped +// instead, because that is a card with nothing to play through. An HDMI or DP card often has no +// volume control but does have an "IEC958 Playback Switch"; the switch alone is still an output, so +// such a card is reported, with no percentage to go with it. +static bool appendAlsaCard(uint32_t card, const FFstrbuf* cardName, bool main, const FFSoundOptions* options, FFlist* devices) { + // "/dev/snd/controlC", at most ten digits of the card index, and the terminator. + char path[sizeof("/dev/snd/controlC") + 11]; + snprintf(path, sizeof(path), "/dev/snd/controlC%u", card); + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateCopy(cardName); + // A card whose control device cannot be opened is not a card without an output, so it still + // gets a device -- without a percentage. + uint8_t volume = FF_SOUND_VOLUME_UNKNOWN; + bool found = false; + bool active = true; + + const int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + // Only read access is asked for: a volume is being read, never written, and read permission + // is the smaller of the two things to need. + FF_DEBUG("Opening \"%s\" failed: %s", path, strerror(errno)); + found = true; + } else { + FFSoundAlsaControls controls = { .fd = fd }; + FFSoundAlsaVolume control = {}; + + if (findAlsaVolume(&controls, &control)) { + found = true; + // A control whose name is nothing but the suffix -- a codec that has one volume and no + // name for it registers "Playback Volume" -- leaves the card's own name in place, which + // is the only thing left to call the device. + if (control.baseLength > 0) { + ffStrbufAppendC(&name, ' '); + ffStrbufAppendNS(&name, control.baseLength, control.base); + } + + FFSoundAlsaValue value = {}; + if (readAlsaValue(fd, &control.id, &value)) { + volume = getAlsaPercent(&value); + } + + FFSoundAlsaControls switches = { .fd = fd }; + struct snd_ctl_elem_id switchId = {}; + FFSoundAlsaValue switchValue = {}; + if (findAlsaSwitch(&switches, &control, &switchId) && readAlsaValue(fd, &switchId, &switchValue)) { + active = switchValue.value != 0; + if (!active) { + // A muted control is reported as 0% rather than as the value it still holds, + // which is what the PulseAudio backend does with a muted sink. + volume = 0; + } + } + } else { + // No volume control: an HDMI or DP card is usually this shape, its only output control + // an "IEC958 Playback Switch". The switch alone is still an output to play through, so + // the card is reported -- without a percentage, because there is no volume to read. + FFSoundAlsaControls switches = { .fd = fd }; + FFSoundAlsaVolume switchControl = {}; + if (findAlsaPlaybackSwitch(&switches, &switchControl)) { + found = true; + if (switchControl.baseLength > 0) { + ffStrbufAppendC(&name, ' '); + ffStrbufAppendNS(&name, switchControl.baseLength, switchControl.base); + } + + FFSoundAlsaValue switchValue = {}; + if (readAlsaValue(fd, &switchControl.id, &switchValue)) { + active = switchValue.value != 0; + if (!active) { + volume = 0; + } + } + } } - ffpa_operation_unref(operation); + close(fd); } - { - pa_operation* operation = ffpa_context_get_sink_info_list(context, paSinkInfoCallback, &bundle); - if (!operation) { - error = "Failed to get pulseaudio sink info list"; - goto exit; + if (!found) { + FF_DEBUG("Card %u has no playback control", card); + return false; + } + + const FFSoundType type = (FFSoundType) ((main ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | (active ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE)); + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !(type & FF_SOUND_TYPE_MAIN)) { + return true; + } + + if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !(type & FF_SOUND_TYPE_ACTIVE)) { + return true; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + // "hw:0" is how the card is named everywhere else -- `amixer -c 0`, and the name after a colon + // in every ALSA configuration -- so it is what identifies the device here. + ffStrbufInitF(&device->identifier, "hw:%u", card); + ffStrbufInitMove(&device->name, &name); + ffStrbufInitStatic(&device->platformApi, "ALSA"); + device->volume = volume; + device->type = type; + FF_DEBUG("Reporting \"%s\" (hw:%u) at %u%%%s", device->name.chars, card, device->volume, active ? "" : ", muted"); + return true; +} + +static const char* detectAlsa(FFSoundOptions* options, FFlist* devices) { + FF_STRBUF_AUTO_DESTROY cards = ffStrbufCreate(); + if (!ffReadFileBuffer("/proc/asound/cards", &cards)) { + FF_DEBUG("Reading \"/proc/asound/cards\" failed: %s", strerror(errno)); + return "Failed to read the ALSA card list"; + } + + bool main = true; // the machine's main output is the first card that has one + bool output = false; + + for (const char* line = cards.chars; *line;) { + const char* newline = strchr(line, '\n'); + const uint32_t length = newline ? (uint32_t) (newline - line) : (uint32_t) strlen(line); + FF_STRBUF_AUTO_DESTROY record = ffStrbufCreateNS(length, line); + line = newline ? newline + 1 : line + length; + + // 0 [PCH ]: HDA-Intel - HDA Intel PCH + // HDA Intel PCH at 0xdf240000 irq 125 + // + // One line per card, with the index in the first column, and the card's long name on the + // line below -- which does not start with a digit, and is what this skips. The name that is + // reported is the one behind the dash: the driver's name for the card, without the + // "HDA-Intel - " in front of it. + char* indexEnd = nullptr; + const long number = strtol(record.chars, &indexEnd, 10); + if (indexEnd == record.chars || number < 0) { + continue; + } + + const char* close = strchr(indexEnd, ']'); + const char* dash = close ? strstr(close, " - ") : nullptr; + if (dash == nullptr) { + FF_DEBUG("The ALSA card list line \"%s\" is not in the expected form", record.chars); + continue; } - while (ffpa_operation_get_state(operation) == PA_OPERATION_RUNNING) { - if (options->soundType & FF_SOUND_TYPE_MAIN && devices->length > 0) { - ffpa_operation_cancel(operation); + FF_STRBUF_AUTO_DESTROY cardName = ffStrbufCreateS(dash + strlen(" - ")); + ffStrbufTrimRightSpace(&cardName); + + if (appendAlsaCard((uint32_t) number, &cardName, main, options, devices)) { + main = false; + output = true; + } + } + + if (!output) { + return "No ALSA card with a playback control was found"; + } + + return nullptr; +} + +#endif // FF_SOUND_ALSA + +// --------------------------------------------------------------------------- +// the server +// --------------------------------------------------------------------------- + +static const char* detectPulseAudio(FFSoundOptions* options, FFlist* devices) { + FF_STRBUF_AUTO_DESTROY defaultSocketPath = ffStrbufCreate(); + getDefaultSocketPath(&defaultSocketPath); + FF_DEBUG("Default pulseaudio socket path: %s", defaultSocketPath.chars); + + FFSoundPulseConn conn = { + .fd = -1, + .version = FF_SOUND_PA_VERSION, + .nextTag = 0, + .frame = ffStrbufCreate(), + }; + + const char* serverList = getenv("PULSE_SERVER"); + if (ffStrSet(serverList)) { + // $PULSE_SERVER may list several whitespace separated candidates; the + // first one that accepts a connection wins, like libpulse does. + for (const char* cursor = serverList; *cursor && conn.fd < 0;) { + while (*cursor == ' ' || *cursor == '\t') { + cursor++; + } + + const char* end = cursor; + while (*end && *end != ' ' && *end != '\t') { + end++; } - ffpa_mainloop_iterate(mainloop, 1, nullptr); + if (end == cursor) { + break; + } + + FF_STRBUF_AUTO_DESTROY server = ffStrbufCreateNS((uint32_t) (end - cursor), cursor); + conn.fd = connectServer(server.chars, &defaultSocketPath); + cursor = end; } + } else if (defaultSocketPath.length > 0) { + conn.fd = connectUnixSocket(defaultSocketPath.chars); + } - ffpa_operation_unref(operation); + if (conn.fd < 0) { + FF_DEBUG("Failed to connect to the pulseaudio server on %s", + defaultSocketPath.length > 0 ? defaultSocketPath.chars : "no known socket path"); + ffStrbufDestroy(&conn.frame); + return "Failed to connect to the pulseaudio server"; } -exit: - ffStrbufDestroy(&bundle.serverName); - ffStrbufDestroy(&bundle.defaultDeviceId); - ffpa_context_unref(context); - ffpa_mainloop_free(mainloop); - return error; + FF_STRBUF_AUTO_DESTROY serverName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY defaultSinkName = ffStrbufCreate(); + FF_LIST_AUTO_DESTROY parsed = ffListCreate(); + + const char* error = handshake(&conn, &serverName, &defaultSinkName); + if (!error) { + error = fetchSinkList(&conn, options, &parsed, &serverName, &defaultSinkName); + } + + close(conn.fd); + ffStrbufDestroy(&conn.frame); + + if (error) { + FF_LIST_FOR_EACH (FFSoundDevice, device, parsed) { + ffStrbufDestroy(&device->identifier); + ffStrbufDestroy(&device->name); + ffStrbufDestroy(&device->platformApi); + } + + return error; + } + + FF_LIST_FOR_EACH (FFSoundDevice, device, parsed) { + FFSoundDevice* target = FF_LIST_ADD(FFSoundDevice, *devices); + target->identifier = ffStrbufCreateMove(&device->identifier); + target->name = ffStrbufCreateMove(&device->name); + target->platformApi = ffStrbufCreateMove(&device->platformApi); + target->volume = device->volume; + target->type = device->type; + } + + return nullptr; } -#endif // FF_HAVE_PULSE +// --------------------------------------------------------------------------- -const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { -#ifdef FF_HAVE_PULSE - return detectSound(options, devices); +// Android compiles this file next to sound_android.c, which owns ffDetectSound and calls this one +// only while the display server is not SurfaceFlinger -- a Termux:X11 or Wayland session runs a real +// desktop on top of the device, and PulseAudio is the sound of that desktop. So the entry point has +// to step aside there. Same arrangement as wallpaper_linux.c and terminalfont_linux.c. +const char* +#ifdef __ANDROID__ +ffDetectSoundLinux #else - FF_UNUSED(options, devices); - return "Fastfetch was built without libpulse support"; +ffDetectSound #endif + (FFSoundOptions* options, FFlist* devices) { + const char* error = detectPulseAudio(options, devices); + if (error == nullptr) { + return nullptr; + } + +#ifdef FF_SOUND_ALSA + // No server answered. A machine that runs none still has the hardware, and that is what is + // asked here -- but only ever after the server, never instead of it: on a machine that runs + // one, the server's answer is the desktop's, and the mixer behind it is not the same number. + FF_DEBUG("PulseAudio is not answering (%s), so the ALSA cards are read instead", error); + + const uint32_t detected = devices->length; + const char* alsaError = detectAlsa(options, devices); + if (alsaError == nullptr || devices->length > detected) { + // ALSA answered. Its list may still be empty after the filter, which is the same answer + // the server path gives when nothing matches, and not a failure. + return nullptr; + } + + FF_DEBUG("Reading the ALSA cards failed as well: %s", alsaError); +#endif + + return error; } diff --git a/src/detection/sound/sound_nbsd.c b/src/detection/sound/sound_nbsd.c index f52ba2cfe4..b676898283 100644 --- a/src/detection/sound/sound_nbsd.c +++ b/src/detection/sound/sound_nbsd.c @@ -1,4 +1,5 @@ #include "sound.h" +#include "common/debug.h" #include "common/io.h" #include @@ -13,10 +14,12 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { char audiop[12]; ssize_t plen = readlink("/dev/audio", audiop, ARRAY_SIZE(audiop)); if (plen < (ssize_t) strlen("audioN")) { + FF_DEBUG("readlink(/dev/audio) failed"); return "readlink(/dev/audio) failed"; } defaultDev = audiop[plen - 1] - '0'; if (defaultDev < 0 || defaultDev > 9) { + FF_DEBUG("Invalid audio device"); return "Invalid audio device"; } } diff --git a/src/detection/sound/sound_obsd.c b/src/detection/sound/sound_obsd.c index e2c4238b5d..b0f3092083 100644 --- a/src/detection/sound/sound_obsd.c +++ b/src/detection/sound/sound_obsd.c @@ -1,4 +1,5 @@ #include "sound.h" +#include "common/debug.h" #include "common/strutil.h" #include @@ -56,18 +57,21 @@ static void enumerate_props(FFSoundDeviceBundle* bundle, struct sioctl_desc* des } } -const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { +const char* ffDetectSound([[maybe_unused]] FFSoundOptions* options, FFlist* devices) { [[gnu::cleanup(close_hdl)]] struct sioctl_hdl* hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0); if (!hdl) { + FF_DEBUG("sio_open() failed"); return "sio_open() failed"; } FFSoundDeviceBundle bundle = {}; if (sioctl_ondesc(hdl, (void*) enumerate_props, &bundle) == 0) { + FF_DEBUG("sioctl_ondesc() failed"); return "sioctl_ondesc() failed"; } if (bundle.iLevel != bundle.iMute || bundle.iLevel == 0) { + FF_DEBUG("Unexpected sioctl_ondesc() result"); return "Unexpected sioctl_ondesc() result"; } diff --git a/src/detection/sound/sound_sunos.c b/src/detection/sound/sound_sunos.c index 3f5e2219e9..98492173df 100644 --- a/src/detection/sound/sound_sunos.c +++ b/src/detection/sound/sound_sunos.c @@ -1,4 +1,5 @@ #include "sound.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -17,10 +18,12 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { char mixerp[12]; ssize_t plen = readlink("/dev/audio", mixerp, ARRAY_SIZE(mixerp)); if (plen < 6) { + FF_DEBUG("readlink(/dev/audio) failed"); return "readlink(/dev/audio) failed"; } defaultDev = mixerp[plen - 1] - '0'; if (defaultDev < 0 || defaultDev > 9) { + FF_DEBUG("Invalid mixer device"); return "Invalid mixer device"; } } @@ -46,6 +49,7 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { if (idev == 0) { if (ioctl(fd, SNDCTL_SYSINFO, &info) != 0) { + FF_DEBUG("ioctl(SNDCTL_SYSINFO) failed: %s", strerror(errno)); return "ioctl(SNDCTL_SYSINFO) failed"; } if (ffAppendFDBuffer(fd, &sndstat)) { diff --git a/src/detection/sound/sound_windows.cpp b/src/detection/sound/sound_windows.cpp index 59823a0b8d..1330037c6b 100644 --- a/src/detection/sound/sound_windows.cpp +++ b/src/detection/sound/sound_windows.cpp @@ -1,5 +1,6 @@ extern "C" { #include "sound.h" +#include "common/debug.h" #include "common/windows/com.h" } #include "common/windows/unicode.hpp" @@ -21,17 +22,23 @@ static void ffCoTaskMemFreeWrapper(void* pptr) { static const char* detectSoundDevice(FFlist* devices /* List of FFSoundDevice */, IMMDevice* immDevice, LPWSTR mainDeviceId) { FF_COTASK_AUTO_FREE LPWSTR immDeviceId = nullptr; - if (FAILED(immDevice->GetId(&immDeviceId))) { + HRESULT hr = immDevice->GetId(&immDeviceId); + if (FAILED(hr)) { + FF_DEBUG("immDevice->GetId() failed: %s", ffDebugHResult(hr)); return "immDevice->GetId() failed"; } FF_AUTO_RELEASE_COM_OBJECT IPropertyStore* immPropStore = nullptr; - if (FAILED(immDevice->OpenPropertyStore(STGM_READ, &immPropStore))) { + hr = immDevice->OpenPropertyStore(STGM_READ, &immPropStore); + if (FAILED(hr)) { + FF_DEBUG("immDevice->OpenPropertyStore() failed: %s", ffDebugHResult(hr)); return "immDevice->OpenPropertyStore() failed"; } DWORD immState; - if (FAILED(immDevice->GetState(&immState))) { + hr = immDevice->GetState(&immState); + if (FAILED(hr)) { + FF_DEBUG("immDevice->GetState() failed: %s", ffDebugHResult(hr)); return "immDevice->GetState() failed"; } @@ -76,7 +83,9 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FF FF_AUTO_RELEASE_COM_OBJECT IMMDeviceEnumerator* pEnum = nullptr; - if (FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)))) { + HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)); + if (FAILED(hr)) { + FF_DEBUG("CoCreateInstance(CLSID_MMDeviceEnumerator) failed: %s", ffDebugHResult(hr)); return "CoCreateInstance(CLSID_MMDeviceEnumerator) failed"; } @@ -85,7 +94,9 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FF { FF_AUTO_RELEASE_COM_OBJECT IMMDevice* pDefaultDevice = nullptr; - if (FAILED(pEnum->GetDefaultAudioEndpoint(eRender, eMultimedia, &pDefaultDevice))) { + HRESULT hrDefault = pEnum->GetDefaultAudioEndpoint(eRender, eMultimedia, &pDefaultDevice); + if (FAILED(hrDefault)) { + FF_DEBUG("GetDefaultAudioEndpoint() failed: %s", ffDebugHResult(hrDefault)); return "GetDefaultAudioEndpoint() failed"; } @@ -93,19 +104,25 @@ const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FF return detectSoundDevice(devices, pDefaultDevice, nullptr); } - if (FAILED(pDefaultDevice->GetId(&mainDeviceId))) { + hrDefault = pDefaultDevice->GetId(&mainDeviceId); + if (FAILED(hrDefault)) { + FF_DEBUG("pDefaultDevice->GetId() failed: %s", ffDebugHResult(hrDefault)); return "pDefaultDevice->GetId() failed"; } } FF_AUTO_RELEASE_COM_OBJECT IMMDeviceCollection* pDevices = nullptr; - if (FAILED(pEnum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE | (options->soundType & FF_SOUND_TYPE_ACTIVE ? 0 : DEVICE_STATE_DISABLED), &pDevices))) { + hr = pEnum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE | (options->soundType & FF_SOUND_TYPE_ACTIVE ? 0 : DEVICE_STATE_DISABLED), &pDevices); + if (FAILED(hr)) { + FF_DEBUG("EnumAudioEndpoints() failed: %s", ffDebugHResult(hr)); return "EnumAudioEndpoints() failed"; } uint32_t deviceCount; - if (FAILED(pDevices->GetCount(&deviceCount))) { + hr = pDevices->GetCount(&deviceCount); + if (FAILED(hr)) { + FF_DEBUG("pDevices->GetCount() failed: %s", ffDebugHResult(hr)); return "pDevices->GetCount() failed"; } diff --git a/src/detection/swap/swap_apple.c b/src/detection/swap/swap_apple.c index 3c457c0ec6..b1fd8abd34 100644 --- a/src/detection/swap/swap_apple.c +++ b/src/detection/swap/swap_apple.c @@ -1,12 +1,17 @@ #include "swap.h" +#include "common/debug.h" #include "common/sysctl.h" + +#include +#include #include const char* ffDetectSwap(FFlist* result) { struct xsw_usage xsw; size_t size = sizeof(xsw); if (sysctl((int[]) { CTL_VM, VM_SWAPUSAGE }, 2, &xsw, &size, nullptr, 0) != 0) { + FF_DEBUG("sysctl(vm.swapusage) failed: %s", strerror(errno)); return "Failed to read vm.swapusage"; } diff --git a/src/detection/swap/swap_bsd.c b/src/detection/swap/swap_bsd.c index 8674e76a8c..eab6f5cfbf 100644 --- a/src/detection/swap/swap_bsd.c +++ b/src/detection/swap/swap_bsd.c @@ -1,6 +1,9 @@ #include "swap.h" +#include "common/debug.h" #include "common/sysctl.h" +#include +#include #include #include #include @@ -26,6 +29,7 @@ const char* ffDetectSwap(FFlist* result) { struct xswdev xsws[32]; size_t size = sizeof(xsws); if (sysctlbyname("vm.swap_info_array", xsws, &size, nullptr, 0) < 0) { + FF_DEBUG("sysctlbyname(\"vm.swap_info_array\") failed: %s", strerror(errno)); return "sysctlbyname(\"vm.swap_info_array\") failed"; } @@ -37,6 +41,7 @@ const char* ffDetectSwap(FFlist* result) { } if (xsws->xsw_version != XSWDEV_VERSION) { + FF_DEBUG("xswdev version mismatch"); return "xswdev version mismatch"; } @@ -53,6 +58,7 @@ const char* ffDetectSwap(FFlist* result) { int mib[16]; size_t mibsize = ARRAY_SIZE(mib); if (sysctlnametomib("vm.swap_info", mib, &mibsize) < 0) { + FF_DEBUG("sysctlnametomib(\"vm.swap_info\") failed: %s", strerror(errno)); return "sysctlnametomib(\"vm.swap_info\") failed"; } @@ -66,6 +72,7 @@ const char* ffDetectSwap(FFlist* result) { break; } if (xsw.xsw_version != XSWDEV_VERSION) { + FF_DEBUG("xswdev version mismatch"); return "xswdev version mismatch"; } diff --git a/src/detection/swap/swap_haiku.c b/src/detection/swap/swap_haiku.c index f4b031a957..10f3fccff6 100644 --- a/src/detection/swap/swap_haiku.c +++ b/src/detection/swap/swap_haiku.c @@ -1,11 +1,14 @@ #include "swap.h" +#include "common/debug.h" #include #include const char* ffDetectSwap(FFlist* result) { system_info info; - if (get_system_info(&info) != B_OK) { + status_t status = get_system_info(&info); + if (status != B_OK) { + FF_DEBUG("get_system_info() failed: %d", (int) status); return "Error getting system info"; } diff --git a/src/detection/swap/swap_linux.c b/src/detection/swap/swap_linux.c index 5d456626ca..a494181847 100644 --- a/src/detection/swap/swap_linux.c +++ b/src/detection/swap/swap_linux.c @@ -1,5 +1,6 @@ #include "swap.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" @@ -11,6 +12,7 @@ static const char* detectByProcMeminfo(FFlist* result) { char buf[PROC_FILE_BUFFSIZ]; ssize_t nRead = ffReadFileData("/proc/meminfo", ARRAY_SIZE(buf) - 1, buf); if (nRead < 0) { + FF_DEBUG("ffReadFileData(\"/proc/meminfo\") failed"); return "ffReadFileData(\"/proc/meminfo\", ARRAY_SIZE(buf)-1, buf)"; } buf[nRead] = '\0'; @@ -39,6 +41,7 @@ static const char* detectByProcSwaps(FFlist* result) { char buf[PROC_FILE_BUFFSIZ]; ssize_t nRead = ffReadFileData("/proc/swaps", ARRAY_SIZE(buf) - 1, buf); if (nRead <= 0) { + FF_DEBUG("ffReadFileData(\"/proc/swaps\") failed"); return "ffReadFileData(\"/proc/swaps\", ARRAY_SIZE(buf)-1, buf) failed"; } buf[nRead] = '\0'; @@ -50,6 +53,7 @@ static const char* detectByProcSwaps(FFlist* result) { uint64_t total, used; char name[256]; if (sscanf(line, "%255s %*[^\t]%" SCNu64 "%" SCNu64, name, &total, &used) != 3) { + FF_DEBUG("Invalid /proc/swaps format found"); return "Invalid /proc/swaps format found"; } diff --git a/src/detection/swap/swap_obsd.c b/src/detection/swap/swap_obsd.c index 9c2c98c9e0..75750ab115 100644 --- a/src/detection/swap/swap_obsd.c +++ b/src/detection/swap/swap_obsd.c @@ -1,7 +1,10 @@ #include "swap.h" +#include "common/debug.h" #include "common/FFlist.h" #include "common/mallocHelper.h" +#include +#include #include #include #include @@ -10,6 +13,7 @@ const char* ffDetectSwap(FFlist* result) { int nswap = swapctl(SWAP_NSWAP, 0, 0); if (nswap < 0) { + FF_DEBUG("swapctl(SWAP_NSWAP) failed: %s", strerror(errno)); return "swapctl(SWAP_NSWAP) failed"; } if (nswap == 0) { @@ -19,6 +23,7 @@ const char* ffDetectSwap(FFlist* result) { FF_AUTO_FREE struct swapent* swdev = malloc((uint32_t) nswap * sizeof(*swdev)); if (swapctl(SWAP_STATS, swdev, nswap) < 0) { + FF_DEBUG("swapctl(SWAP_STATS) failed: %s", strerror(errno)); return "swapctl(SWAP_STATS) failed"; } diff --git a/src/detection/swap/swap_sunos.c b/src/detection/swap/swap_sunos.c index 24a2df1393..3c48291ae6 100644 --- a/src/detection/swap/swap_sunos.c +++ b/src/detection/swap/swap_sunos.c @@ -1,4 +1,7 @@ #include "swap.h" +#include "common/debug.h" +#include +#include #include #include #include @@ -17,6 +20,7 @@ const char* ffDetectSwap(FFlist* result) { int size = swapctl(SC_LIST, table); if (size < 0) { + FF_DEBUG("swapctl() failed: %s", strerror(errno)); return "swapctl() failed"; } diff --git a/src/detection/swap/swap_windows.c b/src/detection/swap/swap_windows.c index 8ce6904e2c..5f78eb1505 100644 --- a/src/detection/swap/swap_windows.c +++ b/src/detection/swap/swap_windows.c @@ -1,4 +1,5 @@ #include "swap.h" +#include "common/debug.h" #include "common/windows/unicode.h" #include @@ -10,7 +11,9 @@ const char* ffDetectSwap(FFlist* result) { alignas(SYSTEM_PAGEFILE_INFORMATION) uint8_t buffer[4096]; ULONG size = sizeof(buffer); SYSTEM_PAGEFILE_INFORMATION* pstart = (SYSTEM_PAGEFILE_INFORMATION*) buffer; - if (!NT_SUCCESS(NtQuerySystemInformation(SystemPagefileInformation, pstart, size, &size))) { + NTSTATUS status = NtQuerySystemInformation(SystemPagefileInformation, pstart, size, &size); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtQuerySystemInformation(SystemPagefileInformation, size) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemPagefileInformation, size) failed"; } diff --git a/src/detection/terminalfont/terminalfont_linux.c b/src/detection/terminalfont/terminalfont_linux.c index a351e584e3..b866a301f2 100644 --- a/src/detection/terminalfont/terminalfont_linux.c +++ b/src/detection/terminalfont/terminalfont_linux.c @@ -169,10 +169,10 @@ static void detectDeepinTerminal(FFTerminalFontResult* terminalFont) { FF_STRBUF_AUTO_DESTROY profile = ffStrbufCreateA(64); ffSearchUserConfigFile(&instance.state.platform.configDirs, "deepin/deepin-terminal/config.conf", &profile); - FILE* file = fopen(profile.chars, "r"); + FF_AUTO_CLOSE_FILE FILE* file = fopen(profile.chars, "r"); if (file) { - char* line = nullptr; + FF_AUTO_FREE char* line = nullptr; size_t len = 0; for (int count = 0; getline(&line, &len, file) != -1 && count < 2;) { @@ -188,9 +188,6 @@ static void detectDeepinTerminal(FFTerminalFontResult* terminalFont) { ++count; } } - - free(line); - fclose(file); } if (fontName.length == 0) { @@ -211,27 +208,17 @@ static void detectFootTerminal(FFTerminalFontResult* terminalFont) { return; } - // Sarasa Term SC Nerd:size=8 - uint32_t colon = ffStrbufFirstIndexC(&font, ':'); - if (colon == font.length) { - ffFontInitValues(&terminalFont->font, font.chars, "8"); - return; - } - uint32_t equal = ffStrbufNextIndexS(&font, colon, "size="); - font.chars[colon] = '\0'; - if (equal == font.length) { - ffFontInitValues(&terminalFont->font, font.chars, "8"); - return; - } - uint32_t size = equal + (uint32_t) strlen("size="); - uint32_t comma = ffStrbufNextIndexC(&font, size, ','); - if (comma < font.length) { + // foot splits the font list on commas to specify fallback fonts. + // Each entry is a fontconfig pattern, e.g. "Berkeley Mono-12" or "Sarasa Term SC Nerd:size=8". + uint32_t comma = ffStrbufFirstIndexC(&font, ','); + if (comma != font.length) { + if (comma + 1 < font.length) { + ffFontInitXft(&terminalFont->fallback, &font.chars[comma + 1]); + } font.chars[comma] = '\0'; + ffStrbufRecalculateLength(&font); } - ffFontInitValues(&terminalFont->font, font.chars, &font.chars[size]); - if (comma < font.length) { - ffFontInitValues(&terminalFont->fallback, &font.chars[comma + 1], nullptr); - } + ffFontInitXft(&terminalFont->font, font.chars); } static void detectQTerminal(FFTerminalFontResult* terminalFont) { @@ -312,21 +299,8 @@ static void detectSt(FFTerminalFontResult* terminalFont, const FFTerminalResult* } } - // JetBrainsMono Nerd Font Mono:pixelsize=12:antialias=true:autohint=true - - uint32_t index = ffStrbufFirstIndexC(&font, ':'); - if (index != font.length) { - uint32_t sIndex = ffStrbufNextIndexS(&font, index + 1, "size="); - if (sIndex != font.length) { - sIndex += (uint32_t) strlen("size="); - uint32_t sIndexEnd = ffStrbufNextIndexC(&font, sIndex, ':'); - ffStrbufSetNS(&size, sIndexEnd - sIndex, font.chars + sIndex); - } - ffStrbufSubstrBefore(&font, index); - } else { - ffStrbufClear(&size); - } - ffFontInitValues(&terminalFont->font, font.chars, size.chars); + // e.g. "JetBrainsMono Nerd Font Mono:pixelsize=12:antialias=true:autohint=true" + ffFontInitXft(&terminalFont->font, font.chars); } static void detectWarp(FFTerminalFontResult* terminalFont) { diff --git a/src/detection/terminalfont/terminalfont_windows.c b/src/detection/terminalfont/terminalfont_windows.c index 1bd765d2be..a41d4056f1 100644 --- a/src/detection/terminalfont/terminalfont_windows.c +++ b/src/detection/terminalfont/terminalfont_windows.c @@ -1,15 +1,16 @@ +#include "common/debug.h" #include "common/library.h" #include "common/io.h" #include "common/path.h" #include "common/processing.h" #include "common/properties.h" +#include "common/windows/folders.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" #include "common/strutil.h" #include "detection/terminalshell/terminalshell.h" #include "terminalfont.h" -#include #include #include @@ -116,6 +117,7 @@ static inline void wrapYyjsonFree(yyjson_doc** doc) { static const char* detectFromWTSettings(FFstrbuf* content, const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(content->chars, content->length, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, nullptr, nullptr); if (!doc) { + FF_DEBUG("Failed to parse WT JSON config file"); return "Failed to parse WT JSON config file"; } @@ -124,6 +126,7 @@ static const char* detectFromWTSettings(FFstrbuf* content, const FFstrbuf* wtPro yyjson_val* profiles = yyjson_obj_get(root, "profiles"); if (!profiles) { + FF_DEBUG("yyjson_obj_get(root, \"profiles\") failed"); return "yyjson_obj_get(root, \"profiles\") failed"; } @@ -225,13 +228,11 @@ static void detectWTProfileFromFragmentsIn(const FFstrbuf* fragmentDir, const FF static void detectFromWTFragments(const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { // Windows Terminal merges the user scoped fragments before the machine scoped ones, // so the user scoped fragments are more important - static const KNOWNFOLDERID* const fragmentFolderIds[] = { &FOLDERID_LocalAppData, &FOLDERID_ProgramData }; + static const FFKnownFolder fragmentFolders[] = { FF_KNOWN_FOLDER_LOCAL_APP_DATA, FF_KNOWN_FOLDER_PROGRAM_DATA }; - for (uint32_t i = 0; i < ARRAY_SIZE(fragmentFolderIds); ++i) { - PWSTR folderW = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(fragmentFolderIds[i], KF_FLAG_DEFAULT, nullptr, &folderW))) { - FF_STRBUF_AUTO_DESTROY fragmentDir = ffStrbufCreateWS(folderW); - CoTaskMemFree(folderW); + for (uint32_t i = 0; i < ARRAY_SIZE(fragmentFolders); ++i) { + FF_STRBUF_AUTO_DESTROY fragmentDir = ffStrbufCreate(); + if (ffGetKnownFolderPath(fragmentFolders[i], &fragmentDir)) { ffStrbufAppendS(&fragmentDir, "\\Microsoft\\Windows Terminal\\Fragments\\"); if (ffPathExists(fragmentDir.chars, FF_PATHTYPE_DIRECTORY)) { @@ -258,11 +259,7 @@ static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFon error = "Error reading Windows Terminal portable settings JSON file"; } } else { - PWSTR localAppDataW = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &localAppDataW))) { - ffStrbufSetWS(&jsonPath, localAppDataW); - CoTaskMemFree(localAppDataW); - + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &jsonPath)) { if (ffStrbufContainIgnCaseS(terminalExe, "_8wekyb3d8bbwe\\")) { // Microsoft Store version if (ffStrbufContainIgnCaseS(terminalExe, ".WindowsTerminalPreview_")) { diff --git a/src/detection/terminalshell/terminalshell_linux.c b/src/detection/terminalshell/terminalshell_linux.c index 76e28db8c8..e5ff41442a 100644 --- a/src/detection/terminalshell/terminalshell_linux.c +++ b/src/detection/terminalshell/terminalshell_linux.c @@ -1,11 +1,16 @@ #include "terminalshell.h" #include "common/FFcache.h" +#include "common/debug.h" #include "common/io.h" #include "common/parsing.h" #include "common/processing.h" #include "common/thread.h" #include "common/strutil.h" +#ifdef __ANDROID__ + #include "common/android/applabel.h" +#endif + #include #include #include @@ -324,8 +329,18 @@ static void setTerminalInfoDetails(FFTerminalResult* result) { #if defined(__ANDROID__) - else if (ffStrbufEqualS(&result->processName, "com.termux")) + else if (ffStrbufEqualS(&result->processName, "com.termux")) { + // The app is installed under a label rather than under its package name, and the label is + // what every screen the user sees calls it -- "Termux:Launcher" on the test device, not + // "Termux". The package name is the fallback for a build whose label cannot be read, which + // includes an app whose label is a literal string in its manifest rather than a resource + // reference; see common/android/applabel.h for what that read costs and when it fails. ffStrbufInitStatic(&result->prettyName, "Termux"); + const char* labelError = ffAndroidGetAppLabel("com.termux", &result->prettyName); + if (labelError != nullptr) { + FF_DEBUG("The label of com.termux could not be resolved: %s", labelError); + } + } #elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c index 7dc9caf68b..b241721ff7 100644 --- a/src/detection/terminalshell/terminalshell_windows.c +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -4,6 +4,7 @@ #include "common/processing.h" #include "common/thread.h" #include "common/mallocHelper.h" +#include "common/windows/folders.h" #include "common/windows/registry.h" #include "common/windows/unicode.h" #include "common/windows/version.h" @@ -16,7 +17,6 @@ #include #include #include -#include static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) { uint32_t ppid = 0; @@ -182,12 +182,7 @@ static bool detectDefaultTerminal(FFTerminalResult* result) { ffStrbufSetS(&result->processName, "WindowsTerminal.exe"); ffStrbufSetS(&result->prettyName, "WindowsTerminal"); - PWSTR programFiles = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &programFiles))) { - ffStrbufSetWS(&result->exe, programFiles); - CoTaskMemFree(programFiles); - programFiles = nullptr; - + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &result->exe)) { ffStrbufAppendS(&result->exe, "\\WindowsApps\\"); ffStrbufAppend(&result->exe, &path); ffStrbufAppendS(&result->exe, "\\WindowsTerminal.exe"); diff --git a/src/detection/theme/theme_linux.c b/src/detection/theme/theme_linux.c index 6f2bc21173..b3684c8b8e 100644 --- a/src/detection/theme/theme_linux.c +++ b/src/detection/theme/theme_linux.c @@ -1,4 +1,5 @@ #include "theme.h" +#include "common/debug.h" #include "common/parsing.h" #include "detection/gtk_qt/gtk_qt.h" #include "detection/displayserver/displayserver.h" @@ -16,6 +17,7 @@ const char* ffDetectTheme(FFThemeResult* result) { const FFstrbuf* gtk4 = &ffDetectGTK4()->theme; if (plasma->widgetStyle.length == 0 && plasma->colorScheme.length == 0 && gtk2->length == 0 && gtk3->length == 0 && gtk4->length == 0) { + FF_DEBUG("No themes found"); return "No themes found"; } diff --git a/src/detection/top/top_apple.c b/src/detection/top/top_apple.c index 5d5c9208e8..3e5cc5ba1d 100644 --- a/src/detection/top/top_apple.c +++ b/src/detection/top/top_apple.c @@ -1,13 +1,16 @@ #include "top.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include +#include #include #include const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int npids = proc_listallpids(nullptr, 0); if (npids <= 0) { + FF_DEBUG("proc_listallpids(nullptr, 0) failed: %s", strerror(errno)); return "proc_listallpids(nullptr, 0) failed"; } // `proc_listallpids` returns the number of pids, but it wants the size of the buffer in bytes: @@ -15,10 +18,12 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { const int pidCapacity = npids + npids / 8 + 1; FF_AUTO_FREE pid_t* pids = malloc((size_t) pidCapacity * sizeof(pid_t)); if (pids == nullptr) { + FF_DEBUG("malloc(%zu) for %d pids failed", (size_t) pidCapacity * sizeof(pid_t), pidCapacity); return "malloc() failed"; } npids = proc_listallpids(pids, pidCapacity * (int) sizeof(pid_t)); if (npids <= 0) { + FF_DEBUG("proc_listallpids(pids, %d) failed: %s", pidCapacity * (int) sizeof(pid_t), strerror(errno)); return "proc_listallpids(pids, bufferSize) failed"; } diff --git a/src/detection/top/top_bsd.c b/src/detection/top/top_bsd.c index 959b7f3d79..60684cb1df 100644 --- a/src/detection/top/top_bsd.c +++ b/src/detection/top/top_bsd.c @@ -1,6 +1,10 @@ #include "top.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include + #include #include #include @@ -11,6 +15,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, [[maybe_unused]] FFTopTyp size_t length; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; } @@ -18,6 +23,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, [[maybe_unused]] FFTopTyp length += length / 8 + sizeof(struct kinfo_proc); FF_AUTO_FREE struct kinfo_proc* processes = malloc(length); if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}, processes) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; } uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc)); diff --git a/src/detection/top/top_nbsd.c b/src/detection/top/top_nbsd.c index c5d79d65c5..0a64903cfe 100644 --- a/src/detection/top/top_nbsd.c +++ b/src/detection/top/top_nbsd.c @@ -1,16 +1,19 @@ #include "top.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include #include #include #include +#include const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_ALL, -1, sizeof(struct kinfo_proc2), INT_MAX }; size_t length; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, nullptr}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, nullptr}) failed"; } @@ -19,6 +22,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { FF_AUTO_FREE struct kinfo_proc2* processes = malloc(length); if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, processes}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL, processes}) failed"; } uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc2)); diff --git a/src/detection/top/top_obsd.c b/src/detection/top/top_obsd.c index 30861f873d..b178f927cd 100644 --- a/src/detection/top/top_obsd.c +++ b/src/detection/top/top_obsd.c @@ -1,7 +1,10 @@ #include "top.h" +#include "common/debug.h" #include "common/mallocHelper.h" +#include +#include #include #include // DEV_BSIZE #include @@ -11,12 +14,14 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { size_t length = 0; if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed"; } FF_AUTO_FREE struct kinfo_proc* processes = (struct kinfo_proc*) malloc(length); request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, processes) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, processes) failed"; } diff --git a/src/detection/top/top_sunos.c b/src/detection/top/top_sunos.c index 0054ae79a8..7722d9a90a 100644 --- a/src/detection/top/top_sunos.c +++ b/src/detection/top/top_sunos.c @@ -1,15 +1,19 @@ #include "top.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" +#include #include +#include #include #include // DEV_BSIZE const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/proc"); if (!dirp) { + FF_DEBUG("opendir(\"/proc\") failed: %s", strerror(errno)); return "opendir(\"/proc\") failed"; } diff --git a/src/detection/top/top_windows.c b/src/detection/top/top_windows.c index 33a430af73..33d1d6dd79 100644 --- a/src/detection/top/top_windows.c +++ b/src/detection/top/top_windows.c @@ -1,5 +1,6 @@ #include "top.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include "common/windows/unicode.h" @@ -24,6 +25,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; } else { + FF_DEBUG("NtQuerySystemInformation(SystemProcessInformation) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessInformation) failed"; } } diff --git a/src/detection/tpm/tpm_apple.c b/src/detection/tpm/tpm_apple.c index b97ecf05bd..774e13b0d8 100644 --- a/src/detection/tpm/tpm_apple.c +++ b/src/detection/tpm/tpm_apple.c @@ -1,4 +1,5 @@ #include "tpm.h" +#include "common/debug.h" #ifndef __aarch64__ #include "common/apple/cf_helpers.h" @@ -26,5 +27,6 @@ const char* ffDetectTPM(FFTPMResult* result) { #endif + FF_DEBUG("Neither Apple T2 nor Apple Silicon Security was reported by IOKit"); return "No Apple Security hardware detected"; } diff --git a/src/detection/tpm/tpm_bsd.c b/src/detection/tpm/tpm_bsd.c index 1df25fb477..380cb3917e 100644 --- a/src/detection/tpm/tpm_bsd.c +++ b/src/detection/tpm/tpm_bsd.c @@ -1,12 +1,15 @@ #include "tpm.h" +#include "common/debug.h" #include "common/sysctl.h" #include "common/kmod.h" const char* ffDetectTPM(FFTPMResult* result) { if (ffSysctlGetString("dev.tpmcrb.0.%desc", &result->description) != nullptr) { if (!ffKmodLoaded("tpm")) { + FF_DEBUG("`tpm` kernel module is not loaded"); return "`tpm` kernel module is not loaded"; } + FF_DEBUG("TPM device is not found"); return "TPM device is not found"; } diff --git a/src/detection/tpm/tpm_linux.c b/src/detection/tpm/tpm_linux.c index 7a54f27f81..e34a3d5ba4 100644 --- a/src/detection/tpm/tpm_linux.c +++ b/src/detection/tpm/tpm_linux.c @@ -1,11 +1,14 @@ #include "tpm.h" +#include "common/debug.h" #include "common/io.h" const char* ffDetectTPM(FFTPMResult* result) { if (!ffPathExists("/sys/class/tpm/tpm0/", FF_PATHTYPE_DIRECTORY)) { if (!ffPathExists("/sys/class/tpm/", FF_PATHTYPE_DIRECTORY)) { + FF_DEBUG("/sys/class/tpm/ does not exist, the kernel has no TPM support"); return "TPM is not supported by kernel"; } + FF_DEBUG("/sys/class/tpm/ exists but /sys/class/tpm/tpm0/ does not"); return "TPM device is not found"; } diff --git a/src/detection/uptime/uptime_bsd.c b/src/detection/uptime/uptime_bsd.c index 00d052b370..80892a3839 100644 --- a/src/detection/uptime/uptime_bsd.c +++ b/src/detection/uptime/uptime_bsd.c @@ -1,6 +1,9 @@ #include "uptime.h" +#include "common/debug.h" #include "common/time.h" +#include +#include #include #include @@ -13,6 +16,7 @@ const char* ffDetectUptime(FFUptimeResult* result) { size_t bootTimeSize = sizeof(bootTime); if (sysctl( (int[]) { CTL_KERN, KERN_BOOTTIME }, 2, &bootTime, &bootTimeSize, nullptr, 0) != 0) { + FF_DEBUG("sysctl({CTL_KERN, KERN_BOOTTIME}) failed: %s", strerror(errno)); return "sysctl({CTL_KERN, KERN_BOOTTIME}) failed"; } diff --git a/src/detection/uptime/uptime_linux.c b/src/detection/uptime/uptime_linux.c index 9ac709416f..1f9888417d 100644 --- a/src/detection/uptime/uptime_linux.c +++ b/src/detection/uptime/uptime_linux.c @@ -1,8 +1,11 @@ #include "uptime.h" +#include "common/debug.h" #include "common/time.h" #include "common/io.h" +#include #include +#include const char* ffDetectUptime(FFUptimeResult* result) { #ifndef __ANDROID__ // cat: /proc/uptime: Permission denied @@ -26,6 +29,7 @@ const char* ffDetectUptime(FFUptimeResult* result) { #ifndef __GNU__ struct timespec uptime; if (clock_gettime(CLOCK_BOOTTIME, &uptime) != 0) { + FF_DEBUG("clock_gettime(CLOCK_BOOTTIME) failed: %s", strerror(errno)); return "clock_gettime(CLOCK_BOOTTIME) failed"; } @@ -33,6 +37,7 @@ const char* ffDetectUptime(FFUptimeResult* result) { result->bootTime = ffTimeGetNow() - result->uptime; return nullptr; #else + FF_DEBUG("read(/proc/uptime) failed"); return "read(/proc/uptime) failed"; #endif } diff --git a/src/detection/users/users_linux.c b/src/detection/users/users_linux.c index a5f31d2a0b..e304b29cb0 100644 --- a/src/detection/users/users_linux.c +++ b/src/detection/users/users_linux.c @@ -1,8 +1,10 @@ +#include "common/debug.h" #include "common/io.h" #include "common/properties.h" #include "fastfetch.h" #include "users.h" +#include #include #if FF_HAVE_UTMPX @@ -102,6 +104,7 @@ const char* detectBySystemd(FFUsersOptions* options, FFlist* users) { const uint32_t pathUsersBaseLen = pathUsers.length; FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pathUsers.chars); if (!dirp) { + FF_DEBUG("opendir(\"%s\") failed: %s", pathUsers.chars, strerror(errno)); return "opendir(\"/run/systemd/users/\") failed"; } diff --git a/src/detection/users/users_obsd.c b/src/detection/users/users_obsd.c index a9b03072c8..d74a71bb5b 100644 --- a/src/detection/users/users_obsd.c +++ b/src/detection/users/users_obsd.c @@ -1,12 +1,17 @@ #include "fastfetch.h" #include "users.h" +#include "common/debug.h" #include "common/io.h" +#include +#include + #include const char* ffDetectUsers([[maybe_unused]] FFUsersOptions* options, FFlist* users) { FF_AUTO_CLOSE_FILE FILE* fp = fopen(_PATH_UTMP, "r"); if (!fp) { + FF_DEBUG("fopen(\"%s\", \"r\") failed: %s", _PATH_UTMP, strerror(errno)); return "fopen(_PATH_UTMP, r) failed"; } diff --git a/src/detection/users/users_windows.c b/src/detection/users/users_windows.c index 837159a740..df64848070 100644 --- a/src/detection/users/users_windows.c +++ b/src/detection/users/users_windows.c @@ -1,4 +1,5 @@ #include "users.h" +#include "common/debug.h" #include "common/windows/unicode.h" #include "common/time.h" @@ -12,6 +13,7 @@ const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) { DWORD level = 1; if (!WTSEnumerateSessionsExW(WTS_CURRENT_SERVER_HANDLE, &level, 0, &sessionInfo, &sessionCount)) { + FF_DEBUG("WTSEnumerateSessionsExW() failed: %s", ffDebugWin32Error(GetLastError())); return "WTSEnumerateSessionsW(WTS_CURRENT_SERVER_HANDLE) failed"; } diff --git a/src/detection/vulkan/vulkan.c b/src/detection/vulkan/vulkan.c index 9d21948316..b129e6605c 100644 --- a/src/detection/vulkan/vulkan.c +++ b/src/detection/vulkan/vulkan.c @@ -19,6 +19,8 @@ static inline void applyVulkanVersion(uint32_t vulkanVersion, FFVersion* ffVersi } static void applyDriverName(VkPhysicalDeviceDriverPropertiesKHR* properties, FFstrbuf* result) { + ffStrbufClear(result); + if (!ffStrSet(properties->driverName)) { return; } diff --git a/src/detection/wallpaper/wallpaper_android.c b/src/detection/wallpaper/wallpaper_android.c index 33735a6431..ee16dc0031 100644 --- a/src/detection/wallpaper/wallpaper_android.c +++ b/src/detection/wallpaper/wallpaper_android.c @@ -1,10 +1,13 @@ #include "wallpaper.h" #include "common/android/binder.h" +#include "common/android/dex.h" #include "common/android/package.h" #include "common/debug.h" #include "common/io.h" #include "detection/displayserver/displayserver.h" +#include +#include #include // Android keeps the wallpaper as a file per user, in a directory no app can enter: @@ -44,10 +47,20 @@ typedef enum FFWallpaperAndroidWhich : int32_t { FF_WALLPAPER_ANDROID_WHICH_LOCK = 2, } FFWallpaperAndroidWhich; -// `getWallpaper` is the fourth method of IWallpaperManager$Stub, and the number is read from the -// device's own framework.jar rather than transcribed from AOSP, because a vendor ROM adds and drops -// methods as it likes -- this one dropped `getName`, whose code answers UNKNOWN_TRANSACTION here. -#define FF_WALLPAPER_ANDROID_TRANSACTION_GET_WALLPAPER 4u +// The jar and the stub class the transaction code is read out of. framework.jar answered on every +// device measured (Android 11, 13 and 16), so that is the one path and there is no fallback; the +// stub sits in classes.dex there, which is the entry the lookup tries first. +#define FF_WALLPAPER_ANDROID_JAR "/system/framework/framework.jar" +#define FF_WALLPAPER_ANDROID_STUB "Landroid/app/IWallpaperManager$Stub;" + +// The method this module calls, by the name of the constant the dex carries for it. `getWallpaper` +// is the fourth method of IWallpaperManager$Stub, but the number is resolved from the device's own +// jar rather than written down here: the numbering is a property of the `.aidl` the ROM was built +// from, and a vendor that inserts a method ahead of this one shifts it, after which a transcribed +// code reaches a different method or none. Spelling the name out is the whole point -- the lookup +// turns it into whatever this build uses, and a build that does not declare it says so instead of +// reaching another method. See common/android/dex.h. +#define FF_WALLPAPER_ANDROID_GET_WALLPAPER "TRANSACTION_getWallpaper" // `ffAndroidGetOwnPackage` writes at most this many bytes, terminator included. #define FF_WALLPAPER_ANDROID_PACKAGE_SIZE 128 @@ -67,7 +80,7 @@ typedef enum FFWallpaperAndroidWhich : int32_t { // being wrong here would fail the detection, not misread it. #define FF_WALLPAPER_ANDROID_REPLY_SIZE 512 -static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, FFstrbuf* result) { +static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, int32_t transaction, FFstrbuf* result) { uint8_t parcelBuffer[FF_WALLPAPER_ANDROID_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); ffBinderParcelPutInterfaceToken(&parcel, FF_WALLPAPER_ANDROID_DESCRIPTOR); @@ -81,7 +94,7 @@ static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const cha // TF_ACCEPT_FDS is neither optional nor a hint: a reply carrying a descriptor is only delivered // when the request asked for one, and a request that did not is answered with BR_FAILED_REPLY -- // which reads like a service that is not running rather than like a missing flag. - const char* error = ffBinderTransact(binder, handle, FF_WALLPAPER_ANDROID_TRANSACTION_GET_WALLPAPER, TF_ACCEPT_FDS, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, TF_ACCEPT_FDS, &parcel, &reply); // Whatever else happened, a descriptor that did arrive is ours: the kernel installed it in our fd // table, and on the paths below that return early nothing else would close it. @@ -113,13 +126,14 @@ static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const cha char path[64]; const int pathLength = snprintf(path, sizeof(path), "/proc/self/fd/%d", (int) fd); if (pathLength <= 0 || (size_t) pathLength >= sizeof(path)) { + FF_DEBUG("Failed to build the /proc path of the wallpaper descriptor"); return "Failed to build the /proc path of the wallpaper descriptor"; } char target[512]; const ssize_t length = readlink(path, target, sizeof(target) - 1); if (length <= 0) { - FF_DEBUG("readlink(%s) failed", path); + FF_DEBUG("readlink(%s) failed: %s", path, strerror(errno)); return "Failed to resolve the wallpaper path"; } target[length] = '\0'; @@ -144,11 +158,28 @@ const char* ffDetectWallpaper(FFstrbuf* result) { char package[FF_WALLPAPER_ANDROID_PACKAGE_SIZE]; if (!ffAndroidGetOwnPackage(package, sizeof(package))) { + FF_DEBUG("Cannot determine the package name of this process"); return "Cannot determine the package name of this process"; } + // 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. 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. + int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED; + const FFDexStaticIntRequest request = { FF_WALLPAPER_ANDROID_STUB, FF_WALLPAPER_ANDROID_GET_WALLPAPER, &transaction }; + const char* error = ffDexStaticInts(FF_WALLPAPER_ANDROID_JAR, &request, 1); + if (error != nullptr) { + FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_WALLPAPER_ANDROID_GET_WALLPAPER, FF_WALLPAPER_ANDROID_JAR, error); + return error; + } + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("The wallpaper service does not declare getWallpaper"); + return "The wallpaper service does not declare getWallpaper"; + } + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); + error = ffBinderOpen(&binder); if (error != nullptr) { return error; } @@ -160,16 +191,16 @@ const char* ffDetectWallpaper(FFstrbuf* result) { FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_WALLPAPER_ANDROID_SERVICE, error); return error; } - FF_DEBUG("The \"%s\" service is handle %u", FF_WALLPAPER_ANDROID_SERVICE, service.handle); + FF_DEBUG("The \"%s\" service is handle %u, getWallpaper is transaction %d", FF_WALLPAPER_ANDROID_SERVICE, service.handle, transaction); // The system wallpaper is what the module means by "the current wallpaper"; the lock screen is a // second, separate picture, and is only reported when the system one is not a file -- which is // the live-wallpaper case, where a component draws it and there is no image behind it. - error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, result); + error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, transaction, result); if (error == nullptr) { return nullptr; } FF_DEBUG("The system wallpaper is not a file (%s), so the lock screen is asked for instead", error); - return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, result); + return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, transaction, result); } diff --git a/src/detection/wallpaper/wallpaper_haiku.cpp b/src/detection/wallpaper/wallpaper_haiku.cpp index ee17f16a0c..4b17d5c0ea 100644 --- a/src/detection/wallpaper/wallpaper_haiku.cpp +++ b/src/detection/wallpaper/wallpaper_haiku.cpp @@ -1,5 +1,6 @@ extern "C" { #include "wallpaper.h" +#include "common/debug.h" #include "common/mallocHelper.h" } @@ -18,6 +19,7 @@ const char* ffDetectWallpaper(FFstrbuf* result) { BString path; if (find_directory(B_DESKTOP_DIRECTORY, &pDesktop) < B_OK) { + FF_DEBUG("find_directory(B_DESKTOP_DIRECTORY) failed"); return "find_directory(B_DESKTOP_DIRECTORY) failed"; } @@ -46,6 +48,7 @@ const char* ffDetectWallpaper(FFstrbuf* result) { } if (path.Length() < 1) { + FF_DEBUG("Failed to detect the current wallpaper path"); return "Failed to detect the current wallpaper path"; } diff --git a/src/detection/wallpaper/wallpaper_linux.c b/src/detection/wallpaper/wallpaper_linux.c index b63f93ab1c..85bf3287f6 100644 --- a/src/detection/wallpaper/wallpaper_linux.c +++ b/src/detection/wallpaper/wallpaper_linux.c @@ -1,4 +1,5 @@ #include "wallpaper.h" +#include "common/debug.h" #include "common/io.h" #include "common/settings.h" #include "detection/displayserver/displayserver.h" @@ -39,16 +40,19 @@ static const char* detectCosmicComp(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate(); if (!ffReadFileBuffer(path.chars, &output)) { + FF_DEBUG("Failed to read \"%s\"", path.chars); return "Failed to read COSMIC wallpaper config"; } const char* sourceStart = strstr(output.chars, "source:"); if (sourceStart == nullptr) { + FF_DEBUG("The COSMIC wallpaper config does not contain \"source:\""); return "COSMIC wallpaper config doesn't contain source"; } const char* pathStart = strstr(sourceStart, "Path("); if (pathStart == nullptr) { + FF_DEBUG("The COSMIC wallpaper source does not contain \"Path(\""); return "COSMIC wallpaper source is not a Path value"; } @@ -58,6 +62,7 @@ static const char* detectCosmicComp(FFstrbuf* result) { } if (*pathStart != '\'' && *pathStart != '"') { + FF_DEBUG("The COSMIC wallpaper Path does not start with a quote"); return "COSMIC wallpaper Path format is invalid"; } @@ -65,6 +70,7 @@ static const char* detectCosmicComp(FFstrbuf* result) { ++pathStart; const char* pathEnd = strchr(pathStart, quote); if (pathEnd == nullptr || pathEnd == pathStart) { + FF_DEBUG("The COSMIC wallpaper path is empty"); return "COSMIC wallpaper path is empty"; } @@ -99,6 +105,7 @@ ffDetectWallpaper } if (!wallpaper) { + FF_DEBUG("Neither GTK nor Qt reported a wallpaper path"); return "Failed to detect the current wallpaper path"; } diff --git a/src/detection/wallpaper/wallpaper_windows.c b/src/detection/wallpaper/wallpaper_windows.c index 7357d39fd1..c5e608146c 100644 --- a/src/detection/wallpaper/wallpaper_windows.c +++ b/src/detection/wallpaper/wallpaper_windows.c @@ -1,13 +1,16 @@ #include "wallpaper.h" +#include "common/debug.h" #include "common/windows/registry.h" const char* ffDetectWallpaper(FFstrbuf* result) { FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Control Panel\\Desktop", &hKey, nullptr)) { + FF_DEBUG("ffRegOpenKeyForRead(Control Panel\\Desktop) failed"); return "ffRegOpenKeyForRead(Control Panel\\Desktop) failed"; } if (!ffRegReadStrbuf(hKey, L"WallPaper", result, nullptr)) { + FF_DEBUG("ffRegReadStrbuf(WallPaper) failed"); return "ffRegReadStrbuf(WallPaper) failed"; } diff --git a/src/detection/wifi/wifi_android.c b/src/detection/wifi/wifi_android.c index 0da49ad94e..01c2dbee2f 100644 --- a/src/detection/wifi/wifi_android.c +++ b/src/detection/wifi/wifi_android.c @@ -109,17 +109,18 @@ // more accurate -- pays nothing for it. // // Three things about the call are positional rather than negotiated, and all three were measured -// on an Android 16 device and an Android 11 device: +// across the Android 11, 13, 16 and 17 devices: // // * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on -// Android 11 and 41 on Android 16) -- see common/android/dex.h. +// Android 11, 36 on Android 13 and 41 on Android 16 and 17) -- see common/android/dex.h. // * The reply layout drifts, and not only by release. Android 12 dropped the duplicate length word -// that Android 11 and 10 wrote in front of the SSID octets, and the Android 16 device fills an -// int between the transmit and the receive speed that the Android 11 device leaves out -- while -// AOSP's own Android 16 writes the head exactly the Android 11 way, so that one int is a vendor -// addition rather than a release difference. Frequency is the only channel centre in the head, so -// it is located by value, with the whole result validated before it is used. A wrong guess shows -// up as a rejected layout, not as a plausible wrong number. +// that Android 11 and 10 wrote in front of the SSID octets, and the Android 17 device writes a +// word of its own between the link speed and the transmit speed that the Android 11 and 13 +// devices leave out -- a vendor addition rather than a release difference, since AOSP's own +// Android 16 writes the head the Android 11 way. Frequency is the only channel centre in the +// head, so it is located by value, with the whole result validated before it is used, and the +// two direction specific speeds are read as its neighbours. A wrong guess shows up as a +// rejected layout, not as a plausible wrong number. // * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every // vendor: the Android 11 device carries three words there that AOSP's own Android 11 // does not. It is located by shape for that reason. @@ -171,13 +172,14 @@ typedef enum FFWifiAndroidRadioState : int32_t } FFWifiAndroidRadioState; // An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker; -// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed have held those -// offsets from Android 9 to 16, and the transmit speed joined them in Android 10 without moving -// since, so all four are read at fixed offsets. Everything after them is located by shape. +// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed are the first three +// fields `writeToParcel` writes, so nothing a release or a vendor adds behind them can move them -- +// they have held those offsets from Android 9 to 17 and are read there. The two direction specific +// speeds sit behind them but are located from the frequency instead, because a vendor does write a +// word of its own in front of that pair: see the note at FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED. #define FF_WIFI_ANDROID_OFF_NET_ID 8 #define FF_WIFI_ANDROID_OFF_RSSI 12 #define FF_WIFI_ANDROID_OFF_LINK_SPEED 16 -#define FF_WIFI_ANDROID_OFF_TX_LINK_SPEED 20 // A `WifiInfo` with no connection to describe carries the sentinels for the two fields that are // always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot @@ -206,11 +208,15 @@ typedef enum FFWifiAndroidRadioState : int32_t #define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08 #define FF_WIFI_ANDROID_SSID_LENGTH 0x04 -// The receive speed is the word in front of Frequency; the transmit speed is the word after the link -// speed. The Android 16 device writes one extra word between those two that AOSP does not -- it repeats the -// transmit speed -- so the receive speed can only be reached from Frequency, and Frequency cannot be -// reached by stepping over the two link speeds. Both were checked against `cmd wifi status` at the -// same moment: 131/219 on the Android 16 device, 86/-1 on the Android 11 device. +// The transmit speed and the receive speed are the two words in front of Frequency, in that order: +// `mTxLinkSpeed, mRxLinkSpeed, mFrequency` are written back to back by every head this file has +// been checked against, from Android 11 through 17. The pair is read from Frequency rather than +// stepped to from the link speed, because the Android 17 device writes a word of its own between +// the link speed and the transmit speed: its dex names that word `mCurrentLinkSpeed`, so the word +// after the link speed is not the transmit speed there and the pair sits one word further along. +// Both were checked against `cmd wifi status` at the same moment: 131/219 on the Android 16 device, +// 86/-1 on the Android 11 device. +#define FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED 0x08 #define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04 #define FF_WIFI_ANDROID_SSID 0x08 // Android 12 and later #define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older @@ -643,7 +649,7 @@ static void debugDumpReply(const uint8_t* data, size_t size) { #endif static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) { - if (size < FF_WIFI_ANDROID_OFF_TX_LINK_SPEED + 4) { + if (size < FF_WIFI_ANDROID_FREQ_SCAN_FROM + 4) { FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size); return "The reply is too short to hold a WifiInfo"; } @@ -666,12 +672,11 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size); return nullptr; } - FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d, tx link speed %d", + FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d", size, ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID), ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI), - ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED), - ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED)); + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED)); // The two orders WifiSsid has written its length in: once before the octets on Android 11 and // older, not at all on Android 12 and later, which leaves the octets one word earlier. The one @@ -755,7 +760,7 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA connection->frequency = frequency; connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI); connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED); - connection->txLinkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED); + connection->txLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED); connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED); findStrings(data, size, stringsFrom, connection); @@ -777,9 +782,9 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA if (!findSecurityType(data, size, connection)) { FF_DEBUG("No security type could be located in this reply, so the field stays empty"); } - FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s", - connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed, - connection->connected ? "yes" : "no"); + FF_DEBUG("Standard %d (%s), tx link speed %d, rx link speed %d, connected %s", + connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->txLinkSpeed, + connection->rxLinkSpeed, connection->connected ? "yes" : "no"); return nullptr; } @@ -827,15 +832,46 @@ static const char* wifiName(const char* value, bool withheld) { return withheld ? FF_WIFI_ANDROID_REDACTED : ""; } -// Calls a method of the service that takes no arguments and answers with an int. The transaction -// code comes out of the jar the same way `getConnectionInfo`'s does, so a release that does not -// declare the method answers UNKNOWN_TRANSACTION instead of a number that means something else. -static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* transactionField, int32_t* result) { - int32_t transaction = 0; - const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, transactionField, &transaction); +// Every transaction code this module sends, resolved from the device's own jar in one walk. They are +// asked for together because the walk is dominated by the dex's type table and both codes sit in the +// same class of the same entry -- see common/android/dex.h. +typedef struct FFWifiAndroidCodes { + int32_t getConnectionInfo; + int32_t getWifiEnabledState; +} FFWifiAndroidCodes; + +// Resolves both codes and reports the one outcome the module cannot work around: a jar that does not +// declare `getConnectionInfo` has no connection to report. The radio state is a refinement -- the +// module already carries on when the service will not answer it -- so it is allowed to come back as +// the sentinel, and what that costs is decided where it is used rather than here. +static const char* resolveCodes(FFWifiAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &codes->getConnectionInfo }, + { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &codes->getWifiEnabledState }, + }; + + const char* error = ffDexStaticInts(FF_WIFI_ANDROID_JAR, requests, ARRAY_SIZE(requests)); if (error != nullptr) { + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error); return error; } + if (codes->getConnectionInfo == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("The Wifi service does not declare getConnectionInfo"); + return "The Wifi service does not declare getConnectionInfo"; + } + return nullptr; +} + +// Calls a method of the service that takes no arguments and answers with an int. `transaction` comes +// out of the jar, and the sentinel the reader leaves for a code it could not find is caught here: a +// release that does not declare the method is a message rather than a number that means something +// else. `transactionField` is carried only to name the method in the debug output, which a release +// build compiles out -- hence the attribute. +static const char* callIntMethod(FFBinder* binder, uint32_t handle, [[maybe_unused]] const char* transactionField, int32_t transaction, int32_t* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("The Wifi service does not declare that method"); + return "The Wifi service does not declare that method"; + } uint8_t parcelBuffer[256]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -843,7 +879,7 @@ static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* uint8_t replyBuffer[64]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -877,13 +913,15 @@ static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* static const char* detectWithBinder(FFlist* result) { char package[FF_WIFI_ANDROID_PACKAGE_SIZE]; if (!ffAndroidGetOwnPackage(package, sizeof(package))) { + FF_DEBUG("Cannot determine the package name of this process"); return "Cannot determine the package name of this process"; } - int32_t transaction = 0; - const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &transaction); + // 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. + FFWifiAndroidCodes codes = {}; + const char* error = resolveCodes(&codes); if (error != nullptr) { - FF_DEBUG("Reading the transaction code from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error); return error; } @@ -901,7 +939,7 @@ static const char* detectWithBinder(FFlist* result) { return error; } FF_DEBUG("The \"%s\" service is handle %u, %s is transaction %d", - FF_WIFI_ANDROID_SERVICE, service.handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, transaction); + FF_WIFI_ANDROID_SERVICE, service.handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, codes.getConnectionInfo); uint8_t parcelBuffer[FF_WIFI_ANDROID_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -912,7 +950,7 @@ static const char* detectWithBinder(FFlist* result) { uint8_t replyBuffer[FF_WIFI_ANDROID_REPLY_SIZE]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getConnectionInfo, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -939,7 +977,7 @@ static const char* detectWithBinder(FFlist* result) { // that is up. WIFI_STATE_DISABLING is a radio that is still up, so only the one value means // down, and WIFI_STATE_UNKNOWN is the service declining to say. int32_t state = 0; - const char* stateError = callIntMethod(&binder, service.handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &state); + const char* stateError = callIntMethod(&binder, service.handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, codes.getWifiEnabledState, &state); if (stateError == nullptr && state != FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN) { connection.up = state != FF_WIFI_ANDROID_WIFI_STATE_DISABLED; connection.upKnown = true; @@ -953,6 +991,7 @@ static const char* detectWithBinder(FFlist* result) { // Nothing was found to report on at all. That is a device without a Wi-Fi interface rather than // one with a Wi-Fi that is off, which has a state to print by now. if (!connection.upKnown && !connection.connected) { + FF_DEBUG("No Wi-Fi interface is present"); return "No Wi-Fi interface is present"; } diff --git a/src/detection/wifi/wifi_bsd.c b/src/detection/wifi/wifi_bsd.c index 4b55d0257a..67af7f5c76 100644 --- a/src/detection/wifi/wifi_bsd.c +++ b/src/detection/wifi/wifi_bsd.c @@ -1,4 +1,5 @@ #include "wifi.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" @@ -11,11 +12,13 @@ const char* ffDetectWifi(FFlist* result) { struct if_nameindex* infs = if_nameindex(); if (!infs) { + FF_DEBUG("if_nameindex() failed: %s", strerror(errno)); return "if_nameindex() failed"; } FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { + FF_DEBUG("socket() failed: %s", strerror(errno)); if_freenameindex(infs); return "socket() failed"; } diff --git a/src/detection/wifi/wifi_nbsd.c b/src/detection/wifi/wifi_nbsd.c index e2b0836790..f423be03e6 100644 --- a/src/detection/wifi/wifi_nbsd.c +++ b/src/detection/wifi/wifi_nbsd.c @@ -1,7 +1,11 @@ #include "wifi.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" +#include +#include + #define COMPAT_FREEBSD_NET80211 1 #include #include @@ -21,11 +25,13 @@ const char* ffDetectWifi(FFlist* result) { struct if_nameindex* infs = if_nameindex(); if (!infs) { + FF_DEBUG("if_nameindex() failed: %s", strerror(errno)); return "if_nameindex() failed"; } FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { + FF_DEBUG("socket(AF_INET, SOCK_DGRAM) failed: %s", strerror(errno)); if_freenameindex(infs); return "socket() failed"; } diff --git a/src/detection/wifi/wifi_obsd.c b/src/detection/wifi/wifi_obsd.c index a9817c0511..7cb946b523 100644 --- a/src/detection/wifi/wifi_obsd.c +++ b/src/detection/wifi/wifi_obsd.c @@ -1,4 +1,5 @@ #include "wifi.h" +#include "common/debug.h" #include "common/io.h" #include @@ -11,11 +12,13 @@ const char* ffDetectWifi(FFlist* result) { struct if_nameindex* infs = if_nameindex(); if (!infs) { + FF_DEBUG("if_nameindex() failed: %s", strerror(errno)); return "if_nameindex() failed"; } FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { + FF_DEBUG("socket() failed: %s", strerror(errno)); if_freenameindex(infs); return "socket() failed"; } diff --git a/src/detection/wifi/wifi_sunos.c b/src/detection/wifi/wifi_sunos.c index 0b2e24ae21..bddfe44e0e 100644 --- a/src/detection/wifi/wifi_sunos.c +++ b/src/detection/wifi/wifi_sunos.c @@ -1,4 +1,5 @@ #include "wifi.h" +#include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" @@ -82,6 +83,7 @@ const char* ffDetectWifi(FFlist* result) { FF_AUTO_FREE void* bufferMemory = malloc(MAX_BUF_LEN); if (!bufferMemory) { + FF_DEBUG("malloc(MAX_BUF_LEN) failed"); return "malloc() failed"; } wldp_t* buffer = (wldp_t*) bufferMemory; @@ -182,8 +184,8 @@ const char* ffDetectWifi(FFlist* result) { } } - wl_encryption_t encryption; - wl_authmode_t authMode; + wl_encryption_t encryption = 0; + wl_authmode_t authMode = 0; bool haveEncryption = getWifiValue(fd, WL_ENCRYPTION, &encryption, sizeof(encryption), buffer); bool haveAuthMode = getWifiValue(fd, WL_AUTH_MODE, &authMode, sizeof(authMode), buffer); if (haveEncryption) { diff --git a/src/detection/wm/wm_linux.c b/src/detection/wm/wm_linux.c index b04bececad..b5468967eb 100644 --- a/src/detection/wm/wm_linux.c +++ b/src/detection/wm/wm_linux.c @@ -8,6 +8,7 @@ #include "common/debug.h" const char* ffDetectWMPlugin([[maybe_unused]] FFstrbuf* pluginName) { + FF_DEBUG("Not supported on this platform"); return "Not supported on this platform"; } @@ -120,6 +121,7 @@ static const char* getSway(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("sway", &path); if (error) { + FF_DEBUG("Failed to find sway executable: %s", error); return "Failed to find sway executable path"; } @@ -133,6 +135,7 @@ static const char* getSway(FFstrbuf* result) { return extractSwayVersion(buffer.chars, buffer.length, result) ? "Failed to parse sway version output" : nullptr; } + FF_DEBUG("Failed to run command `sway --version`"); return "Failed to run command `sway --version`"; } @@ -140,6 +143,7 @@ static const char* getLabwc(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("labwc", &path); if (error) { + FF_DEBUG("Failed to find labwc executable: %s", error); return "Failed to find labwc executable path"; } @@ -154,6 +158,7 @@ static const char* getLabwc(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `labwc --version`"); return "Failed to run command `labwc --version`"; } @@ -165,6 +170,7 @@ static const char* getNiri(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `niri --version`"); return "Failed to run command `niri --version`"; } @@ -176,6 +182,7 @@ static const char* getUmbriel(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `umbriel --version`"); return "Failed to run command `umbriel --version`"; } @@ -183,6 +190,7 @@ static const char* getWeston(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("weston", &path); if (error) { + FF_DEBUG("Failed to find weston executable: %s", error); return "Failed to find weston executable path"; } @@ -192,12 +200,14 @@ static const char* getWeston(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `weston --version`"); return "Failed to run command `weston --version`"; } #ifdef __linux__ static const char* getWslg(FFstrbuf* result) { if (!ffAppendFileBuffer("/mnt/wslg/versions.txt", result)) { + FF_DEBUG("Failed to read /mnt/wslg/versions.txt"); return "Failed to read /mnt/wslg/versions.txt"; } @@ -211,6 +221,7 @@ static const char* getWslg(FFstrbuf* result) { ffStrbufTrimLeft(result, ' '); } else { ffStrbufClear(result); + FF_DEBUG("Failed to parse WSLg version from /mnt/wslg/versions.txt"); return "Failed to parse WSLg version from /mnt/wslg/versions.txt"; } @@ -235,6 +246,7 @@ static const char* getI3(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("i3", &path); if (error) { + FF_DEBUG("Failed to find i3 executable: %s", error); return "Failed to find i3 executable path"; } @@ -249,6 +261,7 @@ static const char* getI3(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `i3 --version`"); return "Failed to run command `i3 --version`"; } @@ -256,6 +269,7 @@ static const char* getCtwm(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("ctwm", &path); if (error) { + FF_DEBUG("Failed to find ctwm executable: %s", error); return "Failed to find ctwm executable path"; } @@ -270,6 +284,7 @@ static const char* getCtwm(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `ctwm --version`"); return "Failed to run command `ctwm --version`"; } @@ -277,6 +292,7 @@ static const char* getFvwm(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("fvwm", &path); if (error) { + FF_DEBUG("Failed to find fvwm executable: %s", error); return "Failed to find fvwm executable path"; } @@ -291,6 +307,7 @@ static const char* getFvwm(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `fvwm -version`"); return "Failed to run command `fvwm -version`"; } @@ -298,6 +315,7 @@ static const char* getOpenbox(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("openbox", &path); if (error) { + FF_DEBUG("Failed to find openbox executable: %s", error); return "Failed to find openbox executable path"; } @@ -312,11 +330,13 @@ static const char* getOpenbox(FFstrbuf* result) { return nullptr; } + FF_DEBUG("Failed to run command `openbox --version`"); return "Failed to run command `openbox --version`"; } const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_unused]] FFWMOptions* options) { if (!wmName) { + FF_DEBUG("No WM detected"); return "No WM detected"; } @@ -370,5 +390,6 @@ const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_ return getOpenbox(result); } + FF_DEBUG("Unsupported WM: %s", wmName->chars); return "Unsupported WM"; } diff --git a/src/detection/wm/wm_windows.c b/src/detection/wm/wm_windows.c index 21e073dd26..ef1da03085 100644 --- a/src/detection/wm/wm_windows.c +++ b/src/detection/wm/wm_windows.c @@ -1,8 +1,10 @@ #include "wm.h" +#include "common/debug.h" #include "common/mallocHelper.h" #include "common/io.h" #include "common/library.h" #include "common/processing.h" +#include "common/windows/folders.h" #include "common/windows/nt.h" #include "common/windows/unicode.h" #include "common/windows/version.h" @@ -10,7 +12,6 @@ #include #include #include -#include #include typedef enum { @@ -69,16 +70,18 @@ static bool isProcessTrusted(DWORD processId, FFProcessType processType, UNICODE static wchar_t windowsAppsPath[MAX_PATH]; static uint32_t windowsAppsPathLen; if (windowsAppsPathLen == 0) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &pPath))) { - windowsAppsPathLen = (uint32_t) wcslen(pPath); - memcpy(windowsAppsPath, pPath, windowsAppsPathLen * sizeof(wchar_t)); - memcpy(windowsAppsPath + windowsAppsPathLen, L"\\WindowsApps\\", sizeof(L"\\WindowsApps\\")); - windowsAppsPathLen += strlen("\\WindowsApps\\"); + FF_STRBUF_AUTO_DESTROY programFiles = ffStrbufCreate(); + ULONG length = 0; // in bytes, including the null terminator + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &programFiles)) { + ffStrbufAppendS(&programFiles, "\\WindowsApps\\"); + if (NT_SUCCESS(RtlUTF8ToUnicodeN(windowsAppsPath, (ULONG) sizeof(windowsAppsPath), &length, programFiles.chars, (ULONG) programFiles.length + 1))) { + windowsAppsPathLen = (uint32_t) (length / sizeof(wchar_t) - 1); + } else { + windowsAppsPathLen = -1u; + } } else { windowsAppsPathLen = -1u; } - CoTaskMemFree(pPath); } if (windowsAppsPathLen != -1u && (buffer->Length <= windowsAppsPathLen * sizeof(wchar_t) || // Path is too short to be in WindowsApps @@ -206,6 +209,7 @@ const char* ffDetectWMPlugin(FFstrbuf* pluginName) { } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; } else { + FF_DEBUG("NtQuerySystemInformation(SystemProcessInformation) failed: %s", ffDebugNtStatus(status)); return "NtQuerySystemInformation(SystemProcessInformation) failed"; } } @@ -226,19 +230,17 @@ const char* ffDetectWMPlugin(FFstrbuf* pluginName) { const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_unused]] FFWMOptions* options) { if (!wmName) { + FF_DEBUG("No WM detected"); return "No WM detected"; } if (ffStrbufEqualS(wmName, "dwm.exe")) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_System, KF_FLAG_DEFAULT, nullptr, &pPath))) { - wchar_t fullPath[MAX_PATH]; - wcscpy(fullPath, pPath); - wcscat(fullPath, L"\\dwm.exe"); - ffGetFileVersion(fullPath, nullptr, result); - } - CoTaskMemFree(pPath); + wchar_t exePath[MAX_PATH]; + _snwprintf(exePath, ARRAY_SIZE(exePath), L"%ls\\system32\\%ls", (const wchar_t*) SharedUserData->NtSystemRoot, L"dwm.exe"); + ffGetFileVersion(exePath, nullptr, result); + return nullptr; } + FF_DEBUG("Not supported on this platform"); return "Not supported on this platform"; } diff --git a/src/logo/ascii/a.inc b/src/logo/ascii/a.inc index f9c26d55ed..c20136860c 100644 --- a/src/logo/ascii/a.inc +++ b/src/logo/ascii/a.inc @@ -665,6 +665,18 @@ static const FFlogo A[] = { .colorTitle = FF_COLOR_FG_CYAN, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_ARTIX3 + // Artix3 + { + .names = { "artix3" }, + .lines = FASTFETCH_DATATEXT_LOGO_ARTIX3, + .colors = { + FF_COLOR_FG_CYAN, + }, + .colorKeys = FF_COLOR_FG_CYAN, + .colorTitle = FF_COLOR_FG_CYAN, + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_ARTIX_SMALL // ArtixSmall { diff --git a/src/logo/ascii/a/artix2.txt b/src/logo/ascii/a/artix2.txt index 68d8399522..e45e825755 100644 --- a/src/logo/ascii/a/artix2.txt +++ b/src/logo/ascii/a/artix2.txt @@ -1,20 +1,20 @@ - .: - .==: - .====: - .======: - .========: - .==========: - .============: - .-===========: - .:-========: - .-:. .:======: - .======:.. :-===. - .============:.. .:=. - .==================:.. - .=======================: - .====================::. . - ================::. :-=== - ============-:. .:======== - ========-:. :-========== - =====:. ..:-===== - =::. ..:: \ No newline at end of file + ▄ + ▟█▙ + ▟███▙ + ▟█████▙ + ▟███████▙ + ▟█████████▙ + ▟███████████▙ + ▐█████████████▙ + ▀▀███████████▙ + ▄▄▂ ▀▀████████▙ + ▟████▆▅▄▂ ▀▀█████▙ + ▟██████████▆▅▄▂ ▀▀██▙ + ▟████████████████▆▅▄▂ ▀ + ▟█████████████████████▀ + ▟███████████████████▀▀ ▂▄▄ + ▟███████████████▀▀ ▂▄▆████▙ + ▟███████████▀▀ █████████▙ + ▟███████▀▀ ▀▀██████▙ + ▟███▀▀ ▀▀███▙ +▟▛▀ ▀▜▙ diff --git a/src/logo/ascii/a/artix3.txt b/src/logo/ascii/a/artix3.txt new file mode 100644 index 0000000000..68d8399522 --- /dev/null +++ b/src/logo/ascii/a/artix3.txt @@ -0,0 +1,20 @@ + .: + .==: + .====: + .======: + .========: + .==========: + .============: + .-===========: + .:-========: + .-:. .:======: + .======:.. :-===. + .============:.. .:=. + .==================:.. + .=======================: + .====================::. . + ================::. :-=== + ============-:. .:======== + ========-:. :-========== + =====:. ..:-===== + =::. ..:: \ No newline at end of file diff --git a/src/logo/ascii/n.inc b/src/logo/ascii/n.inc index e45d33ed13..dcd629a67d 100644 --- a/src/logo/ascii/n.inc +++ b/src/logo/ascii/n.inc @@ -213,6 +213,41 @@ static const FFlogo N[] = { .colorTitle = FF_COLOR_FG_DEFAULT, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_NURA + // Nura + { + .names = { "Nura" }, + .lines = FASTFETCH_DATATEXT_LOGO_NURA, + .colors = { + FF_COLOR_FG_GREEN, + FF_COLOR_FG_WHITE, + }, + }, + #endif + #ifdef FASTFETCH_DATATEXT_LOGO_NURA2 + // Nura2 + { + .names = { "Nura2" }, + .type = FF_LOGO_LINE_TYPE_ALTER_BIT, + .lines = FASTFETCH_DATATEXT_LOGO_NURA2, + .colors = { + FF_COLOR_FG_GREEN, + FF_COLOR_FG_WHITE, + }, + }, + #endif + #ifdef FASTFETCH_DATATEXT_LOGO_NURA_SMALL + // NuraSmall + { + .names = { "Nura_small" }, + .type = FF_LOGO_LINE_TYPE_SMALL_BIT, + .lines = FASTFETCH_DATATEXT_LOGO_NURA_SMALL, + .colors = { + FF_COLOR_FG_GREEN, + FF_COLOR_FG_WHITE, + }, + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_NUROS // NurOS { diff --git a/src/logo/ascii/p.inc b/src/logo/ascii/p.inc index 2cdbff886c..90c8f469f8 100644 --- a/src/logo/ascii/p.inc +++ b/src/logo/ascii/p.inc @@ -273,41 +273,6 @@ static const FFlogo P[] = { }, }, #endif - #ifdef FASTFETCH_DATATEXT_LOGO_POSTMARKETOS - // PostMarketOS - { - .names = { "PostMarketOS" }, - .lines = FASTFETCH_DATATEXT_LOGO_POSTMARKETOS, - .colors = { - FF_COLOR_FG_GREEN, - FF_COLOR_FG_WHITE, - }, - }, - #endif - #ifdef FASTFETCH_DATATEXT_LOGO_POSTMARKETOS2 - // PostMarketOS2 - { - .names = { "PostMarketOS2" }, - .type = FF_LOGO_LINE_TYPE_ALTER_BIT, - .lines = FASTFETCH_DATATEXT_LOGO_POSTMARKETOS2, - .colors = { - FF_COLOR_FG_GREEN, - FF_COLOR_FG_WHITE, - }, - }, - #endif - #ifdef FASTFETCH_DATATEXT_LOGO_POSTMARKETOS_SMALL - // PostMarketOSSmall - { - .names = { "PostMarketOS_small" }, - .type = FF_LOGO_LINE_TYPE_SMALL_BIT, - .lines = FASTFETCH_DATATEXT_LOGO_POSTMARKETOS_SMALL, - .colors = { - FF_COLOR_FG_GREEN, - FF_COLOR_FG_WHITE, - }, - }, - #endif #ifdef FASTFETCH_DATATEXT_LOGO_PROXMOX // Proxmox { diff --git a/src/logo/ascii/p/postmarketos.txt b/src/logo/ascii/p/nura.txt similarity index 100% rename from src/logo/ascii/p/postmarketos.txt rename to src/logo/ascii/p/nura.txt diff --git a/src/logo/ascii/p/postmarketos2.txt b/src/logo/ascii/p/nura2.txt similarity index 100% rename from src/logo/ascii/p/postmarketos2.txt rename to src/logo/ascii/p/nura2.txt diff --git a/src/logo/ascii/p/postmarketos_small.txt b/src/logo/ascii/p/nura_small.txt similarity index 100% rename from src/logo/ascii/p/postmarketos_small.txt rename to src/logo/ascii/p/nura_small.txt diff --git a/src/logo/image/image.c b/src/logo/image/image.c index e0cffbfbed..c96e8db392 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -1,5 +1,6 @@ #include "image.h" #include "common/io.h" +#include "common/library.h" #include "common/mallocHelper.h" #include "common/printing.h" #include "common/processing.h" @@ -488,6 +489,7 @@ static bool printImageKittyDirect(bool printError) { #include #include "common/time.h" + #include "common/debug.h" #ifndef _WIN32 #include @@ -497,20 +499,14 @@ static bool printImageKittyDirect(bool printError) { #endif #ifdef FF_HAVE_ZLIB - #include "common/library.h" - #include + #include "common/zlib.h" static bool compressBlob(void** blob, size_t* length) { - FF_LIBRARY_LOAD(zlib, false, - #ifdef _WIN32 - "zlib1" - #else - "libz" - #endif - FF_LIBRARY_EXTENSION, - 2) - FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false) - FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false) + const char* error = ffZlibLoad(FF_ZLIB_COMPRESS); + if (error != nullptr) { + FF_DEBUG("The logo stays uncompressed: %s", error); + return false; + } #if _WIN32 // zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be @@ -520,7 +516,7 @@ static bool compressBlob(void** blob, size_t* length) { } #endif - uLong compressedLength = ffcompressBound((uLong) *length); + uLong compressedLength = compressBound((uLong) *length); void* compressed = malloc(compressedLength); if (compressed == nullptr) { return false; @@ -534,7 +530,7 @@ static bool compressBlob(void** blob, size_t* length) { // The gap is far wider on Windows, where the same frames cost 37.7 ms each at level 9 against // 6.4 at level 6: raising this back to 9 now that the DLL name above actually loads is a 4.2x // slowdown there (398 ms -> 1692 ms). Re-measure both platforms before touching it. - if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) { + if (compress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) { free(compressed); return false; } diff --git a/src/modules/battery/battery.c b/src/modules/battery/battery.c index 57c4d56663..b9dc6e7c89 100644 --- a/src/modules/battery/battery.c +++ b/src/modules/battery/battery.c @@ -214,6 +214,7 @@ void ffParseBatteryJsonObject(FFBatteryOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateBatteryJsonConfig(FFBatteryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -306,6 +307,7 @@ FFModuleBaseInfo ffBatteryModuleInfo = { .cs = "Baterie", .de = "Akku", .es = "Batería", + .fi = "Akku", .fr = "Batterie", .gl = "Batería", .he = "סוללה", diff --git a/src/modules/bios/bios.c b/src/modules/bios/bios.c index 3bde59597d..bad622167e 100644 --- a/src/modules/bios/bios.c +++ b/src/modules/bios/bios.c @@ -85,6 +85,7 @@ void ffParseBiosJsonObject(FFBiosOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateBiosJsonConfig(FFBiosOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -139,6 +140,7 @@ FFModuleBaseInfo ffBiosModuleInfo = { .cs = "BIOS", .de = "BIOS", .es = "BIOS", + .fi = "BIOS", .fr = "BIOS", .gl = "BIOS", .he = "BIOS", diff --git a/src/modules/bluetooth/bluetooth.c b/src/modules/bluetooth/bluetooth.c index 66d6c809a7..a006e4944e 100644 --- a/src/modules/bluetooth/bluetooth.c +++ b/src/modules/bluetooth/bluetooth.c @@ -143,6 +143,7 @@ void ffParseBluetoothJsonObject(FFBluetoothOptions* options, yyjson_val* module) } } +[[gnu::cold]] void ffGenerateBluetoothJsonConfig(FFBluetoothOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -229,6 +230,7 @@ FFModuleBaseInfo ffBluetoothModuleInfo = { .cs = "Bluetooth", .de = "Bluetooth", .es = "Bluetooth", + .fi = "Bluetooth", .fr = "Bluetooth", .gl = "Bluetooth", .he = "בלוטות'", diff --git a/src/modules/bluetoothradio/bluetoothradio.c b/src/modules/bluetoothradio/bluetoothradio.c index 1ea2d86993..b906699b1d 100644 --- a/src/modules/bluetoothradio/bluetoothradio.c +++ b/src/modules/bluetoothradio/bluetoothradio.c @@ -152,6 +152,7 @@ void ffParseBluetoothRadioJsonObject(FFBluetoothRadioOptions* options, yyjson_va } } +[[gnu::cold]] void ffGenerateBluetoothRadioJsonConfig(FFBluetoothRadioOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -209,6 +210,7 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = { .cs = "Bluetooth rádio", .de = "Bluetooth-Adapter", .es = "Adaptador Bluetooth", + .fi = "Bluetooth-adapteri", .fr = "Adaptateur Bluetooth", .gl = "Adaptador Bluetooth", .he = "רדיו בלוטות'", @@ -219,7 +221,7 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = { .pl = "Adapter Bluetooth", .pt = "Adaptador Bluetooth", .ru = "Bluetooth-адаптер", - .tr = "Bluetooth Radyosu", + .tr = "Bluetooth Adaptörü", .uk = "Радіомодуль Bluetooth", .vi = "Radio Bluetooth", .zh_CN = "蓝牙适配器", diff --git a/src/modules/board/board.c b/src/modules/board/board.c index c0f49baaab..c239dd5368 100644 --- a/src/modules/board/board.c +++ b/src/modules/board/board.c @@ -60,6 +60,7 @@ void ffParseBoardJsonObject(FFBoardOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateBoardJsonConfig(FFBoardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -116,6 +117,7 @@ FFModuleBaseInfo ffBoardModuleInfo = { .cs = "Základní deska", .de = "Mainboard", .es = "Placa base", + .fi = "Emolevy", .fr = "Carte mère", .gl = "Placa base", .he = "לוח אם", diff --git a/src/modules/bootmgr/bootmgr.c b/src/modules/bootmgr/bootmgr.c index 45e29bbbb3..4000bc2493 100644 --- a/src/modules/bootmgr/bootmgr.c +++ b/src/modules/bootmgr/bootmgr.c @@ -63,6 +63,7 @@ void ffParseBootmgrJsonObject(FFBootmgrOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateBootmgrJsonConfig(FFBootmgrOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -111,6 +112,7 @@ FFModuleBaseInfo ffBootmgrModuleInfo = { .cs = "Správce spouštění", .de = "Boot-Manager", .es = "Gestor de arranque", + .fi = "Käynnistyslataaja", .fr = "Gestionnaire de démarrage", .gl = "Xestor de arranque", .he = "מנהל אתחול", diff --git a/src/modules/break/break.c b/src/modules/break/break.c index 11872495af..6d020d9e9d 100644 --- a/src/modules/break/break.c +++ b/src/modules/break/break.c @@ -35,6 +35,7 @@ FFModuleBaseInfo ffBreakModuleInfo = { .cs = "Konec řádku", .de = "Umbruch", .es = "Salto de línea", + .fi = "Rivinvaihto", .fr = "Saut de ligne", .gl = "Salto de liña", .he = "מעבר שורה", diff --git a/src/modules/brightness/brightness.c b/src/modules/brightness/brightness.c index 3cdb28bf11..144436cef7 100644 --- a/src/modules/brightness/brightness.c +++ b/src/modules/brightness/brightness.c @@ -135,6 +135,7 @@ void ffParseBrightnessJsonObject(FFBrightnessOptions* options, yyjson_val* modul } } +[[gnu::cold]] void ffGenerateBrightnessJsonConfig(FFBrightnessOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -199,6 +200,7 @@ FFModuleBaseInfo ffBrightnessModuleInfo = { .cs = "Jas", .de = "Helligkeit", .es = "Brillo", + .fi = "Kirkkaus", .fr = "Luminosité", .gl = "Brillo", .he = "בהירות", diff --git a/src/modules/btrfs/btrfs.c b/src/modules/btrfs/btrfs.c index 3cbacd9fd0..3dc347e651 100644 --- a/src/modules/btrfs/btrfs.c +++ b/src/modules/btrfs/btrfs.c @@ -141,6 +141,7 @@ void ffParseBtrfsJsonObject(FFBtrfsOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateBtrfsJsonConfig(FFBtrfsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -210,6 +211,7 @@ FFModuleBaseInfo ffBtrfsModuleInfo = { .cs = "BTRFS", .de = "BTRFS", .es = "BTRFS", + .fi = "BTRFS", .fr = "BTRFS", .gl = "BTRFS", .he = "BTRFS", diff --git a/src/modules/camera/camera.c b/src/modules/camera/camera.c index 3263e0f181..4e2bf4c22c 100644 --- a/src/modules/camera/camera.c +++ b/src/modules/camera/camera.c @@ -71,6 +71,7 @@ void ffParseCameraJsonObject(FFCameraOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCameraJsonConfig(FFCameraOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -123,6 +124,7 @@ FFModuleBaseInfo ffCameraModuleInfo = { .cs = "Kamera", .de = "Kamera", .es = "Cámara", + .fi = "Kamera", .fr = "Caméra", .gl = "Cámara", .he = "מצלמה", diff --git a/src/modules/chassis/chassis.c b/src/modules/chassis/chassis.c index 6049d533d5..f85ee21a4b 100644 --- a/src/modules/chassis/chassis.c +++ b/src/modules/chassis/chassis.c @@ -62,6 +62,7 @@ void ffParseChassisJsonObject(FFChassisOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateChassisJsonConfig(FFChassisOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -118,6 +119,7 @@ FFModuleBaseInfo ffChassisModuleInfo = { .cs = "Skříň", .de = "Gehäuse", .es = "Chasis", + .fi = "Alusta", .fr = "Châssis", .gl = "Chasis", .he = "מארז", diff --git a/src/modules/codec/codec.c b/src/modules/codec/codec.c index 1419d78c3f..1c2195316c 100644 --- a/src/modules/codec/codec.c +++ b/src/modules/codec/codec.c @@ -79,7 +79,7 @@ static void printCodecLine(const FFCodecOptions* options, uint8_t index, FFstrbu ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY); puts(typesJoined.length ? typesJoined.chars : "None"); } else { - FF_LIST_AUTO_DESTROY typeList = ffListCreate(); // Use list instead of pre-joined string for qjs and lua + FF_LIST_AUTO_DESTROY typeList = ffListCreate(); // Use list instead of pre-joined string for lua for (FFCodecType type = FF_CODEC_TYPE_H261; type <= FF_CODEC_TYPE_MAX; type <<= 1) { if ((types & type) == 0) { continue; @@ -186,6 +186,7 @@ void ffParseCodecJsonObject(FFCodecOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCodecJsonConfig(FFCodecOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); yyjson_mut_obj_add_bool(doc, module, "splitGPU", options->splitGPU); @@ -260,6 +261,7 @@ FFModuleBaseInfo ffCodecModuleInfo = { .cs = "Kodek", .de = "Codec", .es = "Códec", + .fi = "Koodekki", .fr = "Codec", .gl = "Códec", .he = "קודק", diff --git a/src/modules/colors/colors.c b/src/modules/colors/colors.c index b7e74c2613..bbb29a61c5 100644 --- a/src/modules/colors/colors.c +++ b/src/modules/colors/colors.c @@ -236,6 +236,7 @@ void ffParseColorsJsonObject(FFColorsOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateColorsJsonConfig(FFColorsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -313,6 +314,7 @@ FFModuleBaseInfo ffColorsModuleInfo = { .cs = "Barvy", .de = "Farben", .es = "Colores", + .fi = "Värit", .fr = "Couleurs", .gl = "Cores", .he = "צבעים", diff --git a/src/modules/command/command.c b/src/modules/command/command.c index 817bcf0d12..9d89f413dc 100644 --- a/src/modules/command/command.c +++ b/src/modules/command/command.c @@ -85,7 +85,24 @@ void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) { } if (unsafe_yyjson_equals_str(key, "useStdErr")) { - options->useStdErr = yyjson_get_bool(val); + options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT; + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead"); + continue; + } + + if (unsafe_yyjson_equals_str(key, "useOutput")) { + int value; + const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) { + { "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT }, + { "stderr", FF_PROCESS_OUTPUT_STDERR_BIT }, + { "both", FF_PROCESS_OUTPUT_BOTH }, + {}, + }); + if (error) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error); + } else { + options->useOutput = (FFProcessOutputType) value; + } continue; } @@ -103,13 +120,26 @@ void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell); yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param); yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text); - yyjson_mut_obj_add_bool(doc, module, "useStdErr", options->useStdErr); + switch (options->useOutput) { + case FF_PROCESS_OUTPUT_STDOUT_BIT: + yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout"); + break; + case FF_PROCESS_OUTPUT_STDERR_BIT: + yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr"); + break; + case FF_PROCESS_OUTPUT_BOTH: + yyjson_mut_obj_add_str(doc, module, "useOutput", "both"); + break; + default: + break; + } yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel); yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines); } @@ -161,13 +191,15 @@ void ffInitCommandOptions(FFCommandOptions* options) { ); ffStrbufInitStatic(&options->param, #ifdef _WIN32 - "/c" + "/d/c" #else "-c" #endif ); ffStrbufInit(&options->text); - options->useStdErr = false; + // Capture both streams by default: most tools log to stdout or stderr without saying which, and + // the previous single-stream default silently dropped one of them. + options->useOutput = FF_PROCESS_OUTPUT_BOTH; options->parallel = true; options->splitLines = false; } @@ -188,6 +220,7 @@ FFModuleBaseInfo ffCommandModuleInfo = { .cs = "Příkaz", .de = "Befehl", .es = "Comando", + .fi = "Komento", .fr = "Commande", .gl = "Comando", .he = "פקודה", diff --git a/src/modules/command/option.h b/src/modules/command/option.h index 6b2108fb77..255433c91c 100644 --- a/src/modules/command/option.h +++ b/src/modules/command/option.h @@ -1,6 +1,7 @@ #pragma once #include "common/option.h" +#include "common/processing.h" // FFProcessOutputType typedef struct FFCommandOptions { FFModuleArgs moduleArgs; @@ -8,7 +9,7 @@ typedef struct FFCommandOptions { FFstrbuf shell; FFstrbuf param; FFstrbuf text; - bool useStdErr; + FFProcessOutputType useOutput; bool parallel; bool splitLines; } FFCommandOptions; diff --git a/src/modules/cpu/cpu.c b/src/modules/cpu/cpu.c index 9d52576873..398ac3e986 100644 --- a/src/modules/cpu/cpu.c +++ b/src/modules/cpu/cpu.c @@ -144,6 +144,7 @@ void ffParseCPUJsonObject(FFCPUOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCPUJsonConfig(FFCPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -259,6 +260,7 @@ FFModuleBaseInfo ffCPUModuleInfo = { .cs = "CPU", .de = "CPU", .es = "CPU", + .fi = "Suoritin", .fr = "CPU", .gl = "CPU", .he = "מעבד", diff --git a/src/modules/cpucache/cpucache.c b/src/modules/cpucache/cpucache.c index bf93dcca5a..4547b1e123 100644 --- a/src/modules/cpucache/cpucache.c +++ b/src/modules/cpucache/cpucache.c @@ -151,6 +151,7 @@ void ffParseCPUCacheJsonObject(FFCPUCacheOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCPUCacheJsonConfig(FFCPUCacheOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -231,6 +232,7 @@ FFModuleBaseInfo ffCPUCacheModuleInfo = { .cs = "Mezipaměť CPU", .de = "CPU-Cache", .es = "Caché de la CPU", + .fi = "Suorittimen Välimuisti", .fr = "Cache CPU", .gl = "Caché da CPU", .he = "מטמון מעבד", diff --git a/src/modules/cpuusage/cpuusage.c b/src/modules/cpuusage/cpuusage.c index a5a7f2dab0..eba81a24af 100644 --- a/src/modules/cpuusage/cpuusage.c +++ b/src/modules/cpuusage/cpuusage.c @@ -144,6 +144,7 @@ void ffParseCPUUsageJsonObject(FFCPUUsageOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCPUUsageJsonConfig(FFCPUUsageOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -190,6 +191,7 @@ FFModuleBaseInfo ffCPUUsageModuleInfo = { .cs = "Využití CPU", .de = "CPU-Auslastung", .es = "Uso de la CPU", + .fi = "Suorittimen Käyttö", .fr = "Utilisation du CPU", .gl = "Uso da CPU", .he = "שימוש במעבד", diff --git a/src/modules/cursor/cursor.c b/src/modules/cursor/cursor.c index 7bc274802c..39d5b1522a 100644 --- a/src/modules/cursor/cursor.c +++ b/src/modules/cursor/cursor.c @@ -62,6 +62,7 @@ void ffParseCursorJsonObject(FFCursorOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateCursorJsonConfig(FFCursorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -108,6 +109,7 @@ FFModuleBaseInfo ffCursorModuleInfo = { .cs = "Kurzor", .de = "Mauszeiger", .es = "Cursor", + .fi = "Kursori", .fr = "Curseur", .gl = "Cursor", .he = "סמן", diff --git a/src/modules/custom/custom.c b/src/modules/custom/custom.c index 2c11d87c64..6f889f6e60 100644 --- a/src/modules/custom/custom.c +++ b/src/modules/custom/custom.c @@ -9,6 +9,7 @@ bool ffPrintCustom(FFCustomOptions* options) { return true; } +[[gnu::cold]] void ffGenerateCustomJsonConfig(FFCustomOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -17,7 +18,7 @@ bool ffGenerateCustomJsonResult(FFCustomOptions* options, yyjson_mut_doc* doc, y FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); // This module's output *is* its format string, so the JSON result is that string rendered — - // for a `qjs:`/`lua:` format that means the script's output, not the script. `ffPrintCustom()` + // for a `lua:` format that means the script's output, not the script. `ffPrintCustom()` // gets the same value through `ffPrintFormat()`. Without this the module had no JSON result at // all, so a script consuming `--format json` could not read a custom line's value. if (!ffParseFormatString(&result, &options->moduleArgs.outputFormat, 0, (FFformatarg[]) {})) { @@ -62,6 +63,7 @@ FFModuleBaseInfo ffCustomModuleInfo = { .cs = "Vlastní", .de = "Benutzerdefiniert", .es = "Personalizado", + .fi = "Mukautettu", .fr = "Personnalisé", .gl = "Personalizado", .he = "מותאם אישית", diff --git a/src/modules/datetime/datetime.c b/src/modules/datetime/datetime.c index b3c505a16a..6c66e1fc20 100644 --- a/src/modules/datetime/datetime.c +++ b/src/modules/datetime/datetime.c @@ -132,6 +132,7 @@ void ffParseDateTimeJsonObject(FFDateTimeOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDateTimeJsonConfig(FFDateTimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -158,6 +159,7 @@ FFModuleBaseInfo ffDateTimeModuleInfo = { .cs = "Datum a čas", .de = "Datum & Uhrzeit", .es = "Fecha y hora", + .fi = "Päivämäärä ja kellonaika", .fr = "Date et heure", .gl = "Data e hora", .he = "תאריך ושעה", diff --git a/src/modules/de/de.c b/src/modules/de/de.c index a7bb99c3a2..ee97c3e7b1 100644 --- a/src/modules/de/de.c +++ b/src/modules/de/de.c @@ -51,6 +51,7 @@ void ffParseDEJsonObject(FFDEOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDEJsonConfig(FFDEOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -90,6 +91,7 @@ FFModuleBaseInfo ffDEModuleInfo = { .cs = "Pracovní prostředí", .de = "Desktop-Umgebung", .es = "Entorno de escritorio", + .fi = "Työpöytäympäristö", .fr = "Environnement de Bureau", .gl = "Contorno do Escritorio", .he = "סביבת שולחן עבודה", diff --git a/src/modules/disk/disk.c b/src/modules/disk/disk.c index c9c42e2700..b3176fe158 100644 --- a/src/modules/disk/disk.c +++ b/src/modules/disk/disk.c @@ -15,6 +15,8 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index) { FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate(); + uint64_t bytesUsed = disk->bytesTotal - disk->bytesFree; + if (options->moduleArgs.key.length == 0) { ffStrbufSetF(&key, "%s (%s)", FF_MODULE_GET_DISPLAY_NAME(Disk), disk->mountpoint.chars); } else { @@ -50,12 +52,12 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index } FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate(); - ffSizeAppendNum(disk->bytesUsed, &usedPretty); + ffSizeAppendNum(bytesUsed, &usedPretty); FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate(); ffSizeAppendNum(disk->bytesTotal, &totalPretty); - double bytesPercentage = disk->bytesTotal > 0 ? (double) disk->bytesUsed / (double) disk->bytesTotal * 100.0 : 0; + double bytesPercentage = disk->bytesTotal > 0 ? (double) bytesUsed / (double) disk->bytesTotal * 100.0 : 0; FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type; if (options->moduleArgs.outputFormat.length == 0) { @@ -136,9 +138,6 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index FF_STRBUF_AUTO_DESTROY freePretty = ffStrbufCreate(); ffSizeAppendNum(disk->bytesFree, &freePretty); - FF_STRBUF_AUTO_DESTROY availPretty = ffStrbufCreate(); - ffSizeAppendNum(disk->bytesAvailable, &availPretty); - uint64_t now = ffTimeGetNow(); uint64_t duration = now - disk->createTime; uint32_t milliseconds = (uint32_t) (duration % 1000); @@ -178,7 +177,8 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index FF_ARG(age.daysOfYear, "days-of-year"), FF_ARG(age.yearsFraction, "years-fraction"), FF_ARG(freePretty, "size-free"), - FF_ARG(availPretty, "size-available"), + FF_ARG(bytesUsed, "bytes-used"), + FF_ARG(disk->bytesTotal, "bytes-total"), })); } } @@ -332,6 +332,7 @@ void ffParseDiskJsonObject(FFDiskOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDiskJsonConfig(FFDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -373,10 +374,8 @@ bool ffGenerateDiskJsonResult(FFDiskOptions* options, yyjson_mut_doc* doc, yyjso yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr); yyjson_mut_val* bytes = yyjson_mut_obj_add_obj(doc, obj, "bytes"); - yyjson_mut_obj_add_uint(doc, bytes, "available", item->bytesAvailable); yyjson_mut_obj_add_uint(doc, bytes, "free", item->bytesFree); yyjson_mut_obj_add_uint(doc, bytes, "total", item->bytesTotal); - yyjson_mut_obj_add_uint(doc, bytes, "used", item->bytesUsed); yyjson_mut_val* files = yyjson_mut_obj_add_obj(doc, obj, "files"); if (item->filesTotal == 0 && item->filesUsed == 0) { @@ -435,6 +434,8 @@ void ffInitDiskOptions(FFDiskOptions* options) { ffStrbufInit(&options->folders); #if _WIN32 || __APPLE__ || __ANDROID__ ffStrbufInit(&options->hideFolders); +#elif __HAIKU__ + ffStrbufInitS(&options->hideFolders, "/esp"); // /boot on haiku is the system volume #else ffStrbufInitS(&options->hideFolders, "/efi:/boot:/boot/*"); #endif @@ -460,6 +461,7 @@ FFModuleBaseInfo ffDiskModuleInfo = { .cs = "Disk", .de = "Festplatte", .es = "Disco", + .fi = "Levy", .fr = "Disque", .gl = "Disco", .he = "דיסק", @@ -483,8 +485,8 @@ FFModuleBaseInfo ffDiskModuleInfo = { .generateJsonResult = (void*) ffGenerateDiskJsonResult, .generateJsonConfig = (void*) ffGenerateDiskJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Size used", "size-used" }, - { "Size total", "size-total" }, + { "Size used (formatted)", "size-used" }, + { "Size total (formatted)", "size-total" }, { "Size percentage num", "size-percentage" }, { "Files used", "files-used" }, { "Files total", "files-total" }, @@ -507,8 +509,9 @@ FFModuleBaseInfo ffDiskModuleInfo = { { "Years integer after creation", "years" }, { "Days of year after creation", "days-of-year" }, { "Years fraction after creation", "years-fraction" }, - { "Size free", "size-free" }, - { "Size available", "size-available" }, + { "Size free (formatted)", "size-free" }, + { "Size used (in bytes)", "bytes-used" }, + { "Size total (in bytes)", "bytes-total" }, })), .defaultOrder = 42, }; diff --git a/src/modules/diskio/diskio.c b/src/modules/diskio/diskio.c index f87aee72b3..1d079bcc9f 100644 --- a/src/modules/diskio/diskio.c +++ b/src/modules/diskio/diskio.c @@ -125,6 +125,7 @@ void ffParseDiskIOJsonObject(FFDiskIOOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDiskIOJsonConfig(FFDiskIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -185,6 +186,7 @@ FFModuleBaseInfo ffDiskIOModuleInfo = { .cs = "Diskové I/O", .de = "Festplatten I/O", .es = "E/S del disco", + .fi = "Levyn I/O", .fr = "Entrée/Sortie disque", .gl = "E/S do disco", .he = "קלט/פלט דיסק", diff --git a/src/modules/display/display.c b/src/modules/display/display.c index b6d8a13f22..677074ff1c 100644 --- a/src/modules/display/display.c +++ b/src/modules/display/display.c @@ -278,6 +278,7 @@ void ffParseDisplayJsonObject(FFDisplayOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDisplayJsonConfig(FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -439,6 +440,7 @@ FFModuleBaseInfo ffDisplayModuleInfo = { .cs = "Displej", .de = "Anzeige", .es = "Pantalla", + .fi = "Näyttö", .fr = "Écran", .gl = "Pantalla", .he = "תצוגה", diff --git a/src/modules/dns/dns.c b/src/modules/dns/dns.c index 3c76a0b7f6..c1b7b63b24 100644 --- a/src/modules/dns/dns.c +++ b/src/modules/dns/dns.c @@ -84,6 +84,7 @@ void ffParseDNSJsonObject(FFDNSOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateDNSJsonConfig(FFDNSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -142,6 +143,7 @@ FFModuleBaseInfo ffDNSModuleInfo = { .cs = "DNS", .de = "DNS", .es = "DNS", + .fi = "DNS", .fr = "DNS", .gl = "DNS", .he = "DNS", diff --git a/src/modules/editor/editor.c b/src/modules/editor/editor.c index 7a0f4240ec..d743af2835 100644 --- a/src/modules/editor/editor.c +++ b/src/modules/editor/editor.c @@ -70,6 +70,7 @@ void ffParseEditorJsonObject(FFEditorOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateEditorJsonConfig(FFEditorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -118,6 +119,7 @@ FFModuleBaseInfo ffEditorModuleInfo = { .cs = "Editor", .de = "Editor", .es = "Editor", + .fi = "Editori", .fr = "Éditeur", .gl = "Editor", .he = "עורך", diff --git a/src/modules/font/font.c b/src/modules/font/font.c index 47b6d8f2d2..8dbf58f569 100644 --- a/src/modules/font/font.c +++ b/src/modules/font/font.c @@ -53,6 +53,7 @@ void ffParseFontJsonObject(FFFontOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateFontJsonConfig(FFFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -103,6 +104,7 @@ FFModuleBaseInfo ffFontModuleInfo = { .cs = "Písmo", .de = "Schriftart", .es = "Fuente", + .fi = "Fontti", .fr = "Police", .gl = "Fonte", .he = "גופן", diff --git a/src/modules/gamepad/gamepad.c b/src/modules/gamepad/gamepad.c index 81dad9e496..58629e5523 100644 --- a/src/modules/gamepad/gamepad.c +++ b/src/modules/gamepad/gamepad.c @@ -128,6 +128,7 @@ void ffParseGamepadJsonObject(FFGamepadOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateGamepadJsonConfig(FFGamepadOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -201,6 +202,7 @@ FFModuleBaseInfo ffGamepadModuleInfo = { .cs = "Gamepad", .de = "Gamepad", .es = "Gamepad", + .fi = "Peliohjain", .fr = "Manette", .gl = "Gamepad", .he = "משטח משחק", diff --git a/src/modules/gpu/gpu.c b/src/modules/gpu/gpu.c index 56c0eed622..df4306dfdb 100644 --- a/src/modules/gpu/gpu.c +++ b/src/modules/gpu/gpu.c @@ -286,6 +286,7 @@ void ffParseGPUJsonObject(FFGPUOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateGPUJsonConfig(FFGPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -493,6 +494,7 @@ FFModuleBaseInfo ffGPUModuleInfo = { .cs = "GPU", .de = "GPU", .es = "GPU", + .fi = "Grafiikkasuoritin", .fr = "GPU", .gl = "GPU", .he = "מעבד גרפי", diff --git a/src/modules/host/host.c b/src/modules/host/host.c index ef72887766..1802450c4b 100644 --- a/src/modules/host/host.c +++ b/src/modules/host/host.c @@ -79,6 +79,7 @@ void ffParseHostJsonObject(FFHostOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateHostJsonConfig(FFHostOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -144,6 +145,7 @@ FFModuleBaseInfo ffHostModuleInfo = { .cs = "Hostitel", .de = "Rechner", .es = "Host", + .fi = "Isäntä", .fr = "Hôte", .gl = "Host", .he = "מארח", @@ -154,7 +156,7 @@ FFModuleBaseInfo ffHostModuleInfo = { .pl = "Host", .pt = "Host", .ru = "Модель", - .tr = "Ana Bilgisayar", + .tr = "Cihaz", .uk = "Хост", .vi = "Máy chủ", .zh_CN = "主机", diff --git a/src/modules/icons/icons.c b/src/modules/icons/icons.c index 358cc3dbaa..b690cf085d 100644 --- a/src/modules/icons/icons.c +++ b/src/modules/icons/icons.c @@ -56,6 +56,7 @@ void ffParseIconsJsonObject(FFIconsOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateIconsJsonConfig(FFIconsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -102,6 +103,7 @@ FFModuleBaseInfo ffIconsModuleInfo = { .cs = "Ikony", .de = "Symbole", .es = "Iconos", + .fi = "Kuvakkeet", .fr = "Icônes", .gl = "Iconas", .he = "סמלים", diff --git a/src/modules/initsystem/initsystem.c b/src/modules/initsystem/initsystem.c index 3b5db0d8b1..c796f4ba39 100644 --- a/src/modules/initsystem/initsystem.c +++ b/src/modules/initsystem/initsystem.c @@ -58,6 +58,7 @@ void ffParseInitSystemJsonObject(FFInitSystemOptions* options, yyjson_val* modul } } +[[gnu::cold]] void ffGenerateInitSystemJsonConfig(FFInitSystemOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -109,6 +110,7 @@ FFModuleBaseInfo ffInitSystemModuleInfo = { .cs = "Init systém", .de = "Init-System", .es = "Sistema de inicio", + .fi = "Init-järjestelmä", .fr = "Système d'init", .gl = "Sistema de inicio", .he = "מערכת אתחול", diff --git a/src/modules/kernel/kernel.c b/src/modules/kernel/kernel.c index 322b2b41d4..fa23960892 100644 --- a/src/modules/kernel/kernel.c +++ b/src/modules/kernel/kernel.c @@ -36,6 +36,7 @@ void ffParseKernelJsonObject(FFKernelOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateKernelJsonConfig(FFKernelOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -70,6 +71,7 @@ FFModuleBaseInfo ffKernelModuleInfo = { .cs = "Jádro", .de = "Kernel", .es = "Kernel", + .fi = "Ydin", .fr = "Kernel", .gl = "Kernel", .he = "ליבה", diff --git a/src/modules/keyboard/keyboard.c b/src/modules/keyboard/keyboard.c index d7aae3ae56..da8c6a32d0 100644 --- a/src/modules/keyboard/keyboard.c +++ b/src/modules/keyboard/keyboard.c @@ -92,6 +92,7 @@ void ffParseKeyboardJsonObject(FFKeyboardOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateKeyboardJsonConfig(FFKeyboardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -161,6 +162,7 @@ FFModuleBaseInfo ffKeyboardModuleInfo = { .cs = "Klávesnice", .de = "Tastatur", .es = "Teclado", + .fi = "Näppäimistö", .fr = "Clavier", .gl = "Teclado", .he = "מקלדת", diff --git a/src/modules/lm/lm.c b/src/modules/lm/lm.c index ece31fb0ec..8241b9166d 100644 --- a/src/modules/lm/lm.c +++ b/src/modules/lm/lm.c @@ -58,6 +58,7 @@ void ffParseLMJsonObject(FFLMOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateLMJsonConfig(FFLMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -111,6 +112,7 @@ FFModuleBaseInfo ffLMModuleInfo = { .cs = "Správce přihlášení", .de = "Anmeldemanager", .es = "Gestor de inicio de sesión", + .fi = "Sisäänkirjautumishallinta", .fr = "Gestionnaire de connexion", .gl = "Xestor de inicio de sesión", .he = "מנהל התחברות", @@ -121,7 +123,7 @@ FFModuleBaseInfo ffLMModuleInfo = { .pl = "Menedżer logowania", .pt = "Gerenciador de login", .ru = "Менеджер входа", - .tr = "Oturum Açma Yöneticisi", + .tr = "Giriş Yöneticisi", .uk = "Менеджер входу", .vi = "Trình quản lý đăng nhập", .zh_CN = "登录管理器", diff --git a/src/modules/loadavg/loadavg.c b/src/modules/loadavg/loadavg.c index 8d19eabef3..afd24025be 100644 --- a/src/modules/loadavg/loadavg.c +++ b/src/modules/loadavg/loadavg.c @@ -116,6 +116,7 @@ void ffParseLoadavgJsonObject(FFLoadavgOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateLoadavgJsonConfig(FFLoadavgOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -164,6 +165,7 @@ FFModuleBaseInfo ffLoadavgModuleInfo = { .cs = "Průměrná zátěž", .de = "Systemlast", .es = "Promedio de carga", + .fi = "Kuormituksen Keskiarvo", .fr = "Charge moyenne", .gl = "Carga media", .he = "עומס ממוצע", diff --git a/src/modules/locale/locale.c b/src/modules/locale/locale.c index 97d45a8d2e..12ad320dcf 100644 --- a/src/modules/locale/locale.c +++ b/src/modules/locale/locale.c @@ -34,6 +34,7 @@ void ffParseLocaleJsonObject(FFLocaleOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateLocaleJsonConfig(FFLocaleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -75,6 +76,7 @@ FFModuleBaseInfo ffLocaleModuleInfo = { .cs = "Národní prostředí", .de = "Sprachraum", .es = "Configuración regional", + .fi = "Alueasetus", .fr = "Paramètres régionaux", .gl = "Configuración rexional", .he = "לוקאל", diff --git a/src/modules/localip/localip.c b/src/modules/localip/localip.c index 49103bd0bf..afe2780104 100644 --- a/src/modules/localip/localip.c +++ b/src/modules/localip/localip.c @@ -314,6 +314,7 @@ void ffParseLocalIpJsonObject(FFLocalIpOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateLocalIpJsonConfig(FFLocalIpOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -444,6 +445,7 @@ FFModuleBaseInfo ffLocalIPModuleInfo = { .cs = "Místní IP", .de = "Lokale IP", .es = "IP local", + .fi = "Paikallinen IP", .fr = "IP locale", .gl = "IP local", .he = "IP מקומי", diff --git a/src/modules/logo/logo.c b/src/modules/logo/logo.c index 4a30e1defd..6bdc8b3f92 100644 --- a/src/modules/logo/logo.c +++ b/src/modules/logo/logo.c @@ -87,6 +87,7 @@ FFModuleBaseInfo ffLogoModuleInfo = { .cs = "Logo", .de = "Logo", .es = "Logo", + .fi = "Logo", .fr = "Logo", .gl = "Logo", .he = "לוגו", diff --git a/src/modules/media/media.c b/src/modules/media/media.c index 8205ad3407..14b7faa9ac 100644 --- a/src/modules/media/media.c +++ b/src/modules/media/media.c @@ -205,6 +205,7 @@ void ffParseMediaJsonObject(FFMediaOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateMediaJsonConfig(FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -261,6 +262,7 @@ FFModuleBaseInfo ffMediaModuleInfo = { .cs = "Média", .de = "Medien", .es = "Multimedia", + .fi = "Multimedia", .fr = "Médias", .gl = "Multimedia", .he = "מדיה", diff --git a/src/modules/memory/memory.c b/src/modules/memory/memory.c index 9b4ac23944..80fbe96253 100644 --- a/src/modules/memory/memory.c +++ b/src/modules/memory/memory.c @@ -86,6 +86,7 @@ void ffParseMemoryJsonObject(FFMemoryOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateMemoryJsonConfig(FFMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -126,6 +127,7 @@ FFModuleBaseInfo ffMemoryModuleInfo = { .cs = "Paměť", .de = "Speicher", .es = "Memoria", + .fi = "Keskusmuisti", .fr = "Mémoire", .gl = "Memoria", .he = "זיכרון", diff --git a/src/modules/monitor/monitor.c b/src/modules/monitor/monitor.c index 7022a044d1..9ccaecb8be 100644 --- a/src/modules/monitor/monitor.c +++ b/src/modules/monitor/monitor.c @@ -87,6 +87,7 @@ void ffParseMonitorJsonObject(FFMonitorOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateMonitorJsonConfig(FFMonitorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -113,6 +114,7 @@ FFModuleBaseInfo ffMonitorModuleInfo = { .cs = "Monitor", .de = "Monitor", .es = "Monitor", + .fi = "Monitori", .fr = "Moniteur", .gl = "Monitor", .he = "צג", diff --git a/src/modules/mouse/mouse.c b/src/modules/mouse/mouse.c index 6cd28e4871..11eaa209ef 100644 --- a/src/modules/mouse/mouse.c +++ b/src/modules/mouse/mouse.c @@ -91,6 +91,7 @@ void ffParseMouseJsonObject(FFMouseOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateMouseJsonConfig(FFMouseOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -160,6 +161,7 @@ FFModuleBaseInfo ffMouseModuleInfo = { .cs = "Myš", .de = "Maus", .es = "Ratón", + .fi = "Hiiri", .fr = "Souris", .gl = "Rato", .he = "עכבר", diff --git a/src/modules/netio/netio.c b/src/modules/netio/netio.c index 70d74f64a8..810fe4fd7b 100644 --- a/src/modules/netio/netio.c +++ b/src/modules/netio/netio.c @@ -140,6 +140,7 @@ void ffParseNetIOJsonObject(FFNetIOOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateNetIOJsonConfig(FFNetIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -212,6 +213,7 @@ FFModuleBaseInfo ffNetIOModuleInfo = { .cs = "Síťové I/O", .de = "Netzwerk I/O", .es = "E/S de la red", + .fi = "Verkon I/O", .fr = "E/S réseau", .gl = "E/S da rede", .he = "קלט/פלט רשת", diff --git a/src/modules/opencl/opencl.c b/src/modules/opencl/opencl.c index b6c45430bc..50490c90e9 100644 --- a/src/modules/opencl/opencl.c +++ b/src/modules/opencl/opencl.c @@ -39,6 +39,7 @@ void ffParseOpenCLJsonObject(FFOpenCLOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateOpenCLJsonConfig(FFOpenCLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -128,6 +129,7 @@ FFModuleBaseInfo ffOpenCLModuleInfo = { .cs = "OpenCL", .de = "OpenCL", .es = "OpenCL", + .fi = "OpenCL", .fr = "OpenCL", .gl = "OpenCL", .he = "OpenCL", diff --git a/src/modules/opengl/opengl.c b/src/modules/opengl/opengl.c index 79bfc58f54..8ebf1226dc 100644 --- a/src/modules/opengl/opengl.c +++ b/src/modules/opengl/opengl.c @@ -69,6 +69,7 @@ void ffParseOpenGLJsonObject(FFOpenGLOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateOpenGLJsonConfig(FFOpenGLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -135,6 +136,7 @@ FFModuleBaseInfo ffOpenGLModuleInfo = { .cs = "OpenGL", .de = "OpenGL", .es = "OpenGL", + .fi = "OpenGL", .fr = "OpenGL", .gl = "OpenGL", .he = "OpenGL", diff --git a/src/modules/os/os.c b/src/modules/os/os.c index 085a2058e6..e188f6e7f5 100644 --- a/src/modules/os/os.c +++ b/src/modules/os/os.c @@ -117,6 +117,7 @@ void ffParseOSJsonObject(FFOSOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateOSJsonConfig(FFOSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -181,6 +182,7 @@ FFModuleBaseInfo ffOSModuleInfo = { .cs = "OS", .de = "Betriebssystem", .es = "Sistema operativo", + .fi = "Käyttöjärjestelmä", .fr = "Système d'exploitation", .gl = "Sistema operativo", .he = "מערכת הפעלה", diff --git a/src/modules/packages/packages.c b/src/modules/packages/packages.c index 307e1ccd42..b8b7f39b2c 100644 --- a/src/modules/packages/packages.c +++ b/src/modules/packages/packages.c @@ -376,6 +376,7 @@ void ffParsePackagesJsonObject(FFPackagesOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGeneratePackagesJsonConfig(FFPackagesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -528,6 +529,7 @@ FFModuleBaseInfo ffPackagesModuleInfo = { .cs = "Balíčky", .de = "Pakete", .es = "Paquetes", + .fi = "Ohjelmistopaketit", .fr = "Paquets", .gl = "Paquetes", .he = "חבילות", diff --git a/src/modules/physicaldisk/physicaldisk.c b/src/modules/physicaldisk/physicaldisk.c index 9da0d82146..bf8c5c389a 100644 --- a/src/modules/physicaldisk/physicaldisk.c +++ b/src/modules/physicaldisk/physicaldisk.c @@ -171,6 +171,7 @@ void ffParsePhysicalDiskJsonObject(FFPhysicalDiskOptions* options, yyjson_val* m } } +[[gnu::cold]] void ffGeneratePhysicalDiskJsonConfig(FFPhysicalDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -272,6 +273,7 @@ FFModuleBaseInfo ffPhysicalDiskModuleInfo = { .cs = "Fyzický disk", .de = "Physische Festplatte", .es = "Disco físico", + .fi = "Fyysinen Levy", .fr = "Disque physique", .gl = "Disco físico", .he = "דיסק פיזי", diff --git a/src/modules/physicalmemory/physicalmemory.c b/src/modules/physicalmemory/physicalmemory.c index 29f281fdea..7905aa74cb 100644 --- a/src/modules/physicalmemory/physicalmemory.c +++ b/src/modules/physicalmemory/physicalmemory.c @@ -105,6 +105,7 @@ void ffParsePhysicalMemoryJsonObject(FFPhysicalMemoryOptions* options, yyjson_va } } +[[gnu::cold]] void ffGeneratePhysicalMemoryJsonConfig(FFPhysicalMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); yyjson_mut_obj_add_bool(doc, module, "showEmptySlots", options->showEmptySlots); @@ -165,6 +166,7 @@ FFModuleBaseInfo ffPhysicalMemoryModuleInfo = { .cs = "Fyzická paměť", .de = "Physischer Speicher", .es = "Memoria física", + .fi = "Fyysinen Muisti", .fr = "Mémoire physique", .gl = "Memoria física", .he = "זיכרון פיזי", diff --git a/src/modules/player/player.c b/src/modules/player/player.c index f50aca999d..f5f4018839 100644 --- a/src/modules/player/player.c +++ b/src/modules/player/player.c @@ -89,6 +89,7 @@ void ffParsePlayerJsonObject(FFPlayerOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGeneratePlayerJsonConfig(FFPlayerOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -115,6 +116,7 @@ FFModuleBaseInfo ffPlayerModuleInfo = { .cs = "Přehrávač médií", .de = "Medienplayer", .es = "Reproductor multimedia", + .fi = "Mediasoitin", .fr = "Lecteur multimédia", .gl = "Reprodutor multimedia", .he = "נגן מדיה", diff --git a/src/modules/poweradapter/poweradapter.c b/src/modules/poweradapter/poweradapter.c index 3780ba103a..0cb7b2763d 100644 --- a/src/modules/poweradapter/poweradapter.c +++ b/src/modules/poweradapter/poweradapter.c @@ -51,6 +51,7 @@ bool ffPrintPowerAdapter(FFPowerAdapterOptions* options) { return true; } +[[gnu::cold]] void ffGeneratePowerAdapterJsonConfig(FFPowerAdapterOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -116,6 +117,7 @@ FFModuleBaseInfo ffPowerAdapterModuleInfo = { .cs = "Napájecí adaptér", .de = "Netzteil", .es = "Adaptador de corriente", + .fi = "Virta-adapteri", .fr = "Adaptateur secteur", .gl = "Adaptador de corrente", .he = "מתאם מתח", diff --git a/src/modules/processes/processes.c b/src/modules/processes/processes.c index d7bafbff08..c72622d190 100644 --- a/src/modules/processes/processes.c +++ b/src/modules/processes/processes.c @@ -44,6 +44,7 @@ void ffParseProcessesJsonObject(FFProcessesOptions* options, yyjson_val* module) } } +[[gnu::cold]] void ffGenerateProcessesJsonConfig(FFProcessesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); yyjson_mut_obj_add_bool(doc, module, "countKprocs", options->countKprocs); @@ -84,6 +85,7 @@ FFModuleBaseInfo ffProcessesModuleInfo = { .cs = "Procesy", .de = "Prozesse", .es = "Procesos", + .fi = "Prosessit", .fr = "Processus", .gl = "Procesos", .he = "תהליכים", diff --git a/src/modules/publicip/publicip.c b/src/modules/publicip/publicip.c index 6e4f30429d..0de7f7e732 100644 --- a/src/modules/publicip/publicip.c +++ b/src/modules/publicip/publicip.c @@ -62,6 +62,7 @@ void ffParsePublicIpJsonObject(FFPublicIPOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGeneratePublicIpJsonConfig(FFPublicIPOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -116,6 +117,7 @@ FFModuleBaseInfo ffPublicIPModuleInfo = { .cs = "Veřejná IP", .de = "Öffentliche IP", .es = "IP pública", + .fi = "Julkinen IP", .fr = "IP publique", .gl = "IP pública", .he = "IP ציבורי", diff --git a/src/modules/separator/separator.c b/src/modules/separator/separator.c index 0ef3e41eee..c8ac4b337a 100644 --- a/src/modules/separator/separator.c +++ b/src/modules/separator/separator.c @@ -106,6 +106,7 @@ void ffParseSeparatorJsonObject(FFSeparatorOptions* options, yyjson_val* module) } } +[[gnu::cold]] void ffGenerateSeparatorJsonConfig(FFSeparatorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { yyjson_mut_obj_add_strbuf(doc, module, "string", &options->string); yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &options->outputColor); @@ -132,6 +133,7 @@ FFModuleBaseInfo ffSeparatorModuleInfo = { .cs = "Oddělovač", .de = "Trennlinie", .es = "Separador", + .fi = "Erotin", .fr = "Séparateur", .gl = "Separador", .he = "מפריד", diff --git a/src/modules/shell/shell.c b/src/modules/shell/shell.c index 849a905bec..dec838d053 100644 --- a/src/modules/shell/shell.c +++ b/src/modules/shell/shell.c @@ -50,6 +50,7 @@ void ffParseShellJsonObject(FFShellOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateShellJsonConfig(FFShellOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -97,6 +98,7 @@ FFModuleBaseInfo ffShellModuleInfo = { .cs = "Shell", .de = "Shell", .es = "Shell", + .fi = "Komentotulkki", .fr = "Shell", .gl = "Shell", .he = "מעטפת", diff --git a/src/modules/sound/sound.c b/src/modules/sound/sound.c index 4f51fa368f..7a90e59b00 100644 --- a/src/modules/sound/sound.c +++ b/src/modules/sound/sound.c @@ -127,6 +127,7 @@ void ffParseSoundJsonObject(FFSoundOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateSoundJsonConfig(FFSoundOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -206,6 +207,7 @@ FFModuleBaseInfo ffSoundModuleInfo = { .cs = "Zvuk", .de = "Sound", .es = "Sonido", + .fi = "Ääni", .fr = "Son", .gl = "Son", .he = "קול", diff --git a/src/modules/swap/swap.c b/src/modules/swap/swap.c index 803db70690..4cdb4d3eb1 100644 --- a/src/modules/swap/swap.c +++ b/src/modules/swap/swap.c @@ -141,6 +141,7 @@ void ffParseSwapJsonObject(FFSwapOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateSwapJsonConfig(FFSwapOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffPercentGenerateJsonConfig(doc, module, options->percent); @@ -191,6 +192,7 @@ FFModuleBaseInfo ffSwapModuleInfo = { .cs = "Odkládací prostor", .de = "Swap", .es = "Swap", + .fi = "Swap", .fr = "Swap", .gl = "Swap", .he = "זיכרון החלפה", diff --git a/src/modules/terminal/terminal.c b/src/modules/terminal/terminal.c index b13e32e9e2..e80b66beed 100644 --- a/src/modules/terminal/terminal.c +++ b/src/modules/terminal/terminal.c @@ -48,6 +48,7 @@ void ffParseTerminalJsonObject(FFTerminalOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateTerminalJsonConfig(FFTerminalOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -91,6 +92,7 @@ FFModuleBaseInfo ffTerminalModuleInfo = { .cs = "Terminál", .de = "Terminal", .es = "Terminal", + .fi = "Terminaali", .fr = "Terminal", .gl = "Terminal", .he = "טרמינל", diff --git a/src/modules/terminalfont/terminalfont.c b/src/modules/terminalfont/terminalfont.c index ecfb58548a..600af6a25e 100644 --- a/src/modules/terminalfont/terminalfont.c +++ b/src/modules/terminalfont/terminalfont.c @@ -52,6 +52,7 @@ void ffParseTerminalFontJsonObject(FFTerminalFontOptions* options, yyjson_val* m } } +[[gnu::cold]] void ffGenerateTerminalFontJsonConfig(FFTerminalFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -111,6 +112,7 @@ FFModuleBaseInfo ffTerminalFontModuleInfo = { .cs = "Písmo terminálu", .de = "Terminalschriftart", .es = "Fuente del terminal", + .fi = "Terminaalin Fontti", .fr = "Police du terminal", .gl = "Fonte do terminal", .he = "גופן טרמינל", diff --git a/src/modules/terminalsize/terminalsize.c b/src/modules/terminalsize/terminalsize.c index 23f2099d0a..74d5f5078f 100644 --- a/src/modules/terminalsize/terminalsize.c +++ b/src/modules/terminalsize/terminalsize.c @@ -44,6 +44,7 @@ void ffParseTerminalSizeJsonObject(FFTerminalSizeOptions* options, yyjson_val* m } } +[[gnu::cold]] void ffGenerateTerminalSizeJsonConfig(FFTerminalSizeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -82,6 +83,7 @@ FFModuleBaseInfo ffTerminalSizeModuleInfo = { .cs = "Velikost terminálu", .de = "Terminalgröße", .es = "Tamaño del terminal", + .fi = "Terminaalin Koko", .fr = "Taille du terminal", .gl = "Tamaño do terminal", .he = "גודל טרמינל", diff --git a/src/modules/terminaltheme/terminaltheme.c b/src/modules/terminaltheme/terminaltheme.c index a3daf4f428..abb2feb28a 100644 --- a/src/modules/terminaltheme/terminaltheme.c +++ b/src/modules/terminaltheme/terminaltheme.c @@ -53,6 +53,7 @@ void ffParseTerminalThemeJsonObject(FFTerminalThemeOptions* options, yyjson_val* } } +[[gnu::cold]] void ffGenerateTerminalThemeJsonConfig(FFTerminalThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -99,6 +100,7 @@ FFModuleBaseInfo ffTerminalThemeModuleInfo = { .cs = "Motiv terminálu", .de = "Terminalthema", .es = "Tema del terminal", + .fi = "Terminaalin Teema", .fr = "Thème du terminal", .gl = "Tema do terminal", .he = "ערכת נושא של טרמינל", diff --git a/src/modules/theme/theme.c b/src/modules/theme/theme.c index 1f79179308..883acd57d1 100644 --- a/src/modules/theme/theme.c +++ b/src/modules/theme/theme.c @@ -52,6 +52,7 @@ void ffParseThemeJsonObject(FFThemeOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateThemeJsonConfig(FFThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -95,6 +96,7 @@ FFModuleBaseInfo ffThemeModuleInfo = { .cs = "Motiv", .de = "Thema", .es = "Tema", + .fi = "Teema", .fr = "Thème", .gl = "Tema", .he = "ערכת נושא", diff --git a/src/modules/title/title.c b/src/modules/title/title.c index b5a7acacee..aedf829010 100644 --- a/src/modules/title/title.c +++ b/src/modules/title/title.c @@ -128,6 +128,7 @@ void ffParseTitleJsonObject(FFTitleOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateTitleJsonConfig(FFTitleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -184,6 +185,7 @@ FFModuleBaseInfo ffTitleModuleInfo = { .cs = "Titulek", .de = "Titel", .es = "Título", + .fi = "Otsikko", .fr = "Titre", .gl = "Título", .he = "כותרת", diff --git a/src/modules/top/top.c b/src/modules/top/top.c index 02d8116176..1584225b16 100644 --- a/src/modules/top/top.c +++ b/src/modules/top/top.c @@ -174,6 +174,7 @@ void ffParseTopJsonObject(FFTopOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateTopJsonConfig(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); yyjson_mut_obj_add_str(doc, module, "type", "top"); @@ -271,6 +272,7 @@ FFModuleBaseInfo ffTopModuleInfo = { .cs = "Nejvytíženější procesy", .de = "Top-Prozesse", .es = "Procesos principales", + .fi = "Raskaimmat Prosessit", .fr = "Processus principaux", .gl = "Procesos principais", .he = "התהליכים המובילים", @@ -281,7 +283,7 @@ FFModuleBaseInfo ffTopModuleInfo = { .pl = "Najaktywniejsze procesy", .pt = "Processos principais", .ru = "Топ процессов", - .tr = "En yoğun işlemler", + .tr = "En Yoğun Süreçler", .uk = "Найактивніші процеси", .vi = "Tiến trình hàng đầu", .zh_CN = "进程排行", diff --git a/src/modules/tpm/tpm.c b/src/modules/tpm/tpm.c index 726d60ae65..46793e9e94 100644 --- a/src/modules/tpm/tpm.c +++ b/src/modules/tpm/tpm.c @@ -48,6 +48,7 @@ void ffParseTPMJsonObject(FFTPMOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateTPMJsonConfig(FFTPMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -91,6 +92,7 @@ FFModuleBaseInfo ffTPMModuleInfo = { .cs = "TPM", .de = "TPM", .es = "TPM", + .fi = "TPM", .fr = "TPM", .gl = "TPM", .he = "TPM", diff --git a/src/modules/uptime/uptime.c b/src/modules/uptime/uptime.c index 25b8d1f45e..3a68c46963 100644 --- a/src/modules/uptime/uptime.c +++ b/src/modules/uptime/uptime.c @@ -54,6 +54,7 @@ void ffParseUptimeJsonObject(FFUptimeOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateUptimeJsonConfig(FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -91,6 +92,7 @@ FFModuleBaseInfo ffUptimeModuleInfo = { .cs = "Doba běhu", .de = "Betriebszeit", .es = "Tiempo de actividad", + .fi = "Käynnissäoloaika", .fr = "Temps de fonctionnement", .gl = "Tempo de actividade", .he = "זמן פעילות", diff --git a/src/modules/users/users.c b/src/modules/users/users.c index 0b84901b8d..2083cde9a6 100644 --- a/src/modules/users/users.c +++ b/src/modules/users/users.c @@ -125,6 +125,7 @@ void ffParseUsersJsonObject(FFUsersOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateUsersJsonConfig(FFUsersOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -188,6 +189,7 @@ FFModuleBaseInfo ffUsersModuleInfo = { .cs = "Uživatelé", .de = "Benutzer", .es = "Usuarios", + .fi = "Käyttäjät", .fr = "Utilisateurs", .gl = "Usuarios", .he = "משתמשים", diff --git a/src/modules/version/version.c b/src/modules/version/version.c index c744343fb1..f8a368fb9b 100644 --- a/src/modules/version/version.c +++ b/src/modules/version/version.c @@ -52,6 +52,7 @@ void ffParseVersionJsonObject(FFVersionOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateVersionJsonConfig(FFVersionOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -102,6 +103,7 @@ FFModuleBaseInfo ffVersionModuleInfo = { .cs = "Verze", .de = "Version", .es = "Versión", + .fi = "Versio", .fr = "Version", .gl = "Versión", .he = "גרסה", diff --git a/src/modules/vulkan/vulkan.c b/src/modules/vulkan/vulkan.c index c8586b6f56..e015ddba55 100644 --- a/src/modules/vulkan/vulkan.c +++ b/src/modules/vulkan/vulkan.c @@ -58,6 +58,7 @@ void ffParseVulkanJsonObject(FFVulkanOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateVulkanJsonConfig(FFVulkanOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -139,6 +140,7 @@ FFModuleBaseInfo ffVulkanModuleInfo = { .cs = "Vulkan", .de = "Vulkan", .es = "Vulkan", + .fi = "Vulkan", .fr = "Vulkan", .gl = "Vulkan", .he = "Vulkan", diff --git a/src/modules/wallpaper/wallpaper.c b/src/modules/wallpaper/wallpaper.c index 38c7b204a5..5904ff1f4e 100644 --- a/src/modules/wallpaper/wallpaper.c +++ b/src/modules/wallpaper/wallpaper.c @@ -50,6 +50,7 @@ void ffParseWallpaperJsonObject(FFWallpaperOptions* options, yyjson_val* module) } } +[[gnu::cold]] void ffGenerateWallpaperJsonConfig(FFWallpaperOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -83,6 +84,7 @@ FFModuleBaseInfo ffWallpaperModuleInfo = { .cs = "Tapeta", .de = "Hintergrundbild", .es = "Fondo de pantalla", + .fi = "Taustakuva", .fr = "Fond d'écran", .gl = "Fondo de escritorio", .he = "טפט", diff --git a/src/modules/weather/weather.c b/src/modules/weather/weather.c index ac9dc229ce..63e4fbf3e7 100644 --- a/src/modules/weather/weather.c +++ b/src/modules/weather/weather.c @@ -52,6 +52,7 @@ void ffParseWeatherJsonObject(FFWeatherOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateWeatherJsonConfig(FFWeatherOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -100,6 +101,7 @@ FFModuleBaseInfo ffWeatherModuleInfo = { .cs = "Počasí", .de = "Wetter", .es = "Tiempo", + .fi = "Sää", .fr = "Météo", .gl = "Tempo", .he = "מזג אוויר", diff --git a/src/modules/wifi/wifi.c b/src/modules/wifi/wifi.c index ee723c497b..5ac0731168 100644 --- a/src/modules/wifi/wifi.c +++ b/src/modules/wifi/wifi.c @@ -143,6 +143,7 @@ void ffParseWifiJsonObject(FFWifiOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateWifiJsonConfig(FFWifiOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -223,6 +224,7 @@ FFModuleBaseInfo ffWifiModuleInfo = { .cs = "Wi-Fi", .de = "WLAN", .es = "Wi-Fi", + .fi = "Wi-Fi", .fr = "Wi-Fi", .gl = "Wi-Fi", .he = "Wi-Fi", diff --git a/src/modules/wm/wm.c b/src/modules/wm/wm.c index c094e45c6b..5dc181d6d5 100644 --- a/src/modules/wm/wm.c +++ b/src/modules/wm/wm.c @@ -76,6 +76,7 @@ void ffParseWMJsonObject(FFWMOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateWMJsonConfig(FFWMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -128,6 +129,7 @@ FFModuleBaseInfo ffWMModuleInfo = { .cs = "Správce oken", .de = "Fenstermanager", .es = "Gestor de ventanas", + .fi = "Ikkunointijärjestelmä", .fr = "Gestionnaire de fenêtres", .gl = "Xestor de xanelas", .he = "מנהל חלונות", diff --git a/src/modules/wmtheme/wmtheme.c b/src/modules/wmtheme/wmtheme.c index 89caff3888..aad22f0f33 100644 --- a/src/modules/wmtheme/wmtheme.c +++ b/src/modules/wmtheme/wmtheme.c @@ -35,6 +35,7 @@ void ffParseWMThemeJsonObject(FFWMThemeOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateWMThemeJsonConfig(FFWMThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); } @@ -67,6 +68,7 @@ FFModuleBaseInfo ffWMThemeModuleInfo = { .cs = "Motiv správce oken", .de = "Fenstermanager-Thema", .es = "Tema del gestor de ventanas", + .fi = "Ikkunointijärjestelmän Teema", .fr = "Thème du gestionnaire de fenêtres", .gl = "Tema do xestor de xanelas", .he = "ערכת נושא של מנהל חלונות", diff --git a/src/modules/zpool/zpool.c b/src/modules/zpool/zpool.c index a1dd310325..72e10ef6de 100644 --- a/src/modules/zpool/zpool.c +++ b/src/modules/zpool/zpool.c @@ -143,6 +143,7 @@ void ffParseZpoolJsonObject(FFZpoolOptions* options, yyjson_val* module) { } } +[[gnu::cold]] void ffGenerateZpoolJsonConfig(FFZpoolOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); @@ -201,6 +202,7 @@ FFModuleBaseInfo ffZpoolModuleInfo = { .cs = "Zpool", .de = "Zpool", .es = "Zpool", + .fi = "Zpool", .fr = "Zpool", .gl = "Zpool", .he = "Zpool", diff --git a/src/options/display.c b/src/options/display.c index f764e2d7a0..25d807efe8 100644 --- a/src/options/display.c +++ b/src/options/display.c @@ -69,6 +69,7 @@ static uint32_t optionParseLanguageString(FFstrbuf* language) { case 'cs': return offsetof(FFModuleDisplayName, cs); case 'de': return offsetof(FFModuleDisplayName, de); case 'es': return offsetof(FFModuleDisplayName, es); + case 'fi': return offsetof(FFModuleDisplayName, fi); case 'fr': return offsetof(FFModuleDisplayName, fr); case 'gl': return offsetof(FFModuleDisplayName, gl); case 'he': return offsetof(FFModuleDisplayName, he); @@ -1037,6 +1038,49 @@ void ffOptionsDestroyDisplay(FFOptionsDisplay* options) { ffListDestroy(&options->constants); } +// `names` is indexed by the enum value, so its order must match the enum declaration. A value outside +// the array adds no key at all, which is what the exhaustive switches this replaces did. +static void optionAddEnumString(yyjson_mut_doc* doc, yyjson_mut_val* obj, const char* key, const char* const names[], size_t count, uint32_t value) { + if (value < count) { + yyjson_mut_obj_add_str(doc, obj, key, names[value]); + } +} + +static void optionAddSpaceBeforeUnit(yyjson_mut_doc* doc, yyjson_mut_val* obj, FFSpaceBeforeUnitType type) { + static const char* const names[] = {"default", "always", "never"}; + optionAddEnumString(doc, obj, "spaceBeforeUnit", names, sizeof(names) / sizeof(*names), type); +} + +// @returns the language code for offsetof(FFModuleDisplayName, ), or nullptr if unknown +// @see optionParseLanguageString +static const char* optionGetLanguageString(uint32_t offset) { + switch (offset) { + case offsetof(FFModuleDisplayName, en): return "en"; + case offsetof(FFModuleDisplayName, ar): return "ar"; + case offsetof(FFModuleDisplayName, cs): return "cs"; + case offsetof(FFModuleDisplayName, de): return "de"; + case offsetof(FFModuleDisplayName, es): return "es"; + case offsetof(FFModuleDisplayName, fi): return "fi"; + case offsetof(FFModuleDisplayName, fr): return "fr"; + case offsetof(FFModuleDisplayName, gl): return "gl"; + case offsetof(FFModuleDisplayName, he): return "he"; + case offsetof(FFModuleDisplayName, id): return "id"; + case offsetof(FFModuleDisplayName, it): return "it"; + case offsetof(FFModuleDisplayName, ja): return "ja"; + case offsetof(FFModuleDisplayName, ko): return "ko"; + case offsetof(FFModuleDisplayName, pl): return "pl"; + case offsetof(FFModuleDisplayName, pt): return "pt"; + case offsetof(FFModuleDisplayName, ru): return "ru"; + case offsetof(FFModuleDisplayName, tr): return "tr"; + case offsetof(FFModuleDisplayName, uk): return "uk"; + case offsetof(FFModuleDisplayName, vi): return "vi"; + case offsetof(FFModuleDisplayName, zh_CN): return "zh_CN"; + case offsetof(FFModuleDisplayName, zh_TW): return "zh_TW"; + } + return nullptr; +} + +[[gnu::cold]] void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) { yyjson_mut_doc* doc = data->resultDoc; yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, doc->root, "display"); @@ -1070,20 +1114,11 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) { yyjson_mut_val* duration = yyjson_mut_obj_add_obj(doc, obj, "duration"); yyjson_mut_obj_add_bool(doc, duration, "abbreviation", options->durationAbbreviation); - switch (options->durationSpaceBeforeUnit) { - case FF_SPACE_BEFORE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, duration, "spaceBeforeUnit", "default"); - break; - case FF_SPACE_BEFORE_UNIT_ALWAYS: - yyjson_mut_obj_add_str(doc, duration, "spaceBeforeUnit", "always"); - break; - case FF_SPACE_BEFORE_UNIT_NEVER: - yyjson_mut_obj_add_str(doc, duration, "spaceBeforeUnit", "never"); - break; - } + optionAddSpaceBeforeUnit(doc, duration, options->durationSpaceBeforeUnit); } { + static const char* const binaryPrefixNames[] = {"iec", "si", "jedec"}; yyjson_mut_val* size = yyjson_mut_obj_add_obj(doc, obj, "size"); yyjson_mut_obj_add_str(doc, size, "maxPrefix", ((const char*[]) { "B", @@ -1096,47 +1131,15 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) "ZB", "YB", })[options->sizeMaxPrefix]); - switch (options->sizeBinaryPrefix) { - case FF_SIZE_BINARY_PREFIX_TYPE_IEC: - yyjson_mut_obj_add_str(doc, size, "binaryPrefix", "iec"); - break; - case FF_SIZE_BINARY_PREFIX_TYPE_SI: - yyjson_mut_obj_add_str(doc, size, "binaryPrefix", "si"); - break; - case FF_SIZE_BINARY_PREFIX_TYPE_JEDEC: - yyjson_mut_obj_add_str(doc, size, "binaryPrefix", "jedec"); - break; - } + optionAddEnumString(doc, size, "binaryPrefix", binaryPrefixNames, sizeof(binaryPrefixNames) / sizeof(*binaryPrefixNames), options->sizeBinaryPrefix); yyjson_mut_obj_add_uint(doc, size, "ndigits", options->sizeNdigits); - switch (options->sizeSpaceBeforeUnit) { - case FF_SPACE_BEFORE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, size, "spaceBeforeUnit", "default"); - break; - case FF_SPACE_BEFORE_UNIT_ALWAYS: - yyjson_mut_obj_add_str(doc, size, "spaceBeforeUnit", "always"); - break; - case FF_SPACE_BEFORE_UNIT_NEVER: - yyjson_mut_obj_add_str(doc, size, "spaceBeforeUnit", "never"); - break; - } + optionAddSpaceBeforeUnit(doc, size, options->sizeSpaceBeforeUnit); } { + static const char* const tempUnitNames[] = {"D", "C", "F", "K"}; yyjson_mut_val* temperature = yyjson_mut_obj_add_obj(doc, obj, "temp"); - switch (options->tempUnit) { - case FF_TEMPERATURE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, temperature, "unit", "D"); - break; - case FF_TEMPERATURE_UNIT_CELSIUS: - yyjson_mut_obj_add_str(doc, temperature, "unit", "C"); - break; - case FF_TEMPERATURE_UNIT_FAHRENHEIT: - yyjson_mut_obj_add_str(doc, temperature, "unit", "F"); - break; - case FF_TEMPERATURE_UNIT_KELVIN: - yyjson_mut_obj_add_str(doc, temperature, "unit", "K"); - break; - } + optionAddEnumString(doc, temperature, "unit", tempUnitNames, sizeof(tempUnitNames) / sizeof(*tempUnitNames), options->tempUnit); yyjson_mut_obj_add_uint(doc, temperature, "ndigits", options->tempNdigits); { yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, temperature, "color"); @@ -1144,17 +1147,7 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) yyjson_mut_obj_add_strbuf(doc, color, "yellow", &options->tempColorYellow); yyjson_mut_obj_add_strbuf(doc, color, "red", &options->tempColorRed); } - switch (options->tempSpaceBeforeUnit) { - case FF_SPACE_BEFORE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, temperature, "spaceBeforeUnit", "default"); - break; - case FF_SPACE_BEFORE_UNIT_ALWAYS: - yyjson_mut_obj_add_str(doc, temperature, "spaceBeforeUnit", "always"); - break; - case FF_SPACE_BEFORE_UNIT_NEVER: - yyjson_mut_obj_add_str(doc, temperature, "spaceBeforeUnit", "never"); - break; - } + optionAddSpaceBeforeUnit(doc, temperature, options->tempSpaceBeforeUnit); } { @@ -1184,17 +1177,7 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) yyjson_mut_obj_add_strbuf(doc, color, "yellow", &options->percentColorYellow); yyjson_mut_obj_add_strbuf(doc, color, "red", &options->percentColorRed); } - switch (options->percentSpaceBeforeUnit) { - case FF_SPACE_BEFORE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, percent, "spaceBeforeUnit", "default"); - break; - case FF_SPACE_BEFORE_UNIT_ALWAYS: - yyjson_mut_obj_add_str(doc, percent, "spaceBeforeUnit", "always"); - break; - case FF_SPACE_BEFORE_UNIT_NEVER: - yyjson_mut_obj_add_str(doc, percent, "spaceBeforeUnit", "never"); - break; - } + optionAddSpaceBeforeUnit(doc, percent, options->percentSpaceBeforeUnit); yyjson_mut_obj_add_uint(doc, percent, "width", options->percentWidth); } @@ -1220,6 +1203,7 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) } { + static const char* const trailingZerosNames[] = {"default", "always", "never"}; yyjson_mut_val* fraction = yyjson_mut_obj_add_obj(doc, obj, "fraction"); if (options->fractionNdigits < 0) { @@ -1228,17 +1212,7 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) yyjson_mut_obj_add_uint(doc, fraction, "ndigits", (uint8_t) options->fractionNdigits); } - switch (options->fractionTrailingZeros) { - case FF_FRACTION_TRAILING_ZEROS_TYPE_DEFAULT: - yyjson_mut_obj_add_str(doc, fraction, "trailingZeros", "default"); - break; - case FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS: - yyjson_mut_obj_add_str(doc, fraction, "trailingZeros", "always"); - break; - case FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER: - yyjson_mut_obj_add_str(doc, fraction, "trailingZeros", "never"); - break; - } + optionAddEnumString(doc, fraction, "trailingZeros", trailingZerosNames, sizeof(trailingZerosNames) / sizeof(*trailingZerosNames), options->fractionTrailingZeros); } yyjson_mut_obj_add_bool(doc, obj, "noBuffer", options->noBuffer); @@ -1275,84 +1249,16 @@ void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) yyjson_mut_obj_add_uint(doc, key, "paddingLeft", options->keyPaddingLeft); - switch (options->keyLanguage) { - case offsetof(FFModuleDisplayName, en): - yyjson_mut_obj_add_str(doc, key, "language", "en"); - break; - case offsetof(FFModuleDisplayName, ar): - yyjson_mut_obj_add_str(doc, key, "language", "ar"); - break; - case offsetof(FFModuleDisplayName, cs): - yyjson_mut_obj_add_str(doc, key, "language", "cs"); - break; - case offsetof(FFModuleDisplayName, de): - yyjson_mut_obj_add_str(doc, key, "language", "de"); - break; - case offsetof(FFModuleDisplayName, es): - yyjson_mut_obj_add_str(doc, key, "language", "es"); - break; - case offsetof(FFModuleDisplayName, fr): - yyjson_mut_obj_add_str(doc, key, "language", "fr"); - break; - case offsetof(FFModuleDisplayName, gl): - yyjson_mut_obj_add_str(doc, key, "language", "gl"); - break; - case offsetof(FFModuleDisplayName, he): - yyjson_mut_obj_add_str(doc, key, "language", "he"); - break; - case offsetof(FFModuleDisplayName, id): - yyjson_mut_obj_add_str(doc, key, "language", "id"); - break; - case offsetof(FFModuleDisplayName, it): - yyjson_mut_obj_add_str(doc, key, "language", "it"); - break; - case offsetof(FFModuleDisplayName, ja): - yyjson_mut_obj_add_str(doc, key, "language", "ja"); - break; - case offsetof(FFModuleDisplayName, ko): - yyjson_mut_obj_add_str(doc, key, "language", "ko"); - break; - case offsetof(FFModuleDisplayName, pl): - yyjson_mut_obj_add_str(doc, key, "language", "pl"); - break; - case offsetof(FFModuleDisplayName, pt): - yyjson_mut_obj_add_str(doc, key, "language", "pt"); - break; - case offsetof(FFModuleDisplayName, ru): - yyjson_mut_obj_add_str(doc, key, "language", "ru"); - break; - case offsetof(FFModuleDisplayName, tr): - yyjson_mut_obj_add_str(doc, key, "language", "tr"); - break; - case offsetof(FFModuleDisplayName, uk): - yyjson_mut_obj_add_str(doc, key, "language", "uk"); - break; - case offsetof(FFModuleDisplayName, vi): - yyjson_mut_obj_add_str(doc, key, "language", "vi"); - break; - case offsetof(FFModuleDisplayName, zh_CN): - yyjson_mut_obj_add_str(doc, key, "language", "zh_CN"); - break; - case offsetof(FFModuleDisplayName, zh_TW): - yyjson_mut_obj_add_str(doc, key, "language", "zh_TW"); - break; + const char* language = optionGetLanguageString(options->keyLanguage); + if (language != nullptr) { + yyjson_mut_obj_add_str(doc, key, "language", language); } } { yyjson_mut_val* freq = yyjson_mut_obj_add_obj(doc, obj, "freq"); yyjson_mut_obj_add_int(doc, freq, "ndigits", options->freqNdigits); - switch (options->freqSpaceBeforeUnit) { - case FF_SPACE_BEFORE_UNIT_DEFAULT: - yyjson_mut_obj_add_str(doc, freq, "spaceBeforeUnit", "default"); - break; - case FF_SPACE_BEFORE_UNIT_ALWAYS: - yyjson_mut_obj_add_str(doc, freq, "spaceBeforeUnit", "always"); - break; - case FF_SPACE_BEFORE_UNIT_NEVER: - yyjson_mut_obj_add_str(doc, freq, "spaceBeforeUnit", "never"); - break; - } + optionAddSpaceBeforeUnit(doc, freq, options->freqSpaceBeforeUnit); } { diff --git a/src/options/general.c b/src/options/general.c index 890f38e7d3..b3a29a16de 100644 --- a/src/options/general.c +++ b/src/options/general.c @@ -134,6 +134,7 @@ void ffOptionsDestroyGeneral(FFOptionsGeneral* options) { ffStrbufDestroy(&options->playerName); } +[[gnu::cold]] void ffOptionsGenerateGeneralJsonConfig(FFdata* data, FFOptionsGeneral* options) { yyjson_mut_doc* doc = data->resultDoc; yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, doc->root, "general"); diff --git a/src/options/logo.c b/src/options/logo.c index a46d1d728a..bc667c7e03 100644 --- a/src/options/logo.c +++ b/src/options/logo.c @@ -515,6 +515,7 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo return nullptr; } +[[gnu::cold]] void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { yyjson_mut_doc* doc = data->resultDoc; yyjson_mut_val* obj = yyjson_mut_obj(doc); diff --git a/tests/cpu.c b/tests/cpu.c deleted file mode 100644 index 180380ca24..0000000000 --- a/tests/cpu.c +++ /dev/null @@ -1,47 +0,0 @@ -#include "common/FFstrbuf.h" -#include "common/textModifier.h" - -#include -#include - -bool ffCPUQualcommSnapdragonToName(FFstrbuf* name, const char* model); - - -static void verifyName(const char* model, bool expectedResult, const char* expected) { - FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("unchanged"); - const bool returned = ffCPUQualcommSnapdragonToName(&result, model); - - if (returned != expectedResult || !ffStrbufEqualS(&result, expected)) { - fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR - "ffCPUQualcommSnapdragonToName(\"%s\"): expected %s \"%s\", got %s \"%s\"\n" - FASTFETCH_TEXT_MODIFIER_RESET, - model, expectedResult ? "true" : "false", expected, - returned ? "true" : "false", result.chars); - exit(1); - } -} - -#define VERIFY_NAME(model, expectedResult, expected) \ - verifyName((model), (expectedResult), (expected)) - -int main(void) { - // Existing X1 and direct X2 model names - VERIFY_NAME("x1e80100", true, "Qualcomm Snapdragon X Elite X1E-80-100"); - VERIFY_NAME("x1p42100", true, "Qualcomm Snapdragon X Plus X1P-42-100"); - VERIFY_NAME("x2e88100", true, "Qualcomm Snapdragon X2 Elite X2E-88-100"); - VERIFY_NAME("x2e90100", true, "Qualcomm Snapdragon X2 Elite X2E-90-100"); - VERIFY_NAME("x2e94100", true, "Qualcomm Snapdragon X2 Elite Extreme X2E-94-100"); - VERIFY_NAME("x2e96100", true, "Qualcomm Snapdragon X2 Elite Extreme X2E-96-100"); - VERIFY_NAME("x2p42100", true, "Qualcomm Snapdragon X2 Plus X2P-42-100"); - - // X2 platform codenames identify the marketing family, not an exact SKU - VERIFY_NAME("glymur", true, "Qualcomm Snapdragon X2 Elite Extreme"); - VERIFY_NAME("mahua", true, "Qualcomm Snapdragon X2 Elite"); - VERIFY_NAME("kalambo", true, "Qualcomm Snapdragon X2 Plus"); - - // Other Qualcomm platform names are handled by the caller - VERIFY_NAME("sc8280", false, "unchanged"); - - puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); - return 0; -} diff --git a/tests/gpu_pci.c b/tests/gpu_pci.c new file mode 100644 index 0000000000..a7c430a9eb --- /dev/null +++ b/tests/gpu_pci.c @@ -0,0 +1,201 @@ +// Tests for ffGPUParsePciIdsFile(), the pci.ids lookup that resolves a PCI GPU name on Linux and +// the BSDs. +// +// The parser takes the file content as a buffer instead of opening the file itself, which is +// exactly what makes it testable: a small hand written pci.ids excerpt is enough, and the +// expectations stay readable. +// +// The function is only compiled where libfastfetch ships the pci.ids based detection, so on other +// platforms this test has nothing to link against and only reports success. + +#include "common/textModifier.h" +#include "detection/gpu/gpu.h" + +#include +#include + +#if FF_GPU_HAS_PCI_IDS + +static void verifyPciEx( + const char* content, uint8_t subclass, uint16_t vendor, uint16_t device, + const char* presetVendor, const char* presetName, + const char* expectedVendor, const char* expectedName, + int lineNo +) { + FFGPUResult gpu = {}; + ffStrbufInit(&gpu.vendor); + ffStrbufInit(&gpu.name); + + // A caller may already know the vendor (for example from the PCI subsystem), in which case the + // parser must not clobber it. + if (presetVendor) { + ffStrbufSetS(&gpu.vendor, presetVendor); + } + if (presetName) { + ffStrbufSetS(&gpu.name, presetName); + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS(content); + ffGPUParsePciIdsFile(&buffer, subclass, vendor, device, &gpu); + + if (!ffStrbufEqualS(&gpu.vendor, expectedVendor) || !ffStrbufEqualS(&gpu.name, expectedName)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffGPUParsePciIdsFile(vendor=0x%04x, device=0x%04x, subclass=%u): expected vendor=\"%s\" name=\"%s\", got vendor=\"%s\" name=\"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, vendor, device, (unsigned) subclass, expectedVendor, expectedName, gpu.vendor.chars, gpu.name.chars); + exit(1); + } + + ffStrbufDestroy(&gpu.vendor); + ffStrbufDestroy(&gpu.name); +} + +#define VERIFY_PCI(content, subclass, vendor, device, expectedVendor, expectedName) \ + verifyPciEx((content), (subclass), (vendor), (device), nullptr, nullptr, (expectedVendor), (expectedName), __LINE__) + +#define VERIFY_PCI_EX(content, subclass, vendor, device, presetVendor, presetName, expectedVendor, expectedName) \ + verifyPciEx((content), (subclass), (vendor), (device), (presetVendor), (presetName), (expectedVendor), (expectedName), __LINE__) + +// A miniature pci.ids. Vendors are ordered, device ids grow within a block, and the block is closed +// by a blank line -- the same shape the real file has. +static const char PCI_IDS[] = + "# pci.ids excerpt used by the tests\n" + "#\n" + "1a2b Vendor Alpha\n" + "\t0001 Alpha One\n" + "\t0005 Alpha Five\n" + "\t\t0005 0abc Subsystem that must be ignored\n" + "\t00ff Alpha FF [Alias]\n" + "\t1000 Alpha Thousand\n" + "\n" + "1a2c Vendor Beta\n" + "\t0001 Beta One\n" + "\t0002 Beta Two\n" + "\t1234 Beta TwelveThirtyFour\n" + "\n" + "abcd Vendor Delta\n" + "\t0e1f Delta Device\n" + "\n" + "ffff Vendor NoDevices\n" + "\n" + "C 00 Unclassified device\n"; + +// The last line has no terminating newline, so the parser has to fall back to the end of the +// buffer. +static const char PCI_IDS_NO_TRAILING_NL[] = + "#\n" + "1a2b Vendor Alpha\n" + "\t0001 Alpha One"; + +// A vendor with no device lines at all, again without a terminating newline. +static const char PCI_IDS_VENDOR_ONLY[] = + "#\n" + "1a2b Vendor Alpha"; + +int main(void) { + // An empty buffer is not an error: the name is synthesized from the vendor and device id. + { + VERIFY_PCI("", 0, 0x1a2b, 0x0001, "", "Unknown Device 0001 (VGA compatible)"); + VERIFY_PCI("", 2, 0x1a2b, 0x0001, "", "Unknown Device 0001 (3D)"); + } + + // The happy path: vendor and device are both present. + { + VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0005, "Vendor Alpha", "Alpha Five"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1000, "Vendor Alpha", "Alpha Thousand"); + } + + // Hex letters in the ids work on both sides. + { + VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0x0e1f, "Vendor Delta", "Delta Device"); + } + + // A name of the form `... [Alias]` is reduced to the alias. Only the trailing bracket pair is + // considered, and a `[` without a matching `]` at the end is kept verbatim. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x00ff, "Vendor Alpha", "Alias"); + } + + // The lookup must stay inside the vendor block. `Beta` also has a 0x1234, but a lookup under + // `Alpha` must not find it -- `Alpha` has no 0x1234 of its own. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2c, 0x1234, "Vendor Beta", "Beta TwelveThirtyFour"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1234, "Vendor Alpha", "Vendor Alpha Device 1234 (VGA compatible)"); + } + + // An unknown device under a known vendor synthesizes a name from the vendor. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 1, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (XGA compatible)"); + VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (3D)"); + VERIFY_PCI(PCI_IDS, 5, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002"); + } + + // The synthesized name uses the upper case form of the device id, while the lookup needle is + // lower case. + { + VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0xbeef, "Vendor Delta", "Vendor Delta Device BEEF (VGA compatible)"); + } + + // A device id below the first entry, between two entries and above the last one all miss. The + // first case is also what the "stop once the ids grow past the target" shortcut relies on. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0000, "Vendor Alpha", "Vendor Alpha Device 0000 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0020, "Vendor Alpha", "Vendor Alpha Device 0020 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x9999, "Vendor Alpha", "Vendor Alpha Device 9999 (VGA compatible)"); + } + + // A subsystem id lives at twice the indentation and must never be mistaken for a device id, + // even when it is the value being searched for. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0abc, "Vendor Alpha", "Vendor Alpha Device 0ABC (VGA compatible)"); + } + + // A vendor with no device lines falls back to the synthesized name, and the vendor is still + // reported. + { + VERIFY_PCI(PCI_IDS, 0, 0xffff, 0x0001, "Vendor NoDevices", "Vendor NoDevices Device 0001 (VGA compatible)"); + } + + // An unknown vendor leaves the vendor buffer empty, so the synthesized name says "Unknown". + { + VERIFY_PCI(PCI_IDS, 0, 0x1234, 0x5678, "", "Unknown Device 5678 (VGA compatible)"); + } + + // Lines beyond the vendor section (the class list) are not reachable from a vendor block. + { + VERIFY_PCI(PCI_IDS, 0, 0x000c, 0x0000, "", "Unknown Device 0000 (VGA compatible)"); + } + + // A device line as the very last line of the buffer, with no trailing newline. + { + VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One"); + VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)"); + } + + // A vendor line as the very last line of the buffer, with no device lines after it. + { + VERIFY_PCI(PCI_IDS_VENDOR_ONLY, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Vendor Alpha Device 0001 (VGA compatible)"); + } + + // A known vendor is not overwritten, but resolving the device name still happens. + { + VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, "Custom Vendor", nullptr, "Custom Vendor", "Alpha One"); + } + + // Likewise a name the caller already resolved is kept when the entry is a plain one. + { + VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, nullptr, "Custom Name", "Vendor Alpha", "Custom Name"); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} + +#else // FF_TEST_HAS_PCI_IDS + +int main(void) { + puts("\033[32mTests skipped. (pci.ids parser not available on this platform)" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} + +#endif diff --git a/tests/percent.c b/tests/percent.c index e697a854dd..834ad143da 100644 --- a/tests/percent.c +++ b/tests/percent.c @@ -180,6 +180,17 @@ int main(void) { VERIFY_BAR(14, config, "[ ■--------- ]"); VERIFY_BAR(15, config, "[ ■■-------- ]"); + // A value above 100 saturates the bar. That is reachable from real data: the load average is + // normalized against the number of cores, so it exceeds 100% as soon as the load does. + VERIFY_BAR(101, config, "[ ■■■■■■■■■■ ]"); + VERIFY_BAR(150, config, "[ ■■■■■■■■■■ ]"); + + // Narrowing to `uint8_t` before clamping would wrap around here and silently render 4 blocks + VERIFY_BAR(2600, config, "[ ■■■■■■■■■■ ]"); + + // A negative value fills nothing + VERIFY_BAR(-60, config, "[ ---------- ]"); + options->barWidth = 4; VERIFY_BAR(0, config, "[ ---- ]"); VERIFY_BAR(25, config, "[ ■--- ]");