mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-29 19:20:43 +01:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b385f6ce8d | |||
| 957e0d08e2 | |||
| 6e2a25759d | |||
| 460a715901 | |||
| 9ac3822105 | |||
| e02e44f673 | |||
| 0f712b4eaf | |||
| 995d172d3e | |||
| 8102478f2f | |||
| 537ffac39c | |||
| 95bcc4da5b | |||
| 1b3594f600 | |||
| ae1288d087 | |||
| b201fd2db9 | |||
| 7f458b03e8 | |||
| 5f65515c3c | |||
| 3d0747b56d | |||
| 68c5dd31f5 | |||
| a57ebe992a | |||
| 54dcb1b6f9 | |||
| 39d667d1d5 | |||
| 7099964530 | |||
| ae4cbcbdba | |||
| cee27354ba | |||
| 109644ec77 | |||
| 782b7b8fd1 | |||
| a9bf53b4a1 | |||
| ab200636f1 | |||
| 4d8d6a5fe9 | |||
| ef853fe858 | |||
| 47fbfcb0e9 | |||
| c88ec3bcc3 | |||
| 2838dccbfd | |||
| f0d8c996e6 | |||
| 6ff7925f45 | |||
| ffbf4ec7ac | |||
| a3f46d9742 | |||
| 4fb57bb39b | |||
| cdbe8f54c6 | |||
| 80632920bb | |||
| 5246f149eb | |||
| f00a021e27 | |||
| 676dbcfee2 | |||
| 9801d89316 | |||
| fb52d32452 | |||
| 36dc82e926 | |||
| 7fdd1d8e93 | |||
| 0c3b852bf7 | |||
| 866c5e89c8 | |||
| a1edf7316c | |||
| c3164d15e1 | |||
| a2eda0090a | |||
| d8797ae314 | |||
| 93175aea1d | |||
| 537635cd9c | |||
| c68bedd9bf | |||
| 80a931a87f | |||
| d223b04ad8 | |||
| 0e04ebdf63 | |||
| bba45429ae | |||
| 00ccd8be12 | |||
| 42e4fe3085 | |||
| fb5622215b | |||
| ccf1dcfa6d | |||
| 0c6a9d6e98 | |||
| b6b5dabd54 | |||
| fdbaaea962 | |||
| add2339c9c | |||
| e1e2de0929 | |||
| 59f0f533e7 | |||
| 28f4f6464b | |||
| 982313063d | |||
| 1e7386788f | |||
| 3a9595e121 | |||
| f8df95d146 | |||
| 592ef68d60 | |||
| 0bf6c8df74 | |||
| be15a0942c | |||
| 4a97cc6825 | |||
| a78d2a7737 | |||
| 3930759007 | |||
| e21530551d | |||
| c26bace3c2 | |||
| fe49a9624e | |||
| 7f3209181e | |||
| 2f74d6be4d | |||
| 256c6a4257 | |||
| aaa9153f5a | |||
| f26795826c | |||
| 18b31ca875 | |||
| 17b98007b4 | |||
| 1023388d60 | |||
| 6842ae0757 | |||
| b726ee5a85 | |||
| fd08fdb9df | |||
| 203ccb31fa | |||
| 3503facd68 | |||
| b2d48a1908 | |||
| b803d1ac82 | |||
| eda38a37ba | |||
| 0b76d33f07 | |||
| 39564acde4 | |||
| 3bfc4ec674 | |||
| fea55b276e | |||
| bc51f1ba0c | |||
| 38e4ba290d | |||
| 24fb73e9b4 | |||
| f96a290f3b | |||
| 67da836a27 | |||
| 15e4bc998a | |||
| f9f368c1fb | |||
| 4a33a2c410 | |||
| 34639b61b3 | |||
| efbfda9cf8 | |||
| 6142077656 | |||
| 58692adabc | |||
| 665c472482 | |||
| 2594c01fc1 | |||
| e1d7360453 | |||
| 873a881dab | |||
| ecbf218d6e | |||
| 2fb7fabc4d | |||
| 811e634dc8 | |||
| 098b501d0e | |||
| 7baaae166f | |||
| 560a9f419d | |||
| 70d2dd37a6 | |||
| 97ac94eb44 | |||
| 6ff7f2772b | |||
| f8fd708423 | |||
| c23c98a85a | |||
| 43988a8d41 | |||
| 1b7cc1cd30 | |||
| 59e33150dd | |||
| e004c97ee0 | |||
| 9bac273165 | |||
| d1b747192f | |||
| b96d7e7bec | |||
| 7b28ad07d7 | |||
| 02dc6a4428 | |||
| 53e868667a | |||
| fbf79ac6fe | |||
| 416715a505 | |||
| ff3360e036 | |||
| 98ec0a674c | |||
| 6178256404 | |||
| 129bb7309e | |||
| 464adb3540 | |||
| 3d7ea720fb | |||
| a90ae0249d | |||
| 375d0c6b25 | |||
| e3b3b5dfc6 | |||
| 35ffdb3995 | |||
| e8350a0d6e | |||
| 98e9b6eef1 | |||
| d796945604 | |||
| cd5a2def9a | |||
| 8334035927 | |||
| 1ee8528f10 | |||
| ced49ab2a4 | |||
| ec8b9be354 | |||
| 19725cd2f7 | |||
| 20c155e5b4 | |||
| 61e3f6108c | |||
| eff8514536 | |||
| f37a5a2543 | |||
| 9a9f82b59e | |||
| 29818d2dca | |||
| 222fe7e3f9 | |||
| 7d0256fb85 | |||
| a70c37fdaf | |||
| 66e9e38f28 | |||
| 5d290cc3d5 | |||
| 2e542bea64 | |||
| c708b6f5cd | |||
| 6a275da8b1 | |||
| ecee5694fb | |||
| 4e78f1b703 | |||
| 62f78ca60c | |||
| 00af2d1df2 | |||
| 1128a75d8c | |||
| ce61783c4b | |||
| 20597bff00 | |||
| caf64f5e4d | |||
| 90a6e90b19 | |||
| e9f719f37a | |||
| 8c98bf1953 | |||
| 1c1136ebd1 | |||
| 2787d97973 | |||
| 2e06a73102 | |||
| 37c198d997 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: dragonflybsd
|
||||
version: '6.4.2'
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '15.1'
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: haiku
|
||||
version: 'r1beta6'
|
||||
@@ -29,7 +29,6 @@ jobs:
|
||||
run: uname -a
|
||||
|
||||
- name: Install dependencies
|
||||
shell: cpa.sh {0} --reboot
|
||||
run: pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua || pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua
|
||||
|
||||
- name: CMake configuration
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
|
||||
- name: list features
|
||||
run: ./fastfetch --list-features
|
||||
|
||||
@@ -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_PROCESSOR_OVERRIDE=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
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: set up QEMU for loong64
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,13 +26,30 @@ jobs:
|
||||
- name: uname -a
|
||||
run: uname -a
|
||||
|
||||
- name: install required packages
|
||||
- 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"
|
||||
sudo installer -pkg "$RUNNER_TEMP/MacPorts.pkg" -target /
|
||||
echo /opt/local/bin >> "$GITHUB_PATH"
|
||||
echo /opt/local/sbin >> "$GITHUB_PATH"
|
||||
export PATH=/opt/local/bin:/opt/local/sbin:$PATH
|
||||
sudo port selfupdate
|
||||
sudo port install vulkan-loader vulkan-headers MoltenVK ImageMagick7 chafa lua quickjs-ng jq
|
||||
|
||||
- name: install required packages with Homebrew
|
||||
if: inputs.arch == 'aarch64'
|
||||
run: |
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew update
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk imagemagick chafa lua quickjs-ng jq
|
||||
|
||||
- name: configure project
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On .
|
||||
run: |
|
||||
if [ "${{ inputs.arch }}" = "amd64" ]; then
|
||||
export CMAKE_PREFIX_PATH=/opt/local
|
||||
export PKG_CONFIG_PATH=/opt/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
|
||||
fi
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On .
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: netbsd
|
||||
version: '10.1'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: omnios
|
||||
version: 'r151058'
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Start VM
|
||||
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
|
||||
with:
|
||||
operating_system: openbsd
|
||||
version: '7.9'
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: run VM
|
||||
uses: vmactions/solaris-vm@315163f088b66e55bbcc45928bd224d4973b2312 # v1.3.8
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -116,6 +116,7 @@ jobs:
|
||||
|
||||
- if: github.event_name == 'push' && github.repository == 'fastfetch-cli/fastfetch'
|
||||
name: submit signing request
|
||||
# https://github.com/SignPath/github-action-submit-signing-request/issues/18
|
||||
uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -14,4 +14,6 @@ fastfetch.kdev4
|
||||
*.swp
|
||||
*.log
|
||||
/*.zh.md
|
||||
/.*-ai/
|
||||
/.work*
|
||||
/doc/*.md
|
||||
/fastfetch.wiki
|
||||
|
||||
+96
-1
@@ -1,4 +1,96 @@
|
||||
# 2.68.0
|
||||
# 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.
|
||||
* 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)
|
||||
|
||||
Features:
|
||||
* Added Sound detection on Android. (Sound, Android)
|
||||
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
|
||||
* Performance improvements & internal cleanups
|
||||
* Added Finnish Language Support
|
||||
|
||||
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)
|
||||
|
||||
Logos:
|
||||
* Added filled Artix logo (#2631)
|
||||
* Renamed PostmarketOS to nura (#2626)
|
||||
|
||||
# 2.69.0
|
||||
|
||||
Changes:
|
||||
* ImageMagick 6 is deprecated and will be removed in a future release. It remains available for older Debian and Ubuntu releases; upgrade to ImageMagick 7 when possible.
|
||||
* The experimental QuickJS format scripting is deprecated, disabled by default, and planned for removal in the next release. Re-enable it with `cmake -DENABLE_QUICKJS=ON`, or migrate scripts to Lua. (General)
|
||||
* Replaced `--logo-recache` and `logo.recache` with `--logo-cache` and `logo.cache`. Use `true` to reuse or create valid cache entries, `false` to disable caching, or `regen` to force regeneration. (Logo)
|
||||
* Modules selected with `--structure` now honor their options from the JSON config.
|
||||
|
||||
Features:
|
||||
* Added `logo.position: "auto"` and `--logo-position auto` to place wide logos above module output when the terminal has less than 32 columns available for text. (Logo)
|
||||
* The position can also be selected in interactive `--gen-config`.
|
||||
* For now, the default position is `left` for compatibility with previous releases. This may change to `auto` in future releases.
|
||||
* Improved Bluetooth detection support:
|
||||
* Added Bluetooth Low Energy detection on Windows and macOS.
|
||||
* Report device type (LE or classic) and signal quality, if available.
|
||||
* Added Bluetooth Core 6.0–6.3 version reporting. (BluetoothRadio)
|
||||
* Improved image log support:
|
||||
* Image logos on Windows, macOS, and Android now use native image processing libraries instead of ImageMagick.
|
||||
* For package managers: ImageMagick dependencies can be removed on macOS, Windows and Android as they are no longer used for image logos.
|
||||
* Windows Terminal supports sixel logos out of the box
|
||||
* Changes to a source image now correctly invalidate its cache. (Logo)
|
||||
* Added GIF and APNG support to `--kitty` image protocol. Set `logo.animationFrame` to `0` to play animations in compatible kitty terminals; positive or negative values select a still frame. Android animation requires Android 12 (API 31); terminals without kitty graphics protocol support report an error and fall back to the built-in logo instead. (Logo)
|
||||
* Added cmake option `-DENABLE_IMAGE_LOGO=<BOOL>` (default `ON`) to disable image logos and reduce binary size. `raw` logos remain available. (Logo)
|
||||
* Improved Linux support:
|
||||
* Added RakuOS rum overlay package counting (`{rum}`) and CRUX package detection (`{crux}`). (Packages)
|
||||
* Added CPU name and frequency detection on SPARC. (CPU)
|
||||
* Improved COSMIC version detection and added Umbriel version detection. (DE / WM)
|
||||
* Improved Android support:
|
||||
* Expanded ROM detection to HarmonyOS, Flyme, OneUI, LineageOS, and many other ROMs. (DE)
|
||||
* Improved camera detection; Android 7 (API 24) or later is required. (Camera)
|
||||
* Battery level and charging state are now available to apps; detailed battery information requires root or ADB. (Battery)
|
||||
* Added display mode, physical size, rotation, and HDR information. App-based detection requires Android 13 (API 33); root or ADB can also detect displays on older versions. (Display)
|
||||
* Added Wi-Fi interface, connection, standard, and security information. Android 11 (API 30) or later is required. Termux:API is needed for the Wi-Fi permission, and location permission is needed to reveal SSID and BSSID; without location permission the two names appear as `<redacted>`. (Wifi)
|
||||
* Added Wallpaper detection, which effectively reports `/data/system/users/0/wallpaper`. (Wallpaper)
|
||||
* Added Media detection, root/ADB only. (Media / Player)
|
||||
* Improved macOS support:
|
||||
* Improved process-name detection in Top. (Top)
|
||||
* Improved wallpaper detection on macOS Sonoma and later. (#2559, Wallpaper)
|
||||
* Improved Windows support:
|
||||
* `winget` now counts packages from configured sources, excludes Microsoft Store apps, and caches results. It remains disabled by default. (Packages)
|
||||
* Player names now match the names shown by Windows (for example, Chrome is reported as Google Chrome). (Player)
|
||||
* Added battery-level detection for more controllers on Windows and macOS. (Gamepad)
|
||||
* Improved macOS controller name detection (macOS, Gamepad)
|
||||
* macOS controller names now match the system; configs that filter by name may need updating.
|
||||
* Requires macOS 11 or later.
|
||||
* Improved the built-in HTTP client used by PublicIP and Weather; it now supports custom ports and chunked responses. (PublicIP / Weather)
|
||||
* Reduced the default `waitTime` for DiskIO, NetIO, and Top from 500 ms to 250 ms. (DiskIO / NetIO / Top)
|
||||
* Added `general.preload.lua` for loading shared Lua helpers before modules run. It requires a Lua-enabled build and starts the Lua interpreter whenever configured. (General)
|
||||
* Lua format scripting is now considered stable.
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Snapdragon X2 series model detection on Linux. (#2611, CPU / Linux)
|
||||
* Fixed image logo caching, padding, and positioning issues, including an iTerm display bug. (Logo)
|
||||
* Fixed several Windows issues, including Windows Terminal font detection, redirected `--gen-config` output, and gamepad battery reporting for DualShock 4 and Switch controllers. (#2573, TerminalFont; Gamepad; General)
|
||||
* Fixed formatting and display issues, including ANSI-aware truncation, date/time formatting, and bright key/title colors. (Format / DateTime / Display)
|
||||
* Fixed `--dynamic-interval` retries and stale results across multiple rounds. (PublicIP / Weather / Display / Monitor / WM / DE / Media / Player / Shell / Terminal)
|
||||
* Fixed invalid numeric and color options that could cause hangs or excessive output. (Separator / CPUUsage / NetIO / DiskIO / LoadAvg / Colors)
|
||||
* Fixed JSON output and config validation across several modules, including GPU, Btrfs, Gamepad, Packages, and Custom. (General)
|
||||
* Fixed GPU memory and device-type reporting, GNOME Classic and Trinity version detection, and default-route family names. (GPU / DE / LocalIP)
|
||||
* Fixed module output and filtering issues affecting editor failures, sound status, command selection, and ignored keyboard/gamepad devices. (Editor / Sound / Command / Keyboard / Gamepad)
|
||||
* Fixed package counting with read-only databases and corrected other package detection and JSON output issues. (Packages)
|
||||
* Fixed Base64 encoding, big-endian handling, and 64-bit value truncation on 32-bit platforms. (General)
|
||||
|
||||
Logos:
|
||||
* Added ALT Atomic
|
||||
* Removed Zerene
|
||||
|
||||
# 2.68.1
|
||||
|
||||
Changes:
|
||||
* The DE / WM / LM modules now reports the full name "Desktop Environment" / "Window Manager" / "Login Manager" instead of the abbreviations.
|
||||
@@ -11,6 +103,7 @@ Features:
|
||||
* Added module key localization support. (General)
|
||||
* Added a new option `--key-language <?lang>` (`display.key.language: "<?lang>"` in JSON config). When left empty, it defaults to the system locale.
|
||||
* See `fastfetch -h key-language` for all supported languages.
|
||||
* Exposed in custom key format as `{module-name}`.
|
||||
* Improved Wi-Fi module
|
||||
* Added Wifi channel width detection, exposed via `{channel-width}` in custom format (reports 0 when not supported).
|
||||
* Improved Wifi channel frequency accuracy and fortified detection on Windows, macOS.
|
||||
@@ -29,6 +122,8 @@ Bugfixes:
|
||||
* Fixed memory usage detection support on x86-32 FreeBSD (Memory, FreeBSD)
|
||||
* Note: Although 32-bit systems are still supported, they are deprecated and barely tested. Users are encouraged to upgrade to 64-bit systems.
|
||||
* Some internal cleanups and optimizations.
|
||||
* Attempted to fix potential issues on big-endian systems
|
||||
* Not tested due to lack of access to big-endian hardware.
|
||||
|
||||
Logos:
|
||||
* Added macOS_old
|
||||
|
||||
+288
-123
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.12.0) # target_link_libraries with OBJECT libs & project homepage url
|
||||
cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23
|
||||
|
||||
project(fastfetch
|
||||
VERSION 2.68.0
|
||||
VERSION 2.70.0
|
||||
LANGUAGES C
|
||||
DESCRIPTION "Fast neofetch-like system information tool"
|
||||
HOMEPAGE_URL "https://github.com/fastfetch-cli/fastfetch"
|
||||
@@ -9,9 +9,6 @@ project(fastfetch
|
||||
|
||||
set(PROJECT_LICENSE "MIT license")
|
||||
|
||||
if(DEFINED CMAKE_SYSTEM_PROCESSOR_OVERRIDE) # Used by github actions for i686 build
|
||||
set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR_OVERRIDE} CACHE INTERNAL "")
|
||||
endif()
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" CMAKE_SYSTEM_PROCESSOR)
|
||||
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
|
||||
set(CMAKE_SYSTEM_PROCESSOR "amd64")
|
||||
@@ -91,18 +88,25 @@ cmake_dependent_option(ENABLE_EET "Enable eet" ON "LINUX OR FreeBSD OR OpenBSD O
|
||||
cmake_dependent_option(ENABLE_DBUS "Enable dbus-1" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_SQLITE3 "Enable sqlite3" ON "LINUX OR FreeBSD OR APPLE OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_RPM "Enable rpm" ON "LINUX OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR ANDROID OR WIN32 OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7" OFF)
|
||||
cmake_dependent_option(ENABLE_EGL "Enable egl" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR WIN32 OR SunOS OR Haiku OR GNU" OFF)
|
||||
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)
|
||||
|
||||
option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON)
|
||||
# The four below are declared outside the guard on purpose. `cmake_dependent_option` is a macro: when
|
||||
# the condition is false it is never expanded, so nothing rewrites the value already sitting in
|
||||
# CMakeCache.txt, and `cmake -DENABLE_IMAGE_LOGO=OFF .` in an existing build directory would leave
|
||||
# ENABLE_SIXEL=ON behind -- sixel and chafa would still be compiled and linked while image.c, the
|
||||
# only file that uses them, is not. The condition does the disabling instead: it is the fourth
|
||||
# argument's job, and it keeps the option visible and settable in cmake-gui either way.
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "ENABLE_IMAGE_LOGO;WIN32 OR APPLE OR ANDROID" OFF)
|
||||
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGE_LOGO;ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
|
||||
|
||||
option(ENABLE_ZLIB "Enable zlib" ON)
|
||||
option(ENABLE_SYSTEM_YYJSON "Use system provided (instead of fastfetch embedded) yyjson library" OFF)
|
||||
option(ENABLE_ASAN "Build fastfetch with ASAN (address sanitizer)" OFF)
|
||||
@@ -117,7 +121,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" ON)
|
||||
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")
|
||||
@@ -128,12 +131,38 @@ if(APPLE)
|
||||
option(ENABLE_APPLE_MEMSIZE_USABLE "Use usable memory size as total memory size in Memory module, to match other systems" OFF)
|
||||
endif()
|
||||
|
||||
file(GLOB FF_MODULE_DIRS CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src/modules" "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/*/")
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/${FF_MODULE_DIR}")
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
|
||||
# A module is a directory `src/modules/<module>/` that contains `<module>.c`.
|
||||
# Globbing the sources rather than the directories skips empty directories and
|
||||
# other leftovers, and makes `CONFIGURE_DEPENDS` react to added or removed sources.
|
||||
file(GLOB FF_MODULE_SRCS CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src/modules" "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/*/*.c")
|
||||
set(FF_MODULE_DIRS "")
|
||||
set(FF_MODULE_SRC_DIRS "")
|
||||
foreach(FF_MODULE_SRC ${FF_MODULE_SRCS})
|
||||
get_filename_component(FF_MODULE_DIR "${FF_MODULE_SRC}" DIRECTORY)
|
||||
get_filename_component(FF_MODULE_NAME "${FF_MODULE_SRC}" NAME_WE)
|
||||
if("${FF_MODULE_DIR}" STREQUAL "${FF_MODULE_NAME}")
|
||||
list(APPEND FF_MODULE_DIRS "${FF_MODULE_DIR}")
|
||||
endif()
|
||||
list(APPEND FF_MODULE_SRC_DIRS "${FF_MODULE_DIR}")
|
||||
endforeach()
|
||||
unset(FF_MODULE_SRCS)
|
||||
|
||||
# A directory of sources whose name no source carries registers no module at all, and nothing else
|
||||
# in the build says so -- the module simply never appears. Reported rather than left to be noticed.
|
||||
# Directories are compared rather than sources, so that a module which keeps helper sources next to
|
||||
# its own `<module>.c` does not warn about every one of them.
|
||||
list(REMOVE_DUPLICATES FF_MODULE_SRC_DIRS)
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_SRC_DIRS})
|
||||
if(NOT FF_MODULE_DIR IN_LIST FF_MODULE_DIRS)
|
||||
message(WARNING "src/modules/${FF_MODULE_DIR}/ contains C sources but no ${FF_MODULE_DIR}.c, "
|
||||
"so no module is registered for it. A module directory must hold a source file named after it.")
|
||||
endif()
|
||||
endforeach()
|
||||
unset(FF_MODULE_SRC_DIRS)
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
|
||||
endforeach()
|
||||
|
||||
set(BINARY_LINK_TYPE_OPTIONS dlopen dynamic static)
|
||||
@@ -251,6 +280,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 #
|
||||
#######################
|
||||
@@ -431,6 +476,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
|
||||
@@ -453,6 +499,7 @@ set(LIBFASTFETCH_SRC
|
||||
src/common/impl/time.c
|
||||
src/common/impl/edidHelper.c
|
||||
src/common/impl/base64.c
|
||||
src/common/impl/FFcache.c
|
||||
src/common/impl/FFlist.c
|
||||
src/common/impl/FFstrbuf.c
|
||||
src/common/impl/path.c
|
||||
@@ -492,92 +539,33 @@ set(LIBFASTFETCH_SRC
|
||||
src/detection/weather/weather.c
|
||||
src/detection/zpool/zpool.c
|
||||
src/logo/builtin.c
|
||||
src/logo/image/im6.c
|
||||
src/logo/image/im7.c
|
||||
src/logo/image/image.c
|
||||
src/logo/logo.c
|
||||
src/modules/battery/battery.c
|
||||
src/modules/bios/bios.c
|
||||
src/modules/bluetooth/bluetooth.c
|
||||
src/modules/bluetoothradio/bluetoothradio.c
|
||||
src/modules/board/board.c
|
||||
src/modules/bootmgr/bootmgr.c
|
||||
src/modules/brightness/brightness.c
|
||||
src/modules/break/break.c
|
||||
src/modules/btrfs/btrfs.c
|
||||
src/modules/camera/camera.c
|
||||
src/modules/chassis/chassis.c
|
||||
src/modules/codec/codec.c
|
||||
src/modules/colors/colors.c
|
||||
src/modules/cpu/cpu.c
|
||||
src/modules/cpucache/cpucache.c
|
||||
src/modules/cpuusage/cpuusage.c
|
||||
src/modules/cursor/cursor.c
|
||||
src/modules/custom/custom.c
|
||||
src/modules/command/command.c
|
||||
src/modules/datetime/datetime.c
|
||||
src/modules/de/de.c
|
||||
src/modules/disk/disk.c
|
||||
src/modules/diskio/diskio.c
|
||||
src/modules/dns/dns.c
|
||||
src/modules/editor/editor.c
|
||||
src/modules/font/font.c
|
||||
src/modules/gpu/gpu.c
|
||||
src/modules/host/host.c
|
||||
src/modules/icons/icons.c
|
||||
src/modules/initsystem/initsystem.c
|
||||
src/modules/gamepad/gamepad.c
|
||||
src/modules/kernel/kernel.c
|
||||
src/modules/keyboard/keyboard.c
|
||||
src/modules/lm/lm.c
|
||||
src/modules/loadavg/loadavg.c
|
||||
src/modules/locale/locale.c
|
||||
src/modules/localip/localip.c
|
||||
src/modules/logo/logo.c
|
||||
src/modules/memory/memory.c
|
||||
src/modules/monitor/monitor.c
|
||||
src/modules/netio/netio.c
|
||||
src/modules/opencl/opencl.c
|
||||
src/modules/opengl/opengl.c
|
||||
src/modules/os/os.c
|
||||
src/modules/packages/packages.c
|
||||
src/modules/physicaldisk/physicaldisk.c
|
||||
src/modules/physicalmemory/physicalmemory.c
|
||||
src/modules/processes/processes.c
|
||||
src/modules/player/player.c
|
||||
src/modules/poweradapter/poweradapter.c
|
||||
src/modules/publicip/publicip.c
|
||||
src/modules/display/display.c
|
||||
src/modules/separator/separator.c
|
||||
src/modules/shell/shell.c
|
||||
src/modules/sound/sound.c
|
||||
src/modules/swap/swap.c
|
||||
src/modules/media/media.c
|
||||
src/modules/mouse/mouse.c
|
||||
src/modules/terminal/terminal.c
|
||||
src/modules/terminaltheme/terminaltheme.c
|
||||
src/modules/terminalfont/terminalfont.c
|
||||
src/modules/terminalsize/terminalsize.c
|
||||
src/modules/theme/theme.c
|
||||
src/modules/title/title.c
|
||||
src/modules/top/top.c
|
||||
src/modules/tpm/tpm.c
|
||||
src/modules/uptime/uptime.c
|
||||
src/modules/users/users.c
|
||||
src/modules/version/version.c
|
||||
src/modules/vulkan/vulkan.c
|
||||
src/modules/wallpaper/wallpaper.c
|
||||
src/modules/weather/weather.c
|
||||
src/modules/wifi/wifi.c
|
||||
src/modules/wm/wm.c
|
||||
src/modules/wmtheme/wmtheme.c
|
||||
src/modules/zpool/zpool.c
|
||||
src/modules/modules.c
|
||||
src/options/display.c
|
||||
src/options/logo.c
|
||||
src/options/general.c
|
||||
)
|
||||
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/logo/image/image.c
|
||||
src/logo/image/im6.c
|
||||
src/logo/image/im7.c
|
||||
src/logo/image/sixel.c
|
||||
)
|
||||
if(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/logo/image/aid.c
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/modules/${FF_MODULE_DIR}/${FF_MODULE_DIR}.c
|
||||
)
|
||||
endforeach()
|
||||
|
||||
if(LINUX)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/impl/dbus.c
|
||||
@@ -666,6 +654,9 @@ if(LINUX)
|
||||
)
|
||||
elseif(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/android/binder.c
|
||||
src/common/android/dex.c
|
||||
src/common/android/package.c
|
||||
src/common/impl/dbus.c
|
||||
src/common/impl/io_unix.c
|
||||
src/common/impl/netif_linux.c
|
||||
@@ -719,6 +710,7 @@ elseif(ANDROID)
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_nosupport.c
|
||||
src/detection/media/media_android.c
|
||||
src/detection/media/media_linux.c
|
||||
src/detection/memory/memory_linux.c
|
||||
src/detection/mouse/mouse_nosupport.c
|
||||
@@ -729,6 +721,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
|
||||
@@ -740,6 +733,7 @@ elseif(ANDROID)
|
||||
src/detection/tpm/tpm_nosupport.c
|
||||
src/detection/uptime/uptime_linux.c
|
||||
src/detection/users/users_linux.c
|
||||
src/detection/wallpaper/wallpaper_android.c
|
||||
src/detection/wallpaper/wallpaper_linux.c
|
||||
src/detection/wifi/wifi_android.c
|
||||
src/detection/wm/wm_linux.c
|
||||
@@ -1056,7 +1050,7 @@ elseif(APPLE)
|
||||
src/detection/libc/libc_apple.c
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_apple.c
|
||||
src/detection/gamepad/gamepad_apple.m
|
||||
src/detection/media/media_apple.m
|
||||
src/detection/memory/memory_apple.c
|
||||
src/detection/mouse/mouse_apple.c
|
||||
@@ -1084,6 +1078,11 @@ elseif(APPLE)
|
||||
src/detection/wmtheme/wmtheme_apple.c
|
||||
src/detection/camera/camera_apple.m
|
||||
)
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/logo/image/imageio.c
|
||||
)
|
||||
endif()
|
||||
# CMAKE_SYSTEM_PROCESSOR has been normalized before
|
||||
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64")
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
@@ -1110,17 +1109,19 @@ 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
|
||||
src/common/windows/winrt.cpp
|
||||
src/common/windows/version.c
|
||||
src/detection/battery/battery_windows.c
|
||||
src/detection/bios/bios_windows.c
|
||||
src/detection/bluetooth/bluetooth_windows.c
|
||||
src/detection/bluetooth/bluetooth_windows.cpp
|
||||
src/detection/bluetoothradio/bluetoothradio_windows.c
|
||||
src/detection/board/board_windows.c
|
||||
src/detection/bootmgr/bootmgr_windows.c
|
||||
@@ -1177,6 +1178,11 @@ elseif(WIN32)
|
||||
src/detection/wmtheme/wmtheme_windows.c
|
||||
src/detection/camera/camera_windows.cpp
|
||||
)
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/logo/image/wic.cpp
|
||||
)
|
||||
endif()
|
||||
elseif(SunOS)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/impl/dbus.c
|
||||
@@ -1384,7 +1390,7 @@ elseif(GNU)
|
||||
src/detection/keyboard/keyboard_nosupport.c
|
||||
src/detection/libc/libc_linux.c
|
||||
src/detection/lm/lm_linux.c
|
||||
src/detection/loadavg/loadavg_linux.c
|
||||
src/detection/loadavg/loadavg_sunos.c
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_nosupport.c
|
||||
@@ -1480,16 +1486,29 @@ else()
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ENABLE_SIXEL)
|
||||
set(FF_SIXEL_SRC
|
||||
src/3rdparty/sixel/allocator.c
|
||||
src/3rdparty/sixel/dither.c
|
||||
src/3rdparty/sixel/output.c
|
||||
src/3rdparty/sixel/pixelformat.c
|
||||
src/3rdparty/sixel/quant.c
|
||||
src/3rdparty/sixel/status.c
|
||||
src/3rdparty/sixel/tosixel.c
|
||||
)
|
||||
list(APPEND LIBFASTFETCH_SRC ${FF_SIXEL_SRC})
|
||||
|
||||
set_source_files_properties(${FF_SIXEL_SRC} PROPERTIES COMPILE_OPTIONS "-Wno-conversion")
|
||||
endif()
|
||||
|
||||
add_library(libfastfetch OBJECT
|
||||
${LIBFASTFETCH_SRC}
|
||||
)
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/${FF_MODULE_DIR}")
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
if(MODULE_DISABLE_${FF_MODULE_UPPER})
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_MODULE_DISABLE_${FF_MODULE_UPPER}=1)
|
||||
endif()
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
if(MODULE_DISABLE_${FF_MODULE_UPPER})
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_MODULE_DISABLE_${FF_MODULE_UPPER}=1)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -1522,10 +1541,11 @@ if(yyjson_FOUND)
|
||||
# `target_link_libraries(yyjson::yyjson)` sets rpath implicitly
|
||||
endif()
|
||||
|
||||
# Used for dlopen finding dylibs installed by homebrew
|
||||
# `/opt/homebrew/lib` is not on in dlopen search path by default
|
||||
# Used for dlopen finding dylibs installed by package managers
|
||||
# `/opt/homebrew/lib`, `/usr/local/lib` and `/opt/local/lib` are not on the
|
||||
# dlopen search path by default
|
||||
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")
|
||||
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()
|
||||
|
||||
if(ANDROID)
|
||||
@@ -1840,23 +1860,10 @@ ff_lib_enable(FREETYPE
|
||||
"freetype2"
|
||||
"FreeType2"
|
||||
)
|
||||
ff_lib_enable(PULSE
|
||||
"libpulse"
|
||||
"Pulse"
|
||||
)
|
||||
ff_lib_enable(DDCUTIL
|
||||
"ddcutil"
|
||||
"Ddcutil"
|
||||
)
|
||||
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)
|
||||
@@ -1890,6 +1897,10 @@ if(ENABLE_THREADS)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_IMAGE_LOGO=1)
|
||||
endif()
|
||||
|
||||
if(ENABLE_EMBEDDED_PCIIDS)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_EMBEDDED_PCIIDS=1)
|
||||
endif()
|
||||
@@ -1927,24 +1938,28 @@ if(LINUX)
|
||||
elseif(APPLE)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "-framework AVFoundation"
|
||||
PRIVATE "-framework Accelerate"
|
||||
PRIVATE "-framework Cocoa"
|
||||
PRIVATE "-framework CoreFoundation"
|
||||
PRIVATE "-framework CoreAudio"
|
||||
PRIVATE "-framework CoreBluetooth"
|
||||
PRIVATE "-framework CoreFoundation"
|
||||
PRIVATE "-framework CoreMedia"
|
||||
PRIVATE "-framework CoreVideo"
|
||||
PRIVATE "-framework CoreWLAN"
|
||||
PRIVATE "-framework GameController"
|
||||
PRIVATE "-framework IOBluetooth"
|
||||
PRIVATE "-framework IOKit"
|
||||
PRIVATE "-framework ImageIO"
|
||||
PRIVATE "-framework Metal"
|
||||
PRIVATE "-framework OpenGL"
|
||||
PRIVATE "-framework OpenCL"
|
||||
PRIVATE "-framework OpenGL"
|
||||
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
|
||||
@@ -1960,7 +1975,17 @@ elseif(WIN32)
|
||||
PRIVATE "cfgmgr32"
|
||||
PRIVATE "winbrand"
|
||||
PRIVATE "secur32"
|
||||
PRIVATE "shlwapi"
|
||||
)
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
# Only src/logo/image/wic.cpp uses WIC, and that file is only compiled with image logos
|
||||
# enabled (see the WIN32 source list above). Verified by building with
|
||||
# `-DENABLE_IMAGE_LOGO=OFF` and removing the library from the link line: it links and runs
|
||||
# without it, and nothing else in the tree references IWIC*.
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "windowscodecs"
|
||||
)
|
||||
endif()
|
||||
if(NOT ENABLE_WIN81_COMPAT)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "mincore"
|
||||
@@ -1994,7 +2019,6 @@ elseif(FreeBSD)
|
||||
elseif(OpenBSD)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "m"
|
||||
PRIVATE "kvm"
|
||||
PRIVATE "sndio"
|
||||
PRIVATE "util"
|
||||
)
|
||||
@@ -2021,6 +2045,9 @@ elseif(GNU)
|
||||
elseif(ANDROID)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "m"
|
||||
PRIVATE jnigraphics
|
||||
PRIVATE mediandk
|
||||
PRIVATE camera2ndk
|
||||
)
|
||||
if(ENABLE_WORDEXP)
|
||||
# https://github.com/termux/termux-packages/pull/7056
|
||||
@@ -2038,6 +2065,38 @@ elseif(ANDROID)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Three APIs are newer than the API level this build targets: AImageDecoder is 30,
|
||||
# AndroidBitmap_compress is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28.
|
||||
# fastfetch keeps running on devices below those levels, and the NDK headers mark the entry
|
||||
# points unavailable there, so they have to be weak references -- a strong reference would fail
|
||||
# the load on an older device. Guarding every use with __builtin_available() is what makes that
|
||||
# safe, and turning the guard diagnostic into an error keeps a use from slipping past it.
|
||||
#
|
||||
# Each one lives in the library that provides it, and that library is linked above:
|
||||
# AImageDecoder_* and AndroidBitmap_compress are exported by libjnigraphics.so, and
|
||||
# AMediaCodec_* by libmediandk.so. Checked on a device (Android 16, API 36) with
|
||||
# `readelf --dyn-syms /system/lib64/libjnigraphics.so` and a dlopen/dlsym probe: every
|
||||
# AImageDecoder_* entry point resolves there and none of them resolves in libandroid.so, which
|
||||
# is why libandroid.so is *not* linked -- nothing fastfetch calls comes from it. Keep the
|
||||
# provider library linked for the reason the NDK requires it: an undefined weak symbol resolves
|
||||
# to NULL rather than failing the load, and FF_ANDROID_API_AT_LEAST() reads the platform level,
|
||||
# not the symbol, so it would send the call straight through a NULL pointer on every device new
|
||||
# enough to pass the guard.
|
||||
#
|
||||
# camera_android.c is deliberately not in this list. The camera2 NDK is a *library* that starts at
|
||||
# API 24, and a weak reference weakens the symbol, never the library: DT_NEEDED would still name
|
||||
# libcamera2ndk.so, and a device below API 24 would fail to load fastfetch at all. The NDK's guide
|
||||
# is explicit that there is no such thing as a weak library reference, and that dlopen is the only
|
||||
# way to reach a library newer than minSdkVersion. Nothing here pins ANDROID_PLATFORM, so a build
|
||||
# aimed below 24 would need that; on the supported path -- Termux, which targets 24 -- the library
|
||||
# is always present and the plain call is correct. libjnigraphics itself is one of them and is
|
||||
# linked unconditionally above, so the encoder only needs its symbol weakened, not its library.
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/logo/image/aid.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/media/media_android.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/codec/codec_android.c
|
||||
PROPERTIES COMPILE_OPTIONS "-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__;-Werror=unguarded-availability")
|
||||
elseif(Haiku)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "network"
|
||||
@@ -2057,6 +2116,16 @@ target_include_directories(libfastfetch
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
if(ENABLE_SIXEL)
|
||||
# src/3rdparty/sixel/*.c include <sixel.h> (angle brackets, so the directory itself
|
||||
# has to be on the include path)
|
||||
target_include_directories(libfastfetch PRIVATE ${PROJECT_SOURCE_DIR}/src/3rdparty/sixel)
|
||||
|
||||
# The vendored subset is not a library to be detected, so FF_HAVE_SIXEL can't come
|
||||
# from ff_lib_enable() and has to be defined here
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_SIXEL=1)
|
||||
endif()
|
||||
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE ${CMAKE_DL_LIBS}
|
||||
)
|
||||
@@ -2194,6 +2263,20 @@ endif()
|
||||
###################
|
||||
|
||||
if (BUILD_TESTS)
|
||||
add_executable(fastfetch-test-cache
|
||||
tests/cache.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-cache
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-logo
|
||||
tests/logo.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-logo
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-strbuf
|
||||
tests/strbuf.c
|
||||
)
|
||||
@@ -2236,13 +2319,95 @@ if (BUILD_TESTS)
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-networking
|
||||
tests/networking.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-networking
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-base64
|
||||
tests/base64.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-base64
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-parsing
|
||||
tests/parsing.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-parsing
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-properties
|
||||
tests/properties.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-properties
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-size
|
||||
tests/size.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-size
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-frequency
|
||||
tests/frequency.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-frequency
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-percent
|
||||
tests/percent.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-percent
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-temps
|
||||
tests/temps.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-temps
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-endian
|
||||
tests/endian.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-endian
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-endian-bigendian
|
||||
tests/endian-bigendian.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-endian-bigendian
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
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)
|
||||
add_test(NAME test-list COMMAND fastfetch-test-list)
|
||||
add_test(NAME test-format COMMAND fastfetch-test-format)
|
||||
add_test(NAME test-color COMMAND fastfetch-test-color)
|
||||
add_test(NAME test-duration COMMAND fastfetch-test-duration)
|
||||
add_test(NAME test-strutil COMMAND fastfetch-test-strutil)
|
||||
add_test(NAME test-networking COMMAND fastfetch-test-networking)
|
||||
add_test(NAME test-base64 COMMAND fastfetch-test-base64)
|
||||
add_test(NAME test-parsing COMMAND fastfetch-test-parsing)
|
||||
add_test(NAME test-properties COMMAND fastfetch-test-properties)
|
||||
add_test(NAME test-size COMMAND fastfetch-test-size)
|
||||
add_test(NAME test-frequency COMMAND fastfetch-test-frequency)
|
||||
add_test(NAME test-percent COMMAND fastfetch-test-percent)
|
||||
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)
|
||||
endif()
|
||||
|
||||
##################
|
||||
|
||||
+144
-76
@@ -2,15 +2,15 @@
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Note
|
||||
Disclaimer
|
||||
</summary>
|
||||
|
||||
This document is generated by AI, and mainly for AI use. It is not a substitute for human review, and may contain errors or omissions. Please verify any information before relying on it.
|
||||
This document is generated by AI and reviewed by humans. It is primarily intended for AI-assisted development, while remaining useful to human developers.
|
||||
</details>
|
||||
|
||||
Thanks for your interest in fastfetch. This document covers building, architecture, how to add a module or a logo, code style, commit conventions, and the pull request workflow.
|
||||
Thank you for your interest in fastfetch. This document covers building, architecture, how to add a module or a logo, code style, commit conventions, and the pull request workflow.
|
||||
|
||||
fastfetch is a system information tool written in C23, supporting Linux, macOS, Windows, the BSDs, Solaris, Haiku and Android. The project is borderline obsessive about **startup time** and **optional dependencies** — many design decisions only make sense under that constraint, and this document keeps coming back to it.
|
||||
fastfetch is a system information tool written in C23, supporting Linux, macOS, Windows, the BSDs, Solaris, Haiku and Android. The project places strong emphasis on **startup time** and on keeping **dependencies optional**; many design decisions only make sense under that constraint, and this document returns to it repeatedly.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +29,7 @@ fastfetch is a system information tool written in C23, supporting Linux, macOS,
|
||||
- [Changelog](#changelog)
|
||||
- [Tests](#tests)
|
||||
- [Pull requests](#pull-requests)
|
||||
- [Gotchas](#gotchas)
|
||||
- [Pitfalls](#pitfalls)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,14 +38,14 @@ fastfetch is a system information tool written in C23, supporting Linux, macOS,
|
||||
| Task | Start here | Verify with |
|
||||
|---|---|---|
|
||||
| Build and run fastfetch | `run.sh`, `CMakeLists.txt` | `./run.sh` |
|
||||
| Add a module | `src/modules/<name>/`, `src/detection/<name>/`, `src/modules/modules.c` | `cmake -B build && cmake --build build -j` |
|
||||
| Add a platform implementation | `src/detection/<name>/<name>_<platform>.c` | Build on the target platform or CI |
|
||||
| Add a module | `src/modules/<name>/`, `src/detection/<name>/`, `src/modules/modules.c`, `CMakeLists.txt` | `cmake -B build && cmake --build build -j` |
|
||||
| Add a platform implementation | `src/detection/<name>/<name>_<platform>.c`, the matching platform block in `CMakeLists.txt` | Build on the target platform or CI |
|
||||
| Add an ASCII logo | `src/logo/ascii/<letter>/<name>.txt`, matching `<letter>.inc` | `./build/fastfetch --logo <name>` |
|
||||
| Change formatting or JSON output | `src/modules/<name>/<name>.c` | `./build/fastfetch -s <name> --format json` |
|
||||
| Change shared formatting or containers | `src/common/format.h`, `src/common/color.h`, `src/common/FFstrbuf.h` | Build and run the matching test |
|
||||
| Run the test suite | `tests/`, `build/` | `cd build && ctest --output-on-failure` |
|
||||
|
||||
For a normal code change, the shortest useful loop is: configure, build the `fastfetch` target, run the affected module, then run the tests. If you add a directory or a generated input, re-run `cmake -B build` so CMake refreshes its file globs.
|
||||
For a typical code change, the shortest useful iteration is: configure, build the `fastfetch` target, run the affected module, then run the tests. If you add a directory or a generated input, re-run `cmake -B build` so that CMake refreshes its file globs.
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +53,7 @@ For a normal code change, the shortest useful loop is: configure, build the `fas
|
||||
|
||||
### Building
|
||||
|
||||
The quickest way is `run.sh` in the repository root — it creates `build/`, configures, compiles and runs the binary:
|
||||
The quickest way is `run.sh` in the repository root: it creates `build/`, configures, compiles and runs the binary.
|
||||
|
||||
```sh
|
||||
./run.sh # build and run
|
||||
@@ -68,7 +68,7 @@ cmake --build build --target fastfetch -j$(nproc)
|
||||
./build/fastfetch
|
||||
```
|
||||
|
||||
The default build type is `RelWithDebInfo`. LTO is enabled whenever `ENABLE_LTO=ON` **and** `CMAKE_BUILD_TYPE != Debug` — so the default already has it, and only `Debug` builds lack it. This matters because LTO here is not merely an optimization: it is what strips the code of disabled modules. See [Gotchas](#gotchas).
|
||||
The default build type is `RelWithDebInfo`. LTO is enabled whenever `ENABLE_LTO=ON` **and** `CMAKE_BUILD_TYPE != Debug`, so the default configuration enables it and only `Debug` builds omit it. This is significant because LTO here is not merely an optimization: it is what removes the code of disabled modules. See [Pitfalls](#pitfalls).
|
||||
|
||||
### Common build options
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -123,12 +123,12 @@ fastfetch.c → options/ (global configuration)
|
||||
→ common/ (infrastructure)
|
||||
```
|
||||
|
||||
The number of module directories and detection directories does not match, and that is expected. The following relationships are structural rather than a fixed inventory:
|
||||
The number of module directories does not match the number of detection directories, which is expected. The following relationships are structural rather than a fixed inventory:
|
||||
|
||||
- **13 modules have no detection directory** — they are either pure layout (`break`, `separator`, `colors`, `title`, `logo`) or reuse someone else's detection result (`display`, `monitor`, `kernel`, `shell`, `terminal`, `player`, `custom`, `datetime`).
|
||||
- **13 modules have no detection directory** — they are either pure layout (`break`, `separator`, `colors`, `title`, `logo`) or reuse another module's detection result (`display`, `monitor`, `kernel`, `shell`, `terminal`, `player`, `custom`, `datetime`).
|
||||
- **4 detection directories serve modules with different names** — `displayserver` (→ `display`, `monitor`), `gtk_qt` (→ `theme`, `icons`, `font`, `cursor`), `terminalshell` (→ `terminal`, `shell`) and `libc`.
|
||||
|
||||
So do not assume `modules/<x>/` always has a matching `detection/<x>/`.
|
||||
Therefore, do not assume that `modules/<x>/` always has a matching `detection/<x>/`.
|
||||
|
||||
### Layer boundaries
|
||||
|
||||
@@ -139,13 +139,13 @@ So do not assume `modules/<x>/` always has a matching `detection/<x>/`.
|
||||
| `detection/` | cross-platform data retrieval, clean result structs | print anything, read display config |
|
||||
| `common/` | general-purpose utilities, no business logic | depend on a specific module |
|
||||
|
||||
The test is simple: **no file under `detection/` should contain `printf` or read `instance.config`.** Symmetrically, no file under `modules/` should contain `#ifdef __linux__`.
|
||||
A simple check: **no file under `detection/` should contain `printf` or read `instance.config`.** Symmetrically, no file under `modules/` should contain `#ifdef __linux__`.
|
||||
|
||||
---
|
||||
|
||||
## Core architecture: modules vs. detection
|
||||
|
||||
This is the single most important convention in the codebase. Every feature is a fixed set of files — using CPU as the example:
|
||||
This is the most important convention in the codebase. Every feature consists of a fixed set of files; using CPU as the example:
|
||||
|
||||
```
|
||||
modules/cpu/option.h module option struct
|
||||
@@ -158,7 +158,7 @@ detection/cpu/cpu_bsd.c │
|
||||
detection/cpu/cpu_nosupport.c ┘
|
||||
```
|
||||
|
||||
The interface is deliberately minimal — usually just two things:
|
||||
The interface is deliberately minimal, typically consisting of only two declarations:
|
||||
|
||||
```c
|
||||
typedef struct FFCPUResult { ... } FFCPUResult; // result struct
|
||||
@@ -169,7 +169,7 @@ The `const char*` return value is an **error string**; `nullptr` means success.
|
||||
|
||||
Helper functions are only exposed when they are genuinely shared between platform implementations (as in `cpu.h`: `ffCPUAppleCodeToName`, `ffCPUDetectByCpuid`).
|
||||
|
||||
**The payoff:** adding a platform never touches `modules/`; fixing output formatting never touches `detection/`.
|
||||
**The benefit of this separation:** adding a platform never touches `modules/`; fixing output formatting never touches `detection/`.
|
||||
|
||||
---
|
||||
|
||||
@@ -197,7 +197,7 @@ typedef struct FFModuleBaseInfo {
|
||||
} FFModuleBaseInfo;
|
||||
```
|
||||
|
||||
The source comment openly admits this is UB — `void*` is not compatible with `FF*Options*`. It is a pragmatic compromise to get polymorphism in C. Don't try to "fix" it.
|
||||
The source comment acknowledges that this is undefined behavior, since `void*` is not compatible with `FF*Options*`. It is a pragmatic compromise to obtain polymorphism in C; do not attempt to "fix" it.
|
||||
|
||||
### The registry: a first-letter hash bucket
|
||||
|
||||
@@ -210,7 +210,7 @@ for (; *modules; ++modules) { // linear s
|
||||
}
|
||||
```
|
||||
|
||||
The registry is small enough that a subtraction plus a few string comparisons is preferable to a general-purpose hash table. When adding a module, place its descriptor in the bucket matching the first letter of `.name`; keep the existing `nullptr` terminator at the end.
|
||||
Because the registry is small, a single arithmetic bucket lookup followed by a few string comparisons is preferable to a general-purpose hash table. When adding a module, place its descriptor in the bucket matching the first letter of `.name`, and keep the existing `nullptr` terminator at the end.
|
||||
|
||||
### The calling convention: zero heap allocation
|
||||
|
||||
@@ -230,7 +230,7 @@ baseInfo->destroyOptions(optionBuf);
|
||||
static_assert(sizeof(FFCPUOptions) <= FF_OPTION_MAX_SIZE, "FFCPUOptions size exceeds maximum allowed size");
|
||||
```
|
||||
|
||||
**Meaning: no module option struct may exceed 256 bytes.** Hard constraint, enforced at compile time.
|
||||
**In other words, no module option struct may exceed 256 bytes.** This is a hard constraint, enforced at compile time.
|
||||
|
||||
The three dispatch entry points:
|
||||
|
||||
@@ -238,20 +238,49 @@ The three dispatch entry points:
|
||||
|---|---|---|
|
||||
| `parseModuleJsonObject` | `common/impl/jsonconfig.c:90` | the `modules[]` array in a JSONC config |
|
||||
| `parseStructureCommand` | `common/impl/commandoption.c:181` | the colon-separated structure string |
|
||||
| `ffParseModuleOptions` | `common/impl/commandoption.c:13` | **deprecated** — see [Gotchas](#4-cli-module-options-are-removed) |
|
||||
|
||||
### Build-time module discovery
|
||||
|
||||
`CMakeLists.txt:131` globs `src/modules/*/` and generates a `MODULE_DISABLE_<UPPER>` option per directory:
|
||||
`CMakeLists.txt:134` globs `src/modules/*/*.c`, keeps only the entries whose file name matches their directory name, and generates a `MODULE_DISABLE_<UPPER>` option for each:
|
||||
|
||||
```cmake
|
||||
file(GLOB FF_MODULE_DIRS RELATIVE "..." ".../src/modules/*/")
|
||||
file(GLOB FF_MODULE_SRCS CONFIGURE_DEPENDS RELATIVE "..." ".../src/modules/*/*.c")
|
||||
set(FF_MODULE_DIRS "")
|
||||
foreach(FF_MODULE_SRC ${FF_MODULE_SRCS})
|
||||
get_filename_component(FF_MODULE_DIR "${FF_MODULE_SRC}" DIRECTORY)
|
||||
get_filename_component(FF_MODULE_NAME "${FF_MODULE_SRC}" NAME_WE)
|
||||
if("${FF_MODULE_DIR}" STREQUAL "${FF_MODULE_NAME}")
|
||||
list(APPEND FF_MODULE_DIRS "${FF_MODULE_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
|
||||
endforeach()
|
||||
```
|
||||
|
||||
**Adding a module requires no CMake change.** The same trick generates the package manager switches (`:146` regex-scans `src/modules/packages/option.h` for `FF_PACKAGES_FLAG_*_BIT`).
|
||||
Because the glob matches sources rather than directories, a directory counts as a module only when `src/modules/<name>/<name>.c` exists. Empty directories — which git does not track, so they are easy to leave behind — and stray files are ignored instead of breaking the configure step with `Cannot find source file`.
|
||||
|
||||
### Module sources are discovered automatically
|
||||
|
||||
`FF_MODULE_DIRS` also drives the source list (`CMakeLists.txt:516`):
|
||||
|
||||
```cmake
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/modules/${FF_MODULE_DIR}/${FF_MODULE_DIR}.c
|
||||
)
|
||||
endforeach()
|
||||
```
|
||||
|
||||
Consequences:
|
||||
|
||||
- `src/modules/<name>/<name>.c` is compiled as soon as the file exists — **there is no source list to edit for the module layer**.
|
||||
- The file name must match the directory name. A directory whose `.c` file is named differently, or has none at all, is silently not a module: a typo therefore shows up as a module missing from `fastfetch --list-modules`, not as a build error.
|
||||
- The glob uses `CONFIGURE_DEPENDS`, so with the Makefile and Ninja generators the build re-evaluates it and re-runs CMake when a module source is added or removed. With other generators, or to be safe, re-run `cmake -B build` explicitly.
|
||||
|
||||
Only the module layer is automated. Sources under `src/detection/` and `src/common/impl/` are **not** globbed and must still be listed by hand — see [step 5](#5-add-the-platform-sources-to-cmakeliststxt).
|
||||
|
||||
---
|
||||
|
||||
@@ -276,9 +305,7 @@ typedef struct FFFooOptions {
|
||||
static_assert(sizeof(FFFooOptions) <= FF_OPTION_MAX_SIZE, "FFFooOptions size exceeds maximum allowed size");
|
||||
```
|
||||
|
||||
`FFModuleArgs` must come first. It provides `key`, `format`, `outputColor`, `keyColor`, `keyIcon` and `keyWidth`; because it is the first field, `ffJsonConfigParseModuleArgs()` can handle these generically and you get them **for free — no parsing code to write**.
|
||||
|
||||
Note these fields can only be set through the JSON config; CLI module options have been removed (see [Gotchas](#4-cli-module-options-are-removed)).
|
||||
`FFModuleArgs` must come first. It provides `key`, `format`, `outputColor`, `keyColor`, `keyIcon` and `keyWidth`; because it is the first field, `ffJsonConfigParseModuleArgs()` handles these generically, **so no parsing code is required**.
|
||||
|
||||
### 2. Implement detection
|
||||
|
||||
@@ -307,7 +334,7 @@ const char* ffDetectFoo(const FFFooOptions* options, FFFooResult* result) {
|
||||
}
|
||||
```
|
||||
|
||||
**Write one file per target platform, and a `foo_nosupport.c` fallback for the rest.** There are already 45 `*_nosupport.c` files doing exactly this.
|
||||
**Write one file per target platform, plus a `foo_nosupport.c` fallback for the remaining platforms.** The repository currently contains 45 `*_nosupport.c` files serving this purpose.
|
||||
|
||||
### 3. Implement the module
|
||||
|
||||
@@ -339,11 +366,11 @@ FFModuleBaseInfo ffFooModuleInfo = {
|
||||
|
||||
Points to note:
|
||||
|
||||
- **`displayName`** is a literal block of all 20 language fields — there is no shortcut macro, so copy the layout from a neighboring module: **`en`, `ar`, `cs`, `de`, `es`, `fr`, `gl`, `he`, `id`, `it`, `ja`, `ko`, `pl`, `pt`, `ru`, `tr`, `uk`, `vi`, `zh_CN`, `zh_TW`**.
|
||||
**Fill in all 20.** The struct is read by byte offset (see [Gotchas](#6-localization-uses-byte-offsets-not-enums)) and there is **no fallback** — a missing field means the key prints empty in that language.
|
||||
- **`formatArgs`** must be exhaustive. It drives the placeholder list printed by `fastfetch -h foo-format`; **anything you omit is invisible and unusable to the user.**
|
||||
- **`displayName`** is a literal block of all 20 language fields; there is no shortcut macro, so copy the layout from an existing module: **`en`, `ar`, `cs`, `de`, `es`, `fr`, `gl`, `he`, `id`, `it`, `ja`, `ko`, `pl`, `pt`, `ru`, `tr`, `uk`, `vi`, `zh_CN`, `zh_TW`**.
|
||||
**Fill in all 20.** The struct is read by byte offset (see [Pitfalls](#6-localization-uses-byte-offsets-not-enums)) and there is **no fallback** — a missing field means the key prints empty in that language.
|
||||
- **`formatArgs`** must be exhaustive. It drives the placeholder list printed by `fastfetch -h foo-format`; **any placeholder omitted here is neither listed nor usable by the user.**
|
||||
- The `moduleFormat` section in `doc/json_schema.json` is generated by `fastfetch -h format-json`. Its metadata comes from `FFModuleBaseInfo::formatArgs`. To keep the schema and module descriptors consistent, do not edit the `moduleFormat` section in `doc/json_schema.json` by hand; update the module metadata and regenerate it instead.
|
||||
- **`defaultOrder`**: use the current maximum plus 1. Search the existing descriptors for `.defaultOrder =` before choosing a value; do not copy a hard-coded value from this document. Leaving it out, or setting it to `0`, makes the module **disappear** from the interactive `--gen-config` picker. Only `logo`, `command` and `custom` do this deliberately, because they need user arguments or are invoked directly by the display layer.
|
||||
- **`defaultOrder`**: use the current maximum plus 1. Search the existing descriptors for `.defaultOrder =` before choosing a value; do not copy a hard-coded value from this document. Leaving it out, or setting it to `0`, excludes the module from the interactive `--gen-config` picker. Only `logo`, `command` and `custom` rely on this behavior, because they require user arguments or are invoked directly by the display layer.
|
||||
|
||||
### 4. Register it
|
||||
|
||||
@@ -358,11 +385,46 @@ Points to note:
|
||||
|
||||
`FF_MODULE_DISABLE_FOO` is generated by CMake — you do not define it yourself.
|
||||
|
||||
### 5. Modules that need warm-up
|
||||
### 5. Add the platform sources to `CMakeLists.txt`
|
||||
|
||||
If your module needs a sampling interval (CPU usage) or a network round-trip (public IP, weather), also implement `ffPrepareFoo()` and register it in the switch inside `ffPrepareCommandOption()` in `common/impl/commandoption.c`, under the right first-letter `case`. Six modules do this today: CPUUsage, DiskIO, NetIO, PublicIP, Top and Weather.
|
||||
The module layer is discovered by glob (see [Build-time module discovery](#module-sources-are-discovered-automatically)), **but `src/detection/**` and `src/common/impl/**` are not**. `CMakeLists.txt` lists them explicitly, inside one mutually exclusive chain of platform blocks:
|
||||
|
||||
### 6. Verify
|
||||
| Block | Line | Covers |
|
||||
|---|---|---|
|
||||
| `if(LINUX)` | `CMakeLists.txt:522` | Linux |
|
||||
| `elseif(ANDROID)` | `CMakeLists.txt:608` | Android (Termux) |
|
||||
| `elseif(FreeBSD)` | `CMakeLists.txt:691` | FreeBSD, MidnightBSD, DragonFly |
|
||||
| `elseif(NetBSD)` | `CMakeLists.txt:788` | NetBSD |
|
||||
| `elseif(OpenBSD)` | `CMakeLists.txt:872` | OpenBSD |
|
||||
| `elseif(APPLE)` | `CMakeLists.txt:959` | macOS / iOS |
|
||||
| `elseif(WIN32)` | `CMakeLists.txt:1046` | Windows |
|
||||
| `elseif(SunOS)` | `CMakeLists.txt:1121` | Solaris / illumos |
|
||||
| `elseif(Haiku)` | `CMakeLists.txt:1204` | Haiku |
|
||||
| `elseif(GNU)` | `CMakeLists.txt:1282` | GNU/Hurd |
|
||||
|
||||
Add the platform implementation to every block whose platform it supports, and `foo_nosupport.c` to every remaining block, so that all ten platforms still link:
|
||||
|
||||
```cmake
|
||||
elseif(FreeBSD)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
...
|
||||
src/detection/foo/foo_bsd.c
|
||||
)
|
||||
```
|
||||
|
||||
Points to note:
|
||||
|
||||
- Exactly one block is compiled per build, so a file omitted from a block does not exist for that platform, and the link fails with an undefined reference to `ffDetectFoo()` — **on that platform only**. A missing entry therefore builds fine locally and fails in CI; this is why every block must be covered.
|
||||
- A platform may reuse another platform's implementation instead of a stub. `src/common/impl/networking_linux.c`, for example, is listed in nine of the ten blocks (all but `WIN32`). Check where the closest sibling module points before adding a new file.
|
||||
- `DragonFly` is handled by the inner `if(DragonFly)` sub-block inside the FreeBSD block (`CMakeLists.txt:773`); add the variant there, as `processes`, `top` and `wifi` do.
|
||||
- New helpers under `src/common/impl/` follow the same rule: they are not globbed, and each block that needs one must list it.
|
||||
- A few files are appended outside the platform chain because they depend on an option or on a specific feature — for example the proprietary GPU backends (`CMakeLists.txt:1379`) and `src/common/impl/wcwidth.c` (`CMakeLists.txt:1408`). Those are written by hand as well.
|
||||
|
||||
### 6. Modules that need warm-up
|
||||
|
||||
If your module needs a sampling interval (CPU usage) or a network round-trip (public IP, weather), also implement `ffPrepareFoo()` and register it in the switch inside `ffPrepareCommandOption()` in `common/impl/commandoption.c`, under the matching first-letter `case`. Six modules currently do this: CPUUsage, DiskIO, NetIO, PublicIP, Top and Weather.
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```sh
|
||||
cmake -B build && cmake --build build -j
|
||||
@@ -371,7 +433,9 @@ cmake -B build && cmake --build build -j
|
||||
./build/fastfetch --gen-config # confirm it appears in the picker
|
||||
```
|
||||
|
||||
Note the `-format` suffix on the help flag: `fastfetch -h foo` does not work, only `fastfetch -h foo-format`.
|
||||
Note the `-format` suffix on the help flag: `fastfetch -h foo` is not supported; only `fastfetch -h foo-format` works.
|
||||
|
||||
If you added detection sources, re-check [step 5](#5-add-the-platform-sources-to-cmakeliststxt) before pushing: a platform block you missed compiles fine locally and fails only when that platform is built.
|
||||
|
||||
---
|
||||
|
||||
@@ -379,13 +443,13 @@ Note the `-format` suffix on the help flag: `fastfetch -h foo` does not work, on
|
||||
|
||||
A new logo **must** have a corresponding "Logo Request" issue, linked from the PR with `Closes #1234`. Logo PRs without a linked issue are not accepted.
|
||||
|
||||
### 1. Drop in the ASCII file
|
||||
### 1. Add the ASCII file
|
||||
|
||||
```
|
||||
src/logo/ascii/<first-letter>/<distro>.txt
|
||||
```
|
||||
|
||||
For example `src/logo/ascii/o/omarchy.txt`. Directories are already split by first letter (`a/` … `z/`, plus `_/`).
|
||||
For example `src/logo/ascii/d/distro.txt`. Directories are already split by first letter (`a/` … `z/`, plus `_/`).
|
||||
|
||||
The file is plain ASCII art with `$1` … `$9` as color placeholders:
|
||||
|
||||
@@ -395,26 +459,29 @@ $3 /\\\///
|
||||
$2refined.$1 /\\\\//
|
||||
```
|
||||
|
||||
- `$1` … `$9` map to palette slots 1–9; at most 9 (`FASTFETCH_LOGO_MAX_COLORS = 9`)
|
||||
- `$1` … `$9` map to palette slots 1–9; at most 9 are supported (`FASTFETCH_LOGO_MAX_COLORS = 9`)
|
||||
- `$$` is a literal `$`
|
||||
- Characters without a placeholder inherit the current color. Of the 530 existing logos, 239 use no placeholders at all (monochrome) and 291 do — **new logos should use placeholders**; monochrome is a legacy style
|
||||
- Characters without a placeholder inherit the current color. Of the 530 existing logos, 239 use no placeholders at all (monochrome) and 291 do; **new logos should use placeholders**, as monochrome is a legacy style
|
||||
- Tabs are expanded to 4 spaces
|
||||
- Colors can be overridden with `--logo-color-1` … `--logo-color-9`
|
||||
|
||||
### 2. Register it in the `.inc`
|
||||
|
||||
CMake turns each `.txt` into a `FASTFETCH_DATATEXT_LOGO_<UPPERCASE>` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/<first-letter>.inc`:
|
||||
CMake turns each `.txt` file into a `FASTFETCH_DATATEXT_LOGO_<UPPERCASE>` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/<first-letter>.inc`:
|
||||
|
||||
```c
|
||||
#ifdef FASTFETCH_DATATEXT_LOGO_OMARCHY
|
||||
// Omarchy
|
||||
// src/logo/ascii/d.inc
|
||||
|
||||
#ifdef FASTFETCH_DATATEXT_LOGO_DISTRO
|
||||
// Distro
|
||||
{
|
||||
.names = { "omarchy", "Omarchy" },
|
||||
.lines = FASTFETCH_DATATEXT_LOGO_OMARCHY,
|
||||
.names = { "Distro" }, // ID (preferred) or NAME from /etc/os-release, do NOT add both
|
||||
.lines = FASTFETCH_DATATEXT_LOGO_DISTRO,
|
||||
.colors = {
|
||||
FF_COLOR_FG_BLUE,
|
||||
FF_COLOR_FG_WHITE,
|
||||
FF_COLOR_FG_CYAN,
|
||||
FF_COLOR_FG_PRIMARY, // recommended for using as WHITE replacement, light-theme terminal friendly
|
||||
FF_COLOR_FG_BLUE, // preferred
|
||||
FF_COLOR_FG_256 "34",
|
||||
FF_COLOR_FG_RGB "0;255;0", // not recommended because of bad compatibility with raw TTY
|
||||
},
|
||||
},
|
||||
#endif
|
||||
@@ -431,13 +498,13 @@ If the same OS has multiple logo variants, mark the variant explicitly with `.ty
|
||||
.type = FF_LOGO_LINE_TYPE_SMALL_BIT,
|
||||
```
|
||||
|
||||
Use `FF_LOGO_LINE_TYPE_ALTER_BIT` for an alternate logo, `FF_LOGO_LINE_TYPE_SMALL_BIT` for a small logo, or combine the flags when both apply. This is also a lookup optimization: a logo marked `FF_LOGO_LINE_TYPE_SMALL_BIT` is considered only for `type = small`, while a logo marked `FF_LOGO_LINE_TYPE_ALTER_BIT` is never selected by automatic detection. Alternate logos are available only when explicitly requested through `--logo <source>`.
|
||||
Use `FF_LOGO_LINE_TYPE_ALTER_BIT` for an alternate logo, `FF_LOGO_LINE_TYPE_SMALL_BIT` for a small logo, or combine the flags when both apply. This is also a lookup optimization: a logo marked `FF_LOGO_LINE_TYPE_SMALL_BIT` is considered only for `type = small`, while a logo marked `FF_LOGO_LINE_TYPE_ALTER_BIT` is never selected by automatic detection. Alternate logos are available only when explicitly requested through `-l <source>`.
|
||||
|
||||
### 3. Verify
|
||||
|
||||
```sh
|
||||
./build/fastfetch --logo omarchy
|
||||
./build/fastfetch --list-logos | grep -i omarchy
|
||||
./build/fastfetch -l distro
|
||||
./build/fastfetch --list-logos | grep -i distro
|
||||
```
|
||||
|
||||
---
|
||||
@@ -474,9 +541,9 @@ Current distribution of platform files in `detection/`:
|
||||
|
||||
(These are repository inventory figures, counting `.c`, `.m`, `.cpp`, and `.h`; they may change as platforms and modules are added.)
|
||||
|
||||
**Don't pile `#ifdef __linux__` into one file.** When adding platform support, copy the closest existing implementation and change the suffix.
|
||||
**Do not accumulate `#ifdef __linux__` blocks in a single file.** When adding platform support, copy the closest existing implementation and change the suffix.
|
||||
|
||||
`common/` uses the same idea: `common/impl/` contains `io_unix.c` / `io_windows.c`, `netif_linux.c` / `netif_apple.c` / `netif_bsd.c` and so on, with `common/apple/`, `common/windows/` and `common/haiku/` holding platform-specific helpers.
|
||||
`common/` follows the same convention: `common/impl/` contains `io_unix.c` / `io_windows.c`, `netif_linux.c` / `netif_apple.c` / `netif_bsd.c` and similar files, with `common/apple/`, `common/windows/` and `common/haiku/` holding platform-specific helpers.
|
||||
|
||||
When several platform suffixes could apply, use the most specific implementation supported by the build system (for example, `_nbsd.c` instead of the generic `_bsd.c` on NetBSD). Keep the generic file as the fallback for platforms that share its conventions.
|
||||
|
||||
@@ -507,7 +574,7 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
|
||||
|
||||
`src/3rdparty/**`, `build/**` and `src/logo/builtin.c` are listed in `.clang-format-ignore` — **do not reformat them.**
|
||||
|
||||
`.editorconfig` adds LF line endings, 4-space indent, a final newline, and trailing whitespace trimmed (except in Markdown).
|
||||
`.editorconfig` specifies LF line endings, a 4-space indent, a final newline, and trailing whitespace trimmed (except in Markdown).
|
||||
|
||||
### Naming
|
||||
|
||||
@@ -520,10 +587,6 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
|
||||
| Module options | `FF<Name>Options` | `FFCPUOptions` |
|
||||
| Detection results | `FF<Name>Result` | `FFCPUResult` |
|
||||
|
||||
### Spelling
|
||||
|
||||
CI runs codespell (`.codespellrc`). Known false positives live 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:
|
||||
@@ -566,7 +629,7 @@ Common verbs: `adds`, `removes`, `fixes`, `improves`, `updates`, `corrects`, `pr
|
||||
|
||||
Scopes in use: `Top`, `Processes`, `Memory`, `Logo (Builtin)`, `CI`, `Doc`, `Presets`, `Global`, plus individual module names.
|
||||
|
||||
Documentation-only changes get a `[ci skip]` suffix.
|
||||
Documentation-only changes use a `[ci skip]` suffix.
|
||||
|
||||
---
|
||||
|
||||
@@ -617,21 +680,21 @@ 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 (it depends too heavily on a real system) and 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), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs.
|
||||
|
||||
**If you touch `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, please extend the matching test.**
|
||||
**If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
|
||||
|
||||
---
|
||||
|
||||
## Pull requests
|
||||
|
||||
1. **Open an issue first** (feature request / bug report / logo request) so you don't waste effort. Templates are in `.github/ISSUE_TEMPLATE/`.
|
||||
1. **Open an issue first** (feature request / bug report / logo request) to confirm that the change is wanted before investing effort. Templates are in `.github/ISSUE_TEMPLATE/`.
|
||||
2. Branch off `dev` — **`dev` is the main development branch**, not `master`.
|
||||
3. Follow the [commit message convention](#commit-messages).
|
||||
4. Update `CHANGELOG.md` for user-visible changes.
|
||||
5. Open the PR against `dev` and fill in `.github/pull_request_template.md`:
|
||||
- Summary
|
||||
- Related issue (**required for new logos**, otherwise the PR won't be accepted)
|
||||
- Related issue (**required for new logos**; otherwise the PR is not accepted)
|
||||
- Changes
|
||||
- Screenshots (required for visual changes)
|
||||
- Checklist: confirm you tested locally
|
||||
@@ -640,7 +703,6 @@ Coverage focuses on the core data structures and the formatting engine in `commo
|
||||
|
||||
```sh
|
||||
clang-format -i <changed files> # format
|
||||
codespell # spelling
|
||||
cmake -B build -DBUILD_TESTS=On && cmake --build build -j
|
||||
cd build && ctest --output-on-failure # tests
|
||||
./build/fastfetch --format json # verify JSON output is well-formed
|
||||
@@ -649,13 +711,13 @@ cd build && ctest --output-on-failure # tests
|
||||
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
## Pitfalls
|
||||
|
||||
Things that are easy to get wrong when reading this codebase. Most of them follow from the "obsessively fast startup" goal.
|
||||
The following points are easy to misinterpret when reading this codebase. Most of them follow from the startup-time objective described above.
|
||||
|
||||
### 1. Option structs must not exceed 256 bytes
|
||||
|
||||
`FF_OPTION_MAX_SIZE = 1 << 8`. Exceeding it is caught at compile time by `static_assert`. Don't try to raise the value — it determines the stack cost of every module invocation.
|
||||
`FF_OPTION_MAX_SIZE = 1 << 8`. Exceeding it is caught at compile time by `static_assert`. Do not attempt to increase the value: it determines the stack cost of every module invocation.
|
||||
|
||||
### 2. `FF_MODULE_DISABLE_*` controls registration, not compilation
|
||||
|
||||
@@ -668,9 +730,11 @@ Things that are easy to get wrong when reading this codebase. Most of them follo
|
||||
|
||||
**Disabling modules in a Debug build does not shrink the binary.** LTO is enabled whenever `CMAKE_BUILD_TYPE != Debug`, and the default `RelWithDebInfo` already satisfies that — so measure size with `RelWithDebInfo` or `Release`, never with `Debug`. (The "Release mode" wording in the comment above is imprecise.)
|
||||
|
||||
### 3. CMake globs don't trigger reconfiguration
|
||||
### 3. Only two things are discovered by glob
|
||||
|
||||
Both the module directories and the logo files are discovered by glob. CMake will not notice new files on its own — you need to re-run `cmake -B build` (or delete `build/CMakeCache.txt` and start over). This is the classic CMake footgun.
|
||||
The module sources (`CMakeLists.txt:134`) and the logo `.txt` files (`CMakeLists.txt:429`). Both use `CONFIGURE_DEPENDS`, so with the Makefile and Ninja generators the build re-checks the glob and re-runs CMake when the result changes; other generators (Visual Studio and Xcode in particular) do not track it as reliably. Re-run `cmake -B build` after adding a module source or a logo file rather than relying on that behavior.
|
||||
|
||||
Everything else — `src/detection/**` and `src/common/impl/**`, and any source outside the platform chain — must be listed in `CMakeLists.txt` by hand; see [step 5](#5-add-the-platform-sources-to-cmakeliststxt).
|
||||
|
||||
### 4. CLI module options are removed
|
||||
|
||||
@@ -686,9 +750,9 @@ Error: Unsupported module option: --cpu-temp
|
||||
|
||||
### 5. `defaultOrder = 0` hides a module from `--gen-config`
|
||||
|
||||
`collectModuleInfos` (`genconfig.c:186`) skips any module whose `defaultOrder` is `0`. C zero-initializes the field, so **omitting it is the same as setting it to 0**. Only `logo`, `command` and `custom` do this on purpose.
|
||||
`collectModuleInfos` (`genconfig.c:186`) skips any module whose `defaultOrder` is `0`. C zero-initializes the field, so **omitting it is the same as setting it to 0**. Only `logo`, `command` and `custom` rely on this intentionally.
|
||||
|
||||
`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on runtime output order.
|
||||
`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on the runtime output order.
|
||||
|
||||
### 6. Localization uses byte offsets, not enums
|
||||
|
||||
@@ -699,19 +763,23 @@ Error: Unsupported module option: --cpu-temp
|
||||
(*(const char**) ((uint8_t*) &ff ## moduleName ## ModuleInfo.displayName + instance.config.display.keyLanguage))
|
||||
```
|
||||
|
||||
Consequence: **the field order of `FFModuleDisplayName` must never change** — reordering silently scrambles every language.
|
||||
Consequence: **the field order of `FFModuleDisplayName` must never change** — reordering the fields silently corrupts the output for every language.
|
||||
|
||||
### 7. The `multithreading` switch barely does anything
|
||||
### 7. The `multithreading` option has limited effect
|
||||
|
||||
The global `multithreading` option currently takes effect in exactly one place: `common/impl/networking_linux.c:339`. Modules are still printed sequentially. Modules that need concurrency go through the `ffPrepare*` warm-up hooks instead, which start sampling or fire off requests before the print loop begins.
|
||||
|
||||
### 8. Don't print or read config inside `detection/`
|
||||
This may change in the future. The main blocking issue is that there are dependencies between different modules.
|
||||
|
||||
### 8. Do not print or read configuration inside `detection/`
|
||||
|
||||
The `detection/` layer must stay pure: read system state, fill a struct, return an error string. Any `printf` or any read of `instance.config` there is a design error.
|
||||
|
||||
Use `FF_DEBUG` (`src/common/debug.h`) for logging.
|
||||
|
||||
### 9. `src/logo/builtin.c` and `3rdparty/` are formatting-exempt
|
||||
|
||||
The former is a large generated/hand-maintained data table, the latter is upstream code. Both are excluded via `.clang-format-ignore` — leave them alone.
|
||||
The former is a large generated and hand-maintained data table; the latter is upstream code. Both are excluded via `.clang-format-ignore`; do not reformat either.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
[](https://formulae.brew.sh/formula/fastfetch#default)
|
||||
[](https://github.com/fastfetch-cli/fastfetch/releases)
|
||||
[](https://github.com/fastfetch-cli/fastfetch/releases)
|
||||
[](https://repology.org/project/fastfetch/versions)
|
||||
[](https://repology.org/project/fastfetch/versions)
|
||||
[](https://repology.amdmi3.ru/project/fastfetch/versions)
|
||||
[](https://repology.amdmi3.ru/project/fastfetch/versions)
|
||||
[](https://deepwiki.com/fastfetch-cli/fastfetch)
|
||||
[](README-cn.md)
|
||||
|
||||
@@ -36,8 +36,8 @@ There are [screenshots on different platforms](https://github.com/fastfetch-cli/
|
||||
|
||||
Some distributions package outdated versions of fastfetch. Older versions receive no support, so please always try to use the latest version.
|
||||
|
||||
<a href="https://repology.org/project/fastfetch/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right">
|
||||
<a href="https://repology.amdmi3.ru/project/fastfetch/versions">
|
||||
<img src="https://repology.amdmi3.ru/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
* Ubuntu: [`ppa:zhangsongcui3371/fastfetch`](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) (Ubuntu 22.04 or newer; latest version)
|
||||
@@ -248,7 +248,20 @@ Set the key to a white space.
|
||||
|
||||
### Q: How can I display images on Windows?
|
||||
|
||||
As of April 2025:
|
||||
#### Windows Terminal
|
||||
|
||||
Windows Terminal supports the sixel image protocol only.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "sixel", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.png", // ~ can be used to refer to the Windows user home directory
|
||||
"width": <image-width-in-chars>, // Optional
|
||||
"height": <image-height-in-chars> // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### mintty and Wezterm
|
||||
|
||||
@@ -260,38 +273,7 @@ In `config.jsonc`:
|
||||
"logo": {
|
||||
"type": "iterm",
|
||||
"source": "C:/path/to/image.png",
|
||||
"width": <num-in-chars>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Windows Terminal
|
||||
|
||||
Windows Terminal supports the sixel image protocol only.
|
||||
|
||||
* If you installed fastfetch through MSYS2:
|
||||
1. Install imagemagick: `pacman -S mingw-w64-<subsystem>-x86_64-imagemagick`
|
||||
2. In `config.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "sixel", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.png", // Do NOT use `~` as fastfetch is a native Windows program and doesn't apply cygwin path conversion
|
||||
"width": <image-width-in-chars>, // Optional
|
||||
"height": <image-height-in-chars> // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
* If you installed fastfetch via scoop or downloaded the binary directly from the GitHub Releases page:
|
||||
1. Convert your image manually to sixel format using [any online image conversion service](https://www.google.com/search?q=convert+image+to+sixel)
|
||||
2. In `config.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "raw", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.sixel",
|
||||
"width": <image-width-in-chars>, // Required
|
||||
"height": <image-height-in-chars> // Required
|
||||
"width": <image-width-in-chars>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
;;
|
||||
|
||||
Vendored
+6
@@ -1,3 +1,9 @@
|
||||
fastfetch (2.68.1~#UBUNTU_CODENAME#) #UBUNTU_CODENAME#; urgency=medium
|
||||
|
||||
* Update to 2.68.1
|
||||
|
||||
-- Carter Li <zhangsongcui@live.cn> Wed, 02 Sep 2026 10:47:51 +0800
|
||||
|
||||
fastfetch (2.68.0~#UBUNTU_CODENAME#) #UBUNTU_CODENAME#; urgency=medium
|
||||
|
||||
* Update to 2.68.0
|
||||
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@ Source: fastfetch
|
||||
Section: universe/utils
|
||||
Priority: optional
|
||||
Maintainer: Carter Li <zhangsongcui@live.cn>
|
||||
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libpulse-dev, libdrm-dev, libddcutil-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
|
||||
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libdrm-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
|
||||
Standards-Version: 4.0.0
|
||||
Homepage: https://github.com/fastfetch-cli/fastfetch
|
||||
|
||||
|
||||
+36
-4
@@ -195,6 +195,7 @@
|
||||
"file-raw": "Text file, printed as is",
|
||||
"data": "Text data, printed with color code replacement",
|
||||
"data-raw": "Text data, printed as is",
|
||||
"command-raw": "Command to run, whose output is printed as is",
|
||||
"sixel": "Image file, rendered as sixel",
|
||||
"kitty": "Image file, rendered using the Kitty graphics protocol",
|
||||
"kitty-direct": "Image file, tells the terminal emulator to read image data from the specified file",
|
||||
@@ -269,6 +270,14 @@
|
||||
"type": "num"
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-padding-bottom",
|
||||
"desc": "Set the padding at the bottom of the logo",
|
||||
"remark": "Only used with \"--logo-position top\"",
|
||||
"arg": {
|
||||
"type": "num"
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-print-remaining",
|
||||
"desc": "Specify whether to print remaining logo lines when the logo is taller than the module list",
|
||||
@@ -284,6 +293,7 @@
|
||||
"arg": {
|
||||
"type": "enum",
|
||||
"enum": {
|
||||
"auto": "Automatically choose left or top based on terminal width",
|
||||
"left": "Left",
|
||||
"top": "Top",
|
||||
"right": "Right"
|
||||
@@ -291,12 +301,33 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-recache",
|
||||
"desc": "If true, regenerate the image logo cache",
|
||||
"long": "logo-cache",
|
||||
"desc": "Specify how the image logo cache is used",
|
||||
"remark": "See \"--help logo-animation-frame\" for the one case where a cached rendering can outlive the image type that produced it",
|
||||
"arg": {
|
||||
"type": "bool",
|
||||
"type": "enum",
|
||||
"optional": true,
|
||||
"default": false
|
||||
"default": "true",
|
||||
"enum": {
|
||||
"true": "Reuse a cached rendering when it is valid, and write it back on a cache miss",
|
||||
"false": "Ignore the image logo cache completely: neither read nor write it",
|
||||
"regen": "Ignore any existing cached rendering and regenerate it"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-animation-frame",
|
||||
"desc": "Select which frame of an animated image logo to render",
|
||||
"remark": [
|
||||
"Animated kitty graphics only, and only if the image source is animated; 1 or higher renders that frame as a still image, 0 plays the animation",
|
||||
"A selected frame is cached exactly like the first frame is, sharing its entries; the frame number is recorded next to them, so changing this option re-renders",
|
||||
"That record is per cache directory, not per image type: switching --logo-type between two types that both have a cached rendering here, and asking for a different frame, can pair one type's rendering with the other's frame number",
|
||||
"Run \"--logo-cache regen\" after such a switch"
|
||||
],
|
||||
"arg": {
|
||||
"type": "num",
|
||||
"optional": true,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -599,6 +630,7 @@
|
||||
"cs": "Czech",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fi": "Finnish",
|
||||
"fr": "French",
|
||||
"gl": "Galician",
|
||||
"he": "Hebrew",
|
||||
|
||||
+118
-61
@@ -19,15 +19,14 @@
|
||||
},
|
||||
"key": {
|
||||
"description": "Module key\nUse a single space (` `) to hide the key",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"type": "string"
|
||||
},
|
||||
"keyColor": {
|
||||
"description": "Color of the module key. Overrides `display.color.key`",
|
||||
"description": "Color of the module key. Overrides `display.color.keys`",
|
||||
"$ref": "#/$defs/colors"
|
||||
},
|
||||
"keyWidth": {
|
||||
"description": "Width of the module key. Overrides `display.keyWidth`",
|
||||
"description": "Width of the module key. Overrides `display.key.width`",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
@@ -131,7 +130,9 @@
|
||||
"SunOS",
|
||||
"OpenBSD",
|
||||
"NetBSD",
|
||||
"Haiku"
|
||||
"Haiku",
|
||||
"GNU",
|
||||
"Unknown"
|
||||
]
|
||||
},
|
||||
"architectures": {
|
||||
@@ -150,7 +151,9 @@
|
||||
"sparc",
|
||||
"alpha",
|
||||
"hppa",
|
||||
"m68k"
|
||||
"sh",
|
||||
"m68k",
|
||||
"Unknown"
|
||||
]
|
||||
},
|
||||
"conditions": {
|
||||
@@ -274,19 +277,19 @@
|
||||
|
||||
|
||||
"batteryFormat": {
|
||||
"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)",
|
||||
"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)",
|
||||
"type": "string"
|
||||
},
|
||||
"biosFormat": {
|
||||
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type",
|
||||
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type *",
|
||||
"type": "string"
|
||||
},
|
||||
"bluetoothFormat": {
|
||||
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {battery-percentage}: Battery percentage number\n 5. {connected}: Is connected\n 6. {battery-percentage-bar}: Battery percentage bar",
|
||||
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {device-type}: Bluetooth stacks the device answers on\n 5. {battery-percentage}: Battery percentage number\n 6. {battery-percentage-bar}: Battery percentage bar\n 7. {signal-quality}: Signal quality number\n 8. {signal-quality-bar}: Signal quality bar\n 9. {connected}: Is connected",
|
||||
"type": "string"
|
||||
},
|
||||
"bluetoothradioFormat": {
|
||||
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
|
||||
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering *\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
|
||||
"type": "string"
|
||||
},
|
||||
"boardFormat": {
|
||||
@@ -298,11 +301,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"brightnessFormat": {
|
||||
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
|
||||
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name *\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
|
||||
"type": "string"
|
||||
},
|
||||
"btrfsFormat": {
|
||||
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
|
||||
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label *\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
|
||||
"type": "string"
|
||||
},
|
||||
"cameraFormat": {
|
||||
@@ -314,7 +317,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"codecFormat": {
|
||||
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name\n 2. {direction}: Decoder / Encoder\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
|
||||
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name *\n 2. {direction}: Decoder / Encoder *\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
|
||||
"type": "string"
|
||||
},
|
||||
"commandFormat": {
|
||||
@@ -322,11 +325,11 @@
|
||||
"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\n 14. {technology}: CPU technology",
|
||||
"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)",
|
||||
"type": "string"
|
||||
},
|
||||
"cpucacheFormat": {
|
||||
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result",
|
||||
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result\n 3. {level}: Cache level *",
|
||||
"type": "string"
|
||||
},
|
||||
"cpuusageFormat": {
|
||||
@@ -346,15 +349,15 @@
|
||||
"type": "string"
|
||||
},
|
||||
"displayFormat": {
|
||||
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name\n 7. {type}: Screen type (Built-in or External)\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
|
||||
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name *\n 7. {type}: Screen type (Built-in or External) *\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
|
||||
"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\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",
|
||||
"type": "string"
|
||||
},
|
||||
"diskioFormat": {
|
||||
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name\n 4. {dev-path}: Device raw file path\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
|
||||
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name *\n 4. {dev-path}: Device raw file path *\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
|
||||
"type": "string"
|
||||
},
|
||||
"dnsFormat": {
|
||||
@@ -390,7 +393,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"kernelFormat": {
|
||||
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {display-version}: Display version\n 6. {page-size}: Page size",
|
||||
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {page-size}: Page size",
|
||||
"type": "string"
|
||||
},
|
||||
"keyboardFormat": {
|
||||
@@ -410,7 +413,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"localipFormat": {
|
||||
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address\n 4. {ifname}: Interface name\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
|
||||
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address *\n 4. {ifname}: Interface name *\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
|
||||
"type": "string"
|
||||
},
|
||||
"mediaFormat": {
|
||||
@@ -422,7 +425,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"monitorFormat": {
|
||||
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
|
||||
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name *\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
|
||||
"type": "string"
|
||||
},
|
||||
"mouseFormat": {
|
||||
@@ -430,7 +433,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"netioFormat": {
|
||||
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
|
||||
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name *\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
|
||||
"type": "string"
|
||||
},
|
||||
"openclFormat": {
|
||||
@@ -442,15 +445,15 @@
|
||||
"type": "string"
|
||||
},
|
||||
"osFormat": {
|
||||
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel\n 2. {name}: Name of the OS\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS",
|
||||
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel *\n 2. {name}: Name of the OS *\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS\n 13. {kernel-release}: Release of the kernel",
|
||||
"type": "string"
|
||||
},
|
||||
"packagesFormat": {
|
||||
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {dpkg}: Number of dpkg packages\n 10. {emerge}: Number of emerge packages\n 11. {eopkg}: Number of eopkg packages\n 12. {flatpak-system}: Number of flatpak-system app packages\n 13. {flatpak-user}: Number of flatpak-user app packages\n 14. {guix-home}: Number of guix-home packages\n 15. {guix-system}: Number of guix-system packages\n 16. {guix-user}: Number of guix-user packages\n 17. {hpkg-system}: Number of hpkg-system packages\n 18. {hpkg-user}: Number of hpkg-user packages\n 19. {install-release}: Number of install-release packages\n 20. {kiss}: Number of kiss packages\n 21. {linglong}: Number of linglong packages\n 22. {lpkg}: Number of lpkg packages\n 23. {lpkgbuild}: Number of lpkgbuild packages\n 24. {macports}: Number of macports packages\n 25. {mport}: Number of mport packages\n 26. {moss}: Number of moss packages\n 27. {nix-default}: Number of nix-default packages\n 28. {nix-system}: Number of nix-system packages\n 29. {nix-user}: Number of nix-user packages\n 30. {opkg}: Number of opkg packages\n 31. {pacman}: Number of pacman packages\n 32. {pacman-branch}: Pacman branch on manjaro\n 33. {pacstall}: Number of pacstall packages\n 34. {paludis}: Number of paludis packages\n 35. {pisi}: Number of pisi packages\n 36. {pkg}: Number of pkg packages\n 37. {pkgsrc}: Number of pkgsrc packages\n 38. {pkgtool}: Number of pkgtool packages\n 39. {porg}: Number of porg packages\n 40. {rpm}: Number of rpm packages\n 41. {scoop-global}: Number of scoop-global packages\n 42. {scoop-user}: Number of scoop-user packages\n 43. {snap}: Number of snap packages\n 44. {soar}: Number of soar packages\n 45. {sorcery}: Number of sorcery packages\n 46. {winget}: Number of winget packages\n 47. {xbps}: Number of xbps packages\n 48. {brew-all}: Total number of all brew packages\n 49. {flatpak-all}: Total number of all flatpak app packages\n 50. {guix-all}: Total number of all guix packages\n 51. {hpkg-all}: Total number of all hpkg packages\n 52. {nix-all}: Total number of all nix packages\n 53. {all}: Number of all packages",
|
||||
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {crux}: Number of crux packages\n 10. {dpkg}: Number of dpkg packages\n 11. {emerge}: Number of emerge packages\n 12. {eopkg}: Number of eopkg packages\n 13. {flatpak-system}: Number of flatpak-system app packages\n 14. {flatpak-user}: Number of flatpak-user app packages\n 15. {guix-home}: Number of guix-home packages\n 16. {guix-system}: Number of guix-system packages\n 17. {guix-user}: Number of guix-user packages\n 18. {hpkg-system}: Number of hpkg-system packages\n 19. {hpkg-user}: Number of hpkg-user packages\n 20. {install-release}: Number of install-release packages\n 21. {kiss}: Number of kiss packages\n 22. {linglong}: Number of linglong packages\n 23. {lpkg}: Number of lpkg packages\n 24. {lpkgbuild}: Number of lpkgbuild packages\n 25. {macports}: Number of macports packages\n 26. {mport}: Number of mport packages\n 27. {moss}: Number of moss packages\n 28. {nix-default}: Number of nix-default packages\n 29. {nix-system}: Number of nix-system packages\n 30. {nix-user}: Number of nix-user packages\n 31. {opkg}: Number of opkg packages\n 32. {pacman}: Number of pacman packages\n 33. {pacman-branch}: Pacman branch on manjaro\n 34. {pacstall}: Number of pacstall packages\n 35. {paludis}: Number of paludis packages\n 36. {pisi}: Number of pisi packages\n 37. {pkg}: Number of pkg packages\n 38. {pkgsrc}: Number of pkgsrc packages\n 39. {pkgtool}: Number of pkgtool packages\n 40. {porg}: Number of porg packages\n 41. {rpm}: Number of rpm packages\n 42. {rum}: Number of rum overlay packages\n 43. {scoop-global}: Number of scoop-global packages\n 44. {scoop-user}: Number of scoop-user packages\n 45. {snap}: Number of snap packages\n 46. {soar}: Number of soar packages\n 47. {sorcery}: Number of sorcery packages\n 48. {winget}: Number of winget packages\n 49. {xbps}: Number of xbps packages\n 50. {brew-all}: Total number of all brew packages\n 51. {flatpak-all}: Total number of all flatpak app packages\n 52. {guix-all}: Total number of all guix packages\n 53. {hpkg-all}: Total number of all hpkg packages\n 54. {nix-all}: Total number of all nix packages\n 55. {all}: Number of all packages",
|
||||
"type": "string"
|
||||
},
|
||||
"physicaldiskFormat": {
|
||||
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
|
||||
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name *\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path *\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
|
||||
"type": "string"
|
||||
},
|
||||
"physicalmemoryFormat": {
|
||||
@@ -462,11 +465,11 @@
|
||||
"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}: 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\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
|
||||
"type": "string"
|
||||
},
|
||||
"processesFormat": {
|
||||
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count",
|
||||
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count\n 2. {threads}: Thread count",
|
||||
"type": "string"
|
||||
},
|
||||
"publicipFormat": {
|
||||
@@ -482,7 +485,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"swapFormat": {
|
||||
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name",
|
||||
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name *",
|
||||
"type": "string"
|
||||
},
|
||||
"terminalFormat": {
|
||||
@@ -554,7 +557,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"zpoolFormat": {
|
||||
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name\n 2. {guid}: Zpool guid\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
|
||||
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name *\n 2. {guid}: Zpool guid *\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -766,21 +769,36 @@
|
||||
"description": "Whether to preserve the aspect ratio of the logo. Supported by iTerm image protocol only",
|
||||
"default": false
|
||||
},
|
||||
"recache": {
|
||||
"type": "boolean",
|
||||
"description": "If true, regenerate image logo cache",
|
||||
"default": false
|
||||
"cache": {
|
||||
"description": "How the image logo cache is used. true reuses a cached rendering when it is valid and writes it back on a cache miss; false ignores the cache completely, neither reading nor writing it; \"regen\" ignores any existing cached rendering and regenerates it",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Whether to use the image logo cache"
|
||||
},
|
||||
{
|
||||
"const": "regen",
|
||||
"description": "Regenerate the image logo cache, ignoring any existing cached rendering"
|
||||
}
|
||||
],
|
||||
"default": true
|
||||
},
|
||||
"position": {
|
||||
"type": "string",
|
||||
"description": "Position of the logo",
|
||||
"description": "Position of the logo. Auto places a left-side logo above the output when the terminal is too narrow to reserve 32 columns for module output.",
|
||||
"enum": [
|
||||
"auto",
|
||||
"left",
|
||||
"top",
|
||||
"right"
|
||||
],
|
||||
"default": "left"
|
||||
},
|
||||
"animationFrame": {
|
||||
"type": "integer",
|
||||
"description": "Frame of an animated image to render:\n0 plays the animation (kitty image protocol only), a positive value is the 1-based frame number, and a negative value counts back from the end, so -1 is the last frame.\nOnly the JSON config can express a negative value, as the command line parser reads a leading `-` as another option",
|
||||
"default": 1
|
||||
},
|
||||
"chafa": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -920,6 +938,17 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether to detect and display component versions. Mainly for benchmarking",
|
||||
"default": true
|
||||
},
|
||||
"preload": {
|
||||
"description": "Scripts to run while the config is read, before any module runs",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"lua": {
|
||||
"type": "string",
|
||||
"description": "Lua script to run once, as soon as the config is read, for example `dofile('/path/to/helpers.lua')`"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1094,6 +1123,11 @@
|
||||
"const": "es",
|
||||
"description": "Spanish"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "fi",
|
||||
"description": "Finnish"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "fr",
|
||||
@@ -1562,6 +1596,7 @@
|
||||
"command",
|
||||
"colors",
|
||||
"cursor",
|
||||
"custom",
|
||||
"datetime",
|
||||
"display",
|
||||
"disk",
|
||||
@@ -1581,6 +1616,7 @@
|
||||
"loadavg",
|
||||
"locale",
|
||||
"localip",
|
||||
"logo",
|
||||
"media",
|
||||
"memory",
|
||||
"monitor",
|
||||
@@ -1730,6 +1766,24 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showType": {
|
||||
"description": "Which Bluetooth stacks to look for",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "classic",
|
||||
"description": "Look for BR/EDR devices only"
|
||||
},
|
||||
{
|
||||
"const": "le",
|
||||
"description": "Look for Bluetooth Low Energy devices only"
|
||||
},
|
||||
{
|
||||
"const": "both",
|
||||
"description": "Look for devices on either stack"
|
||||
}
|
||||
],
|
||||
"default": "both"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
@@ -1865,7 +1919,7 @@
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"ddcciSleep": {
|
||||
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection\nSee <https://www.ddcutil.com/performance_options/#option-sleep-multiplier> for details",
|
||||
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null",
|
||||
@@ -2086,7 +2140,7 @@
|
||||
"showPeCoreCount": {
|
||||
"description": "Whether to detect and display CPU frequencies for different core types (for example, P-cores and E-cores), if supported",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"default": true
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
@@ -2120,8 +2174,10 @@
|
||||
"const": "cpucache",
|
||||
"description": "Print CPU cache sizes"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
"compact": {
|
||||
"type": "boolean",
|
||||
"description": "Print one line with a total per cache level instead of one line per level",
|
||||
"default": false
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
@@ -2240,6 +2296,7 @@
|
||||
"description": "Set the number of white spaces to print before the symbol",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"default": 0
|
||||
},
|
||||
"block": {
|
||||
@@ -2251,6 +2308,7 @@
|
||||
"description": "Set the block width in spaces",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 9,
|
||||
"default": 3
|
||||
},
|
||||
"range": {
|
||||
@@ -2308,7 +2366,7 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Run a custom shell command",
|
||||
"description": "Run custom shell scripts",
|
||||
"const": "command"
|
||||
},
|
||||
"shell": {
|
||||
@@ -2370,9 +2428,6 @@
|
||||
"const": "cursor",
|
||||
"description": "Print cursor style name"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2406,8 +2461,7 @@
|
||||
"const": "custom"
|
||||
},
|
||||
"key": {
|
||||
"description": "Leave empty to hide the key",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
"keyColor": {
|
||||
"$ref": "#/$defs/keyColor"
|
||||
@@ -2439,9 +2493,6 @@
|
||||
"const": "datetime",
|
||||
"description": "Print the current date and time"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2701,7 +2752,7 @@
|
||||
"waitTime": {
|
||||
"type": "integer",
|
||||
"description": "Wait time (in ms) used to measure the I/O rate (calculated in bytes/sec).\nAvoid setting this too low, as the kernel requires time to accurately update counters.\nIgnored if 'detectTotal' is true.",
|
||||
"default": 500,
|
||||
"default": 250,
|
||||
"minimum": 1
|
||||
},
|
||||
"key": {
|
||||
@@ -2818,9 +2869,6 @@
|
||||
"const": "editor",
|
||||
"description": "Print information about the default editor (`$VISUAL` or `$EDITOR`)"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2853,9 +2901,6 @@
|
||||
"const": "font",
|
||||
"description": "Print system font names"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -3602,7 +3647,7 @@
|
||||
"waitTime": {
|
||||
"type": "integer",
|
||||
"description": "Wait time (in ms) used to measure the I/O rate (calculated in bytes/sec).\nAvoid setting this too low, as the kernel requires time to accurately update counters.\nIgnored if 'detectTotal' is true.",
|
||||
"default": 500,
|
||||
"default": 250,
|
||||
"minimum": 1
|
||||
},
|
||||
"key": {
|
||||
@@ -3761,18 +3806,24 @@
|
||||
"enum": [
|
||||
"am",
|
||||
"apk",
|
||||
"appimage",
|
||||
"brew",
|
||||
"cards",
|
||||
"choco",
|
||||
"crux",
|
||||
"dpkg",
|
||||
"emerge",
|
||||
"eopkg",
|
||||
"flatpak",
|
||||
"guix",
|
||||
"hpkg",
|
||||
"installrelease",
|
||||
"kiss",
|
||||
"linglong",
|
||||
"lpkg",
|
||||
"lpkgbuild",
|
||||
"macports",
|
||||
"moss",
|
||||
"mport",
|
||||
"nix",
|
||||
"opkg",
|
||||
@@ -3781,10 +3832,14 @@
|
||||
"paludis",
|
||||
"pisi",
|
||||
"pkg",
|
||||
"pkgsrc",
|
||||
"pkgtool",
|
||||
"porg",
|
||||
"rpm",
|
||||
"rum",
|
||||
"scoop",
|
||||
"snap",
|
||||
"soar",
|
||||
"sorcery",
|
||||
"winget",
|
||||
"xbps"
|
||||
@@ -3796,8 +3851,7 @@
|
||||
"description": "Enable all package managers",
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": ["winget"]
|
||||
]
|
||||
},
|
||||
"combined": {
|
||||
"description": "Whether to combine related package managers into single counts (e.g., nix-system + nix-user = nix)",
|
||||
@@ -4024,10 +4078,9 @@
|
||||
"const": "publicip"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL of the public IP detection server to use. Only HTTP is supported",
|
||||
"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",
|
||||
"default": "http://ipinfo.io/ip"
|
||||
"format": "url"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Time in milliseconds to wait for the public IP server to respond\nSet to 0 to disable the timeout",
|
||||
@@ -4085,6 +4138,7 @@
|
||||
"description": "Number of times to repeat the separator string, or 0 to auto-detect",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"default": 0
|
||||
},
|
||||
"condition": {
|
||||
@@ -4498,7 +4552,7 @@
|
||||
"type": "integer",
|
||||
"description": "Wait time between process samples, in milliseconds. Ignored when showTypes contains \"memory\" only",
|
||||
"minimum": 1,
|
||||
"default": 500
|
||||
"default": 250
|
||||
},
|
||||
"compact": {
|
||||
"type": "boolean",
|
||||
@@ -4789,6 +4843,9 @@
|
||||
"const": "wifi",
|
||||
"description": "Print connected Wi-Fi info (SSID, connection and security protocol)"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
|
||||
+2
-2
@@ -99,7 +99,7 @@
|
||||
"gamepad",
|
||||
"mouse",
|
||||
"keyboard",
|
||||
"top",
|
||||
"tpm",
|
||||
"netio",
|
||||
"diskio",
|
||||
{
|
||||
@@ -110,7 +110,7 @@
|
||||
"type": "weather",
|
||||
"timeout": 1000
|
||||
},
|
||||
"tpm",
|
||||
"top",
|
||||
"version",
|
||||
"break",
|
||||
"colors"
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@
|
||||
"gamepad",
|
||||
"mouse",
|
||||
"keyboard",
|
||||
"top",
|
||||
"tpm",
|
||||
"netio",
|
||||
"diskio",
|
||||
{
|
||||
@@ -112,7 +112,7 @@
|
||||
"type": "weather",
|
||||
"timeout": 1000
|
||||
},
|
||||
"tpm",
|
||||
"top",
|
||||
"version",
|
||||
"logo",
|
||||
"break",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
{
|
||||
"type": "disk",
|
||||
"key": "║{#cyan}│ {icon} Disk │{$4}│{#keys}║{$2}",
|
||||
"format": "{size-used} \/ {size-total} ({size-percentage}) - {filesystem}",
|
||||
"format": "{mountpoint} {size-used} \/ {size-total} ({size-percentage}) - {filesystem}",
|
||||
},
|
||||
{
|
||||
"type": "battery",
|
||||
@@ -195,7 +195,7 @@
|
||||
"keyIcon": "",
|
||||
"key": "║{#red}│ {icon} Clang │{$4}│{#keys}║{$2}",
|
||||
"text": "clang --version | findstr version", // Finds the line with "version"
|
||||
"format": "clang {~-6}" // Prints the last 6 characters (version number)
|
||||
"format": "clang {~14,20}" // Prints 14~20 characters (version number)
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
@@ -204,6 +204,13 @@
|
||||
"text": "node --version",
|
||||
"format": "node {~1}" // {~1} removes first character (v)
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"keyIcon": "",
|
||||
"key": "║{#red}│ {icon} Python │{$4}│{#keys}║{$2}",
|
||||
"text": "python --version",
|
||||
"format": "python {~7}" // {~7} removes first 7 characters ("Python" with extra space)
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"keyIcon": "",
|
||||
|
||||
+12
-3
@@ -38,8 +38,14 @@
|
||||
"compactType": "original",
|
||||
"key": "Resolution"
|
||||
},
|
||||
"de",
|
||||
"wm",
|
||||
{
|
||||
"type": "de",
|
||||
"key": "DE"
|
||||
},
|
||||
{
|
||||
"type": "wm",
|
||||
"key": "WM"
|
||||
},
|
||||
"wmtheme",
|
||||
"theme",
|
||||
"icons",
|
||||
@@ -48,7 +54,10 @@
|
||||
"type": "terminalfont",
|
||||
"format": "{/name}{-}{/}{name}{?size} {size}{?}"
|
||||
},
|
||||
"cpu",
|
||||
{
|
||||
"type": "cpu",
|
||||
"showPeCoreCount": false
|
||||
},
|
||||
{
|
||||
"type": "gpu",
|
||||
"key": "GPU",
|
||||
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
Copyright (c) 2014-2016 Hayaki Saito
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
|
||||
src/quant.c is derived from ppmquant, originally by Jef Poskanzer.
|
||||
|
||||
Copyright (C) 1989, 1991 by Jef Poskanzer.
|
||||
Copyright (C) 2001 by Bryan Henderson.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted, provided
|
||||
that the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation. This software is provided "as is" without express or
|
||||
implied warranty.
|
||||
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
"sixel" license (original license)
|
||||
==================================
|
||||
|
||||
src/tosixel.c and src/fromsixel.c are derived from "sixel" original version (2014-3-2)
|
||||
|
||||
Package: http://nanno.dip.jp/softlib/man/rlogin/sixel.tar.gz
|
||||
|
||||
Unofficial repo: https://github.com/saitoha/sixel
|
||||
|
||||
This work is written by kmiya@ culti. He distributes it under very permissive license.
|
||||
|
||||
The original license text(in Japanese) is:
|
||||
|
||||
```
|
||||
このプログラム及びソースコードの使用について個人・商用を問わず
|
||||
|
||||
ご自由に使用していただいで結構です。
|
||||
|
||||
また、配布・転載・紹介もご連絡の必要もありません。
|
||||
|
||||
ソースの改変による配布も自由ですが、どのバージョンの改変かを
|
||||
|
||||
明記されることを希望します。
|
||||
|
||||
バージョン情報が無い場合は、配布物の年月日を明記されることを
|
||||
|
||||
希望します。
|
||||
|
||||
2014/10/05 kmiya
|
||||
```
|
||||
|
||||
The unofficial translation:
|
||||
|
||||
```
|
||||
Anyone is free to use this program for any purpose,
|
||||
commercial or non-commercial, without any restriction.
|
||||
|
||||
Anyone is free to distribute, copy, publish, or
|
||||
advertise this software, without any contact.
|
||||
|
||||
Anyone is free to distribute with modification of the
|
||||
source code, but I "hope" that its based version or
|
||||
date will be written clearly.
|
||||
|
||||
2014/10/05 kmiya
|
||||
```
|
||||
|
||||
kmiya also said this is compatible with MIT/BSD/GPL.
|
||||
|
||||
Vendored
+218
@@ -0,0 +1,218 @@
|
||||
# libsixel (encoder subset, vendored)
|
||||
|
||||
Encoder-only subset of [libsixel](https://github.com/saitoha/libsixel) `1.8.7-r2`,
|
||||
used by `src/logo/image/` to render the `sixel` logo type **on Windows and macOS**.
|
||||
|
||||
## Why this is vendored
|
||||
|
||||
Neither MSYS2 nor vcpkg packages libsixel, so on Windows there is nothing to
|
||||
`dlopen()` or to link against. The encoder is small and has no external
|
||||
dependencies, so the necessary files are embedded instead. The intent is to upstream
|
||||
a libsixel package to MSYS2 later and switch to it; until then this directory is the
|
||||
source of truth.
|
||||
|
||||
macOS *does* have a package — Homebrew's `libsixel` is this very version, `1.8.7-r2`
|
||||
(`brew info libsixel`) — but it is deliberately not used. It drags in `jpeg-turbo` /
|
||||
`libpng` for the decoder side we never call, and linking it would mean a different
|
||||
encoder on each platform. Since Windows needs an embedded copy regardless, one shared
|
||||
copy is simpler than two.
|
||||
|
||||
## Why only Windows and macOS
|
||||
|
||||
Windows loses ImageMagick entirely — its image backend moves to WIC, which decodes
|
||||
and scales but cannot *encode* sixel — so it needs an encoder of its own.
|
||||
|
||||
macOS does not get ImageMagick either: `ENABLE_IMAGEMAGICK6` / `ENABLE_IMAGEMAGICK7`
|
||||
are gated on `LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR
|
||||
GNU`, with no `APPLE`, and its image backend is ImageIO. So it shares the same
|
||||
requirement.
|
||||
|
||||
On every other platform ImageMagick stays, and it already produces sixel through
|
||||
its own SIXEL coder. Adding libsixel there would mean a second encoder for a
|
||||
capability that already works: one more dependency for zero gain. So `ENABLE_SIXEL`
|
||||
is gated on `WIN32 OR APPLE`, and these sources are **not compiled at all** on
|
||||
Linux / the BSDs.
|
||||
|
||||
Consequence: sixel bytes differ between Windows / macOS (libsixel) and the rest
|
||||
(ImageMagick). That is deliberate and accepted — see
|
||||
`doc/windows-image-backend.md` decision 9.
|
||||
|
||||
## Transparency
|
||||
|
||||
The subset is used for its keycolor support. `sixel_dither_set_transparent()` marks a
|
||||
palette index as the transparent one; `output_rgb_palette_definition()` then leaves that
|
||||
entry out of the palette definition, and `sixel_encode_body()` never puts a keycolor
|
||||
pixel into the node map, so nothing is drawn there and the terminal background shows
|
||||
through. `src/logo/image/sixel.c` drives this for sources that contain fully transparent
|
||||
pixels — it maps those pixels onto a dedicated index one past the quantized palette,
|
||||
because keying a *content* colour would also drop every opaque pixel that shares it.
|
||||
|
||||
Two things have to line up for the background to actually be transparent: the caller has to
|
||||
mark a keycolor, so the keyed pixels are never painted (above), **and** the encoder has to ask
|
||||
for a transparent background, so the terminal leaves those unpainted pixels alone instead of
|
||||
filling them. The second half is the DCS `P2` parameter — see
|
||||
[Local modifications](#local-modifications) item 3.
|
||||
|
||||
Note what this is and is not: it is a **palette-index** keycolor, not alpha support. A
|
||||
partially transparent pixel has to be drawn in its unblended colour, since a sixel has
|
||||
no way to blend with whatever the terminal has behind it.
|
||||
|
||||
⚠️ Do not vendor a *newer* libsixel in the belief that this version cannot do
|
||||
transparency. It can, and it was measured: the official `1.8.7-r2` omits the keycolor
|
||||
entry and leaves the keyed pixels unpainted, with the remaining pixels correctly placed.
|
||||
When a transparent background is not showing up, check the caller first — historically
|
||||
the caller simply never called `sixel_dither_set_transparent()`, and an image logo only
|
||||
renders correctly once it does. If the caller *is* setting a keycolor and the background is
|
||||
still solid, check `P2` instead: the keyed pixels are being left unpainted correctly, but the
|
||||
terminal is filling them.
|
||||
|
||||
## What was kept
|
||||
|
||||
Starting from `sixel_dither_new` / `sixel_dither_initialize` / `sixel_output_new` /
|
||||
`sixel_encode` and following the dependency closure, only these files are reachable:
|
||||
|
||||
| File | Provides |
|
||||
|---|---|
|
||||
| `tosixel.c` | `sixel_encode`, `sixel_putc`, `sixel_node_*`, DCS envelope handling |
|
||||
| `dither.c` / `dither.h` | `sixel_dither_new`, `sixel_dither_initialize`, `sixel_dither_unref` |
|
||||
| `output.c` / `output.h` | `sixel_output_new`, `sixel_output_unref` |
|
||||
| `quant.c` / `quant.h` | Median-cut palette construction |
|
||||
| `pixelformat.c` | `sixel_helper_compute_depth`, `sixel_helper_normalize_pixelformat` |
|
||||
| `allocator.c` / `allocator.h` | `sixel_allocator_*` |
|
||||
| `status.c` / `status.h` | `sixel_helper_set_additional_message` |
|
||||
| `malloc_stub.h` | Included by `allocator.c`; compiles to nothing when `HAVE_MALLOC` |
|
||||
| `sixel.h` | Public API (upstream `include/sixel.h`) |
|
||||
|
||||
## What was removed
|
||||
|
||||
Everything else in the upstream tree, in particular:
|
||||
|
||||
- **Decoding**: `decoder.c`, `fromsixel.c`, `frompnm.c`, `fromgif.c`, `frame.c`,
|
||||
`loader.c`, `stb_image.h`, `stb_image_write.*`. Image decoding is done by the
|
||||
platform backend (WIC on Windows, ImageIO on macOS, ImageMagick elsewhere), so
|
||||
libsixel only ever sees an RGBA8 buffer here.
|
||||
- **The `sixel_encoder_*` / `sixel_decoder_*` high-level API**: `encoder.c`,
|
||||
`writer.c`, `tty.c`, `scale.c`. These pull in file I/O, terminal probing and the
|
||||
loader registry. We call the dither/output/encode trio directly.
|
||||
- **`rgblookup.h` / `rgblookup.gperf`**: a gperf-generated color-name table used
|
||||
only by `encoder.c` for `--builtin-palette` parsing.
|
||||
- **`converters/`** (`img2sixel`, `sixel2png`), **`python/`**, **`tools/`**,
|
||||
**`tests/`**, **`images/`** (216 test images), and the packaging templates
|
||||
(`package.json.in.in`, `libsixel.pc.in`).
|
||||
- **The autotools machinery**: `configure`, `Makefile.in`, `aclocal.m4`, `m4/`,
|
||||
`ltmain.sh`, `config.h.in`, … See item 2 under
|
||||
[Local modifications](#local-modifications).
|
||||
- **Six of the nine `LICENSE.*` files**: `LICENSE.images` / `.mesa` / `.pngsuite` /
|
||||
`.sdump` / `.stb` cover only material that was dropped (test images, the OpenGL
|
||||
example, the `sdump` tool, stb). The three that remain are the complete set
|
||||
required by the files above — verified against each file's own header, see
|
||||
[License](#license).
|
||||
|
||||
Result: **20 files / 256 KB**, down from 319 files / 11.3 MB. Note that 8.0 MB of
|
||||
that 11.3 MB was `images/` fixtures, so the meaningful comparison is code:
|
||||
`src/` + `include/` went from 50 files / 1.17 MB to 20 files / 256 KB.
|
||||
|
||||
## Local modifications
|
||||
|
||||
Four deliberate divergences from upstream. Re-apply all four when re-syncing.
|
||||
|
||||
1. **`sixel.h` — `SIXELAPI` is empty.** Upstream defines it as
|
||||
`__declspec(dllexport)` on Windows. This subset is compiled into `libfastfetch`
|
||||
statically rather than into a standalone DLL, so the attribute would (a) add
|
||||
every libsixel symbol to fastfetch's export table and (b) make `sixel.h`'s
|
||||
declarations disagree with the ones in `dither.h` / `output.h` / `quant.h` /
|
||||
`allocator.h`, which do not carry it (clang warns:
|
||||
`-Wdll-attribute-on-redeclaration`).
|
||||
|
||||
2. **`config.h` is hand-written.** Upstream generates it with `configure`. Only
|
||||
these vendored files read it, and they reference a small fixed set of macros,
|
||||
so the values are hard-coded. Two things to watch:
|
||||
- `HAVE_TESTS` must stay **undefined**, not `0`: `sixel.h` gates its test-only
|
||||
declarations with `#ifdef HAVE_TESTS`, so `#define HAVE_TESTS 0` would still
|
||||
pull in declarations for functions we do not compile.
|
||||
- `HAVE_MEMORY_H` / `HAVE_STRING_H` must be `1`. fastfetch builds with
|
||||
`-Werror=implicit-function-declaration`, so a missing `<memory.h>` /
|
||||
`<string.h>` include in `pixelformat.c` / `status.c` is a hard error.
|
||||
|
||||
3. **`tosixel.c` — the DCS `P2` parameter is initialised to 1.** `sixel_encode_header()`
|
||||
declares `int p[3] = {0, 0, 0}`, and the trailing-zero trimming right below it then drops
|
||||
all three, so the envelope is a bare `ESC P q`. That means `P2 = 0`, which tells the
|
||||
terminal to fill every unset pixel with **colour-table entry 0** — and that entry is
|
||||
defined by the image's own palette, so a keycolor image comes out with a solid rectangle
|
||||
behind it rather than transparency. Setting `p[1] = 1` leaves unset pixels untouched.
|
||||
|
||||
The trimming still drops the trailing aspect-ratio parameter, so the envelope is
|
||||
`ESC P 0;1 q`. `P3` defaults to 0 and the two spellings are equivalent — verified by
|
||||
decoding both with libsixel's own decoder and with ImageMagick's. ImageMagick's SIXEL
|
||||
coder and chafa both write `0;1;0` explicitly.
|
||||
|
||||
This only shows up on a terminal that implements the VT340 background rule. Windows
|
||||
Terminal does (`_backgroundFillRequired = (_conformanceLevel == 1 || !transparent)` in
|
||||
`src/terminal/adapter/SixelParser.cpp`), which is where it was reproduced; WezTerm does
|
||||
not fill, so the identical bytes look correct there. It is also why Linux never showed
|
||||
the problem: that path goes through ImageMagick, which already sends `P2 = 1`.
|
||||
|
||||
4. **`output.c` — release the allocator reference if output allocation fails.**
|
||||
`sixel_output_new()` creates an allocator or takes an additional reference before
|
||||
allocating the output context. If that allocation fails, the reference is not owned by
|
||||
an output object and must be released before returning `SIXEL_BAD_ALLOCATION`.
|
||||
Fastfetch passes a null allocator, so without this cleanup the newly created allocator
|
||||
becomes unreachable on this error path. The successful path and returned error code are
|
||||
unchanged.
|
||||
|
||||
The vendored sources are also exempted from `-Wconversion` in `CMakeLists.txt`:
|
||||
upstream has ~160 implicit int→`unsigned char` narrowing warnings. Everything else
|
||||
(`-Wall -Wextra` and fastfetch's `-Werror=` set) applies unchanged, and the subset
|
||||
compiles clean.
|
||||
|
||||
## Re-syncing with upstream
|
||||
|
||||
```sh
|
||||
VER=1.8.7-r2
|
||||
git clone --depth 1 --branch "$VER" https://github.com/saitoha/libsixel /tmp/libsixel
|
||||
|
||||
# 1. copy the closure (paths change from src/foo.c to foo.c, include/sixel.h to sixel.h)
|
||||
for f in tosixel.c dither.c dither.h output.c output.h quant.c quant.h \
|
||||
pixelformat.c allocator.c allocator.h status.c status.h malloc_stub.h; do
|
||||
cp "/tmp/libsixel/src/$f" .
|
||||
done
|
||||
cp /tmp/libsixel/include/sixel.h .
|
||||
cp /tmp/libsixel/LICENSE /tmp/libsixel/LICENSE.sixel /tmp/libsixel/LICENSE.pnmcolormap .
|
||||
|
||||
# 2. re-apply the four local modifications listed above
|
||||
# 3. verify
|
||||
```
|
||||
|
||||
Verification (must be 0 diagnostics, and the encoder must emit a DCS-wrapped stream).
|
||||
Both platforms compile this subset, so run it on both:
|
||||
|
||||
```sh
|
||||
# Windows: CC=/c/msys64/clang64/bin/cc.exe
|
||||
# macOS: CC=cc (verified with Apple clang 21: 0 diagnostics)
|
||||
CC=cc
|
||||
FLAGS="-I. -Wall -Wextra -Wconversion -Wno-conversion -Werror=uninitialized \
|
||||
-Werror=return-type -Werror=vla -Werror=incompatible-pointer-types \
|
||||
-Werror=implicit-function-declaration -Werror=int-conversion -std=gnu23"
|
||||
for f in tosixel dither output quant allocator pixelformat status; do
|
||||
$CC $FLAGS -c $f.c -o /dev/null || echo "FAILED: $f"
|
||||
done
|
||||
```
|
||||
|
||||
If upstream adds a call from the closure to a new file, the build fails at link
|
||||
time with an undefined `sixel_*` symbol — add that file and repeat.
|
||||
|
||||
## License
|
||||
|
||||
MIT-compatible, but three separate notices apply and all three files must be kept.
|
||||
Mapping verified against each source file's own copyright header:
|
||||
|
||||
| File | Applies to |
|
||||
|---|---|
|
||||
| `LICENSE` | MIT, Copyright (c) 2014-2016 Hayaki Saito — `dither.c`, `output.c`, `pixelformat.c`, `allocator.c`, `status.c` (their headers say 2014-2018/2019; same MIT terms) |
|
||||
| `LICENSE.sixel` | `tosixel.c` — derived from kmiya's original `sixel` (2014-3-2), permissive, re-licensed MIT by Hayaki Saito |
|
||||
| `LICENSE.pnmcolormap` | `quant.c` — derived from `ppmquant` by Jef Poskanzer / Bryan Henderson |
|
||||
|
||||
Upstream also ships `LICENSE.images`, `.mesa`, `.pngsuite`, `.sdump` and `.stb`;
|
||||
none of them cover a file in this subset, so they were not carried over. If a
|
||||
re-sync ever pulls in a file from the decoder side (`fromgif.c` → `.stb`,
|
||||
`fromsixel.c` → `.sixel`), re-check this table.
|
||||
Vendored
+379
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
/* STDC_HEADERS */
|
||||
#include <stdlib.h>
|
||||
|
||||
#if HAVE_ASSERT_H
|
||||
# include <assert.h>
|
||||
#endif /* HAVE_ASSERT_H */
|
||||
#if HAVE_SYS_TYPES_H
|
||||
# include <sys/types.h>
|
||||
#endif /* HAVE_SYS_TYPES_H */
|
||||
#if HAVE_ERRNO_H
|
||||
# include <errno.h>
|
||||
#endif /* HAVE_ERRNO_H */
|
||||
#if HAVE_MEMORY_H
|
||||
# include <memory.h>
|
||||
#endif /* HAVE_MEMORY_H */
|
||||
|
||||
#include "allocator.h"
|
||||
#include "malloc_stub.h"
|
||||
|
||||
/* create allocator object */
|
||||
SIXELSTATUS
|
||||
sixel_allocator_new(
|
||||
sixel_allocator_t /* out */ **ppallocator, /* allocator object to be created */
|
||||
sixel_malloc_t /* in */ fn_malloc, /* custom malloc() function */
|
||||
sixel_calloc_t /* in */ fn_calloc, /* custom calloc() function */
|
||||
sixel_realloc_t /* in */ fn_realloc, /* custom realloc() function */
|
||||
sixel_free_t /* in */ fn_free) /* custom free() function */
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
|
||||
if (ppallocator == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_allocator_new: given argument ppallocator is null.");
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (fn_malloc == NULL) {
|
||||
fn_malloc = malloc;
|
||||
}
|
||||
|
||||
if (fn_calloc == NULL) {
|
||||
fn_calloc = calloc;
|
||||
}
|
||||
|
||||
if (fn_realloc == NULL) {
|
||||
fn_realloc = realloc;
|
||||
}
|
||||
|
||||
if (fn_free == NULL) {
|
||||
fn_free = free;
|
||||
}
|
||||
|
||||
*ppallocator = fn_malloc(sizeof(sixel_allocator_t));
|
||||
if (*ppallocator == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_allocator_new: fn_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
|
||||
(*ppallocator)->ref = 1;
|
||||
(*ppallocator)->fn_malloc = fn_malloc;
|
||||
(*ppallocator)->fn_calloc = fn_calloc;
|
||||
(*ppallocator)->fn_realloc = fn_realloc;
|
||||
(*ppallocator)->fn_free = fn_free;
|
||||
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
/* destruct allocator object */
|
||||
static void
|
||||
sixel_allocator_destroy(
|
||||
sixel_allocator_t /* in */ *allocator) /* allocator object to
|
||||
be destroyed */
|
||||
{
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
assert(allocator->fn_free);
|
||||
|
||||
allocator->fn_free(allocator);
|
||||
}
|
||||
|
||||
|
||||
/* increase reference count of allocatort object (thread-unsafe) */
|
||||
SIXELAPI void
|
||||
sixel_allocator_ref(
|
||||
sixel_allocator_t /* in */ *allocator) /* allocator object to be
|
||||
increment reference counter */
|
||||
{
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
|
||||
/* TODO: be thread safe */
|
||||
++allocator->ref;
|
||||
}
|
||||
|
||||
|
||||
/* decrease reference count of output context object (thread-unsafe) */
|
||||
SIXELAPI void
|
||||
sixel_allocator_unref(
|
||||
sixel_allocator_t /* in */ *allocator) /* allocator object to be unreference */
|
||||
{
|
||||
/* TODO: be thread safe */
|
||||
if (allocator) {
|
||||
assert(allocator->ref > 0);
|
||||
--allocator->ref;
|
||||
if (allocator->ref == 0) {
|
||||
sixel_allocator_destroy(allocator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* call custom malloc() */
|
||||
SIXELAPI void *
|
||||
sixel_allocator_malloc(
|
||||
sixel_allocator_t /* in */ *allocator, /* allocator object */
|
||||
size_t /* in */ n) /* allocation size */
|
||||
{
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
assert(allocator->fn_malloc);
|
||||
|
||||
if (n == 0) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_allocator_malloc: called with n == 0");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (n > SIXEL_ALLOCATE_BYTES_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return allocator->fn_malloc(n);
|
||||
}
|
||||
|
||||
|
||||
/* call custom calloc() */
|
||||
SIXELAPI void *
|
||||
sixel_allocator_calloc(
|
||||
sixel_allocator_t /* in */ *allocator, /* allocator object */
|
||||
size_t /* in */ nelm, /* number of elements */
|
||||
size_t /* in */ elsize) /* size of element */
|
||||
{
|
||||
size_t n;
|
||||
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
assert(allocator->fn_calloc);
|
||||
|
||||
n = nelm * elsize;
|
||||
|
||||
if (n == 0) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_allocator_malloc: called with n == 0");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (n > SIXEL_ALLOCATE_BYTES_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return allocator->fn_calloc(nelm, elsize);
|
||||
}
|
||||
|
||||
|
||||
/* call custom realloc() */
|
||||
SIXELAPI void *
|
||||
sixel_allocator_realloc(
|
||||
sixel_allocator_t /* in */ *allocator, /* allocator object */
|
||||
void /* in */ *p, /* existing buffer to be re-allocated */
|
||||
size_t /* in */ n) /* re-allocation size */
|
||||
{
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
assert(allocator->fn_realloc);
|
||||
|
||||
if (n == 0) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_allocator_malloc: called with n == 0");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (n > SIXEL_ALLOCATE_BYTES_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return allocator->fn_realloc(p, n);
|
||||
}
|
||||
|
||||
|
||||
/* call custom free() */
|
||||
SIXELAPI void
|
||||
sixel_allocator_free(
|
||||
sixel_allocator_t /* in */ *allocator, /* allocator object */
|
||||
void /* in */ *p) /* existing buffer to be freed */
|
||||
{
|
||||
/* precondition */
|
||||
assert(allocator);
|
||||
assert(allocator->fn_free);
|
||||
|
||||
allocator->fn_free(p);
|
||||
}
|
||||
|
||||
|
||||
#if HAVE_TESTS
|
||||
volatile int sixel_debug_malloc_counter;
|
||||
|
||||
void *
|
||||
sixel_bad_malloc(size_t size)
|
||||
{
|
||||
return sixel_debug_malloc_counter-- == 0 ? NULL: malloc(size);
|
||||
}
|
||||
|
||||
|
||||
void *
|
||||
sixel_bad_calloc(size_t count, size_t size)
|
||||
{
|
||||
(void) count;
|
||||
(void) size;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void *
|
||||
sixel_bad_realloc(void *ptr, size_t size)
|
||||
{
|
||||
(void) ptr;
|
||||
(void) size;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
#endif /* HAVE_TESTS */
|
||||
|
||||
#if 0
|
||||
int
|
||||
rpl_posix_memalign(void **memptr, size_t alignment, size_t size)
|
||||
{
|
||||
#if HAVE_POSIX_MEMALIGN
|
||||
return posix_memalign(memptr, alignment, size);
|
||||
#elif HAVE_ALIGNED_ALLOC
|
||||
*memptr = aligned_alloc(alignment, size);
|
||||
return *memptr ? 0: ENOMEM;
|
||||
#elif HAVE_MEMALIGN
|
||||
*memptr = memalign(alignment, size);
|
||||
return *memptr ? 0: ENOMEM;
|
||||
#elif HAVE__ALIGNED_MALLOC
|
||||
return _aligned_malloc(size, alignment);
|
||||
#else
|
||||
# error
|
||||
#endif /* _MSC_VER */
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if HAVE_TESTS
|
||||
static int
|
||||
test1(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
SIXELSTATUS status;
|
||||
sixel_allocator_t *allocator = NULL;
|
||||
|
||||
status = sixel_allocator_new(NULL, malloc, calloc, realloc, free);
|
||||
if (status != SIXEL_BAD_ARGUMENT) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
status = sixel_allocator_new(&allocator, NULL, calloc, realloc, free);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
status = sixel_allocator_new(&allocator, malloc, NULL, realloc, free);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
status = sixel_allocator_new(&allocator, malloc, calloc, NULL, free);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
status = sixel_allocator_new(&allocator, malloc, calloc, realloc, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test2(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
SIXELSTATUS status;
|
||||
sixel_allocator_t *allocator = NULL;
|
||||
|
||||
sixel_debug_malloc_counter = 1;
|
||||
|
||||
status = sixel_allocator_new(&allocator, sixel_bad_malloc, calloc, realloc, free);
|
||||
if (status == SIXEL_BAD_ALLOCATION) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI int
|
||||
sixel_allocator_tests_main(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
size_t i;
|
||||
typedef int (* testcase)(void);
|
||||
|
||||
static testcase const testcases[] = {
|
||||
test1,
|
||||
test2
|
||||
};
|
||||
|
||||
for (i = 0; i < sizeof(testcases) / sizeof(testcase); ++i) {
|
||||
nret = testcases[i]();
|
||||
if (nret != EXIT_SUCCESS) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
#endif /* HAVE_TESTS */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_ALLOCATOR_H
|
||||
#define LIBSIXEL_ALLOCATOR_H
|
||||
|
||||
#include <sixel.h>
|
||||
|
||||
struct sixel_allocator {
|
||||
unsigned int ref; /* reference counter */
|
||||
sixel_malloc_t fn_malloc;
|
||||
sixel_calloc_t fn_calloc;
|
||||
sixel_realloc_t fn_realloc;
|
||||
sixel_free_t fn_free;
|
||||
};
|
||||
|
||||
#if HAVE_TESTS
|
||||
int
|
||||
sixel_allocator_tests_main(void);
|
||||
#endif
|
||||
|
||||
#endif /* LIBSIXEL_ALLOCATOR_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Minimal hand-written replacement for the autoconf-generated config.h.
|
||||
*
|
||||
* Upstream libsixel ships config.h.in and lets `configure` produce config.h.
|
||||
* We only vendor the encoder-side subset (see README.md), and those files
|
||||
* reference a small, fixed set of macros, so the values are hard-coded here
|
||||
* instead of carrying the autotools machinery.
|
||||
*
|
||||
* The values below hold for every platform fastfetch targets:
|
||||
* Windows/MinGW-w64, Linux/glibc, Linux/musl, macOS and the BSDs.
|
||||
* Re-check this file whenever the vendored subset is re-synced with upstream.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_CONFIG_H
|
||||
#define LIBSIXEL_CONFIG_H
|
||||
|
||||
/* Standard headers (upstream: AC_CHECK_HEADERS) */
|
||||
#define HAVE_ASSERT_H 1
|
||||
#define HAVE_ERRNO_H 1
|
||||
#define HAVE_INTTYPES_H 1
|
||||
#define HAVE_LIMITS_H 1
|
||||
#define HAVE_MATH_H 1
|
||||
#define HAVE_MEMORY_H 1
|
||||
#define HAVE_STRING_H 1
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
|
||||
/* libc functions (upstream: AC_CHECK_FUNCS) */
|
||||
#define HAVE_LDIV 1
|
||||
#define HAVE_MALLOC 1
|
||||
#define HAVE_REALLOC 1
|
||||
|
||||
/* Byte order of the 32bpp pixel readers in pixelformat.c; 0 = little endian */
|
||||
#define SWAP_BYTES 0
|
||||
|
||||
/*
|
||||
* Guards the diagnostic pragma push/pop in dither.c. Not needed, and the
|
||||
* pragma spelling is not portable across the compilers we support.
|
||||
*/
|
||||
#define HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS 0
|
||||
|
||||
/* Verbose quantizer tracing to stderr */
|
||||
#define HAVE_DEBUG 0
|
||||
|
||||
/*
|
||||
* HAVE_TESTS must stay *undefined* rather than 0: sixel.h gates its test-only
|
||||
* declarations with `#ifdef HAVE_TESTS`, so defining it to 0 would still pull
|
||||
* in declarations for functions we do not compile.
|
||||
*/
|
||||
#undef HAVE_TESTS
|
||||
|
||||
/* Optional image loaders and features; not vendored */
|
||||
#undef HAVE_GD
|
||||
#undef HAVE_GDK_PIXBUF2
|
||||
#undef HAVE_JPEG
|
||||
#undef HAVE_LIBCURL
|
||||
#undef HAVE_LIBPNG
|
||||
|
||||
#endif /* LIBSIXEL_CONFIG_H */
|
||||
Vendored
+918
@@ -0,0 +1,918 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#if HAVE_MATH_H
|
||||
# include <math.h>
|
||||
#endif /* HAVE_MATH_H */
|
||||
#if HAVE_STRING_H
|
||||
# include <string.h>
|
||||
#endif /* HAVE_STRING_H */
|
||||
#if HAVE_LIMITS_H
|
||||
# include <limits.h>
|
||||
#endif /* HAVE_LIMITS_H */
|
||||
#if HAVE_INTTYPES_H
|
||||
# include <inttypes.h>
|
||||
#endif /* HAVE_INTTYPES_H */
|
||||
|
||||
#include "dither.h"
|
||||
#include "quant.h"
|
||||
#include <sixel.h>
|
||||
|
||||
|
||||
static const unsigned char pal_mono_dark[] = {
|
||||
0x00, 0x00, 0x00, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_mono_light[] = {
|
||||
0xff, 0xff, 0xff, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
static const unsigned char pal_gray_1bit[] = {
|
||||
0x00, 0x00, 0x00, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_gray_2bit[] = {
|
||||
0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0xaa, 0xaa, 0xaa, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_gray_4bit[] = {
|
||||
0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x33, 0x33, 0x33,
|
||||
0x44, 0x44, 0x44, 0x55, 0x55, 0x55, 0x66, 0x66, 0x66, 0x77, 0x77, 0x77,
|
||||
0x88, 0x88, 0x88, 0x99, 0x99, 0x99, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb,
|
||||
0xcc, 0xcc, 0xcc, 0xdd, 0xdd, 0xdd, 0xee, 0xee, 0xee, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_gray_8bit[] = {
|
||||
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03,
|
||||
0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07,
|
||||
0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b,
|
||||
0x0c, 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f, 0x0f,
|
||||
0x10, 0x10, 0x10, 0x11, 0x11, 0x11, 0x12, 0x12, 0x12, 0x13, 0x13, 0x13,
|
||||
0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x16, 0x16, 0x16, 0x17, 0x17, 0x17,
|
||||
0x18, 0x18, 0x18, 0x19, 0x19, 0x19, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b,
|
||||
0x1c, 0x1c, 0x1c, 0x1d, 0x1d, 0x1d, 0x1e, 0x1e, 0x1e, 0x1f, 0x1f, 0x1f,
|
||||
0x20, 0x20, 0x20, 0x21, 0x21, 0x21, 0x22, 0x22, 0x22, 0x23, 0x23, 0x23,
|
||||
0x24, 0x24, 0x24, 0x25, 0x25, 0x25, 0x26, 0x26, 0x26, 0x27, 0x27, 0x27,
|
||||
0x28, 0x28, 0x28, 0x29, 0x29, 0x29, 0x2a, 0x2a, 0x2a, 0x2b, 0x2b, 0x2b,
|
||||
0x2c, 0x2c, 0x2c, 0x2d, 0x2d, 0x2d, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, 0x2f,
|
||||
0x30, 0x30, 0x30, 0x31, 0x31, 0x31, 0x32, 0x32, 0x32, 0x33, 0x33, 0x33,
|
||||
0x34, 0x34, 0x34, 0x35, 0x35, 0x35, 0x36, 0x36, 0x36, 0x37, 0x37, 0x37,
|
||||
0x38, 0x38, 0x38, 0x39, 0x39, 0x39, 0x3a, 0x3a, 0x3a, 0x3b, 0x3b, 0x3b,
|
||||
0x3c, 0x3c, 0x3c, 0x3d, 0x3d, 0x3d, 0x3e, 0x3e, 0x3e, 0x3f, 0x3f, 0x3f,
|
||||
0x40, 0x40, 0x40, 0x41, 0x41, 0x41, 0x42, 0x42, 0x42, 0x43, 0x43, 0x43,
|
||||
0x44, 0x44, 0x44, 0x45, 0x45, 0x45, 0x46, 0x46, 0x46, 0x47, 0x47, 0x47,
|
||||
0x48, 0x48, 0x48, 0x49, 0x49, 0x49, 0x4a, 0x4a, 0x4a, 0x4b, 0x4b, 0x4b,
|
||||
0x4c, 0x4c, 0x4c, 0x4d, 0x4d, 0x4d, 0x4e, 0x4e, 0x4e, 0x4f, 0x4f, 0x4f,
|
||||
0x50, 0x50, 0x50, 0x51, 0x51, 0x51, 0x52, 0x52, 0x52, 0x53, 0x53, 0x53,
|
||||
0x54, 0x54, 0x54, 0x55, 0x55, 0x55, 0x56, 0x56, 0x56, 0x57, 0x57, 0x57,
|
||||
0x58, 0x58, 0x58, 0x59, 0x59, 0x59, 0x5a, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b,
|
||||
0x5c, 0x5c, 0x5c, 0x5d, 0x5d, 0x5d, 0x5e, 0x5e, 0x5e, 0x5f, 0x5f, 0x5f,
|
||||
0x60, 0x60, 0x60, 0x61, 0x61, 0x61, 0x62, 0x62, 0x62, 0x63, 0x63, 0x63,
|
||||
0x64, 0x64, 0x64, 0x65, 0x65, 0x65, 0x66, 0x66, 0x66, 0x67, 0x67, 0x67,
|
||||
0x68, 0x68, 0x68, 0x69, 0x69, 0x69, 0x6a, 0x6a, 0x6a, 0x6b, 0x6b, 0x6b,
|
||||
0x6c, 0x6c, 0x6c, 0x6d, 0x6d, 0x6d, 0x6e, 0x6e, 0x6e, 0x6f, 0x6f, 0x6f,
|
||||
0x70, 0x70, 0x70, 0x71, 0x71, 0x71, 0x72, 0x72, 0x72, 0x73, 0x73, 0x73,
|
||||
0x74, 0x74, 0x74, 0x75, 0x75, 0x75, 0x76, 0x76, 0x76, 0x77, 0x77, 0x77,
|
||||
0x78, 0x78, 0x78, 0x79, 0x79, 0x79, 0x7a, 0x7a, 0x7a, 0x7b, 0x7b, 0x7b,
|
||||
0x7c, 0x7c, 0x7c, 0x7d, 0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f,
|
||||
0x80, 0x80, 0x80, 0x81, 0x81, 0x81, 0x82, 0x82, 0x82, 0x83, 0x83, 0x83,
|
||||
0x84, 0x84, 0x84, 0x85, 0x85, 0x85, 0x86, 0x86, 0x86, 0x87, 0x87, 0x87,
|
||||
0x88, 0x88, 0x88, 0x89, 0x89, 0x89, 0x8a, 0x8a, 0x8a, 0x8b, 0x8b, 0x8b,
|
||||
0x8c, 0x8c, 0x8c, 0x8d, 0x8d, 0x8d, 0x8e, 0x8e, 0x8e, 0x8f, 0x8f, 0x8f,
|
||||
0x90, 0x90, 0x90, 0x91, 0x91, 0x91, 0x92, 0x92, 0x92, 0x93, 0x93, 0x93,
|
||||
0x94, 0x94, 0x94, 0x95, 0x95, 0x95, 0x96, 0x96, 0x96, 0x97, 0x97, 0x97,
|
||||
0x98, 0x98, 0x98, 0x99, 0x99, 0x99, 0x9a, 0x9a, 0x9a, 0x9b, 0x9b, 0x9b,
|
||||
0x9c, 0x9c, 0x9c, 0x9d, 0x9d, 0x9d, 0x9e, 0x9e, 0x9e, 0x9f, 0x9f, 0x9f,
|
||||
0xa0, 0xa0, 0xa0, 0xa1, 0xa1, 0xa1, 0xa2, 0xa2, 0xa2, 0xa3, 0xa3, 0xa3,
|
||||
0xa4, 0xa4, 0xa4, 0xa5, 0xa5, 0xa5, 0xa6, 0xa6, 0xa6, 0xa7, 0xa7, 0xa7,
|
||||
0xa8, 0xa8, 0xa8, 0xa9, 0xa9, 0xa9, 0xaa, 0xaa, 0xaa, 0xab, 0xab, 0xab,
|
||||
0xac, 0xac, 0xac, 0xad, 0xad, 0xad, 0xae, 0xae, 0xae, 0xaf, 0xaf, 0xaf,
|
||||
0xb0, 0xb0, 0xb0, 0xb1, 0xb1, 0xb1, 0xb2, 0xb2, 0xb2, 0xb3, 0xb3, 0xb3,
|
||||
0xb4, 0xb4, 0xb4, 0xb5, 0xb5, 0xb5, 0xb6, 0xb6, 0xb6, 0xb7, 0xb7, 0xb7,
|
||||
0xb8, 0xb8, 0xb8, 0xb9, 0xb9, 0xb9, 0xba, 0xba, 0xba, 0xbb, 0xbb, 0xbb,
|
||||
0xbc, 0xbc, 0xbc, 0xbd, 0xbd, 0xbd, 0xbe, 0xbe, 0xbe, 0xbf, 0xbf, 0xbf,
|
||||
0xc0, 0xc0, 0xc0, 0xc1, 0xc1, 0xc1, 0xc2, 0xc2, 0xc2, 0xc3, 0xc3, 0xc3,
|
||||
0xc4, 0xc4, 0xc4, 0xc5, 0xc5, 0xc5, 0xc6, 0xc6, 0xc6, 0xc7, 0xc7, 0xc7,
|
||||
0xc8, 0xc8, 0xc8, 0xc9, 0xc9, 0xc9, 0xca, 0xca, 0xca, 0xcb, 0xcb, 0xcb,
|
||||
0xcc, 0xcc, 0xcc, 0xcd, 0xcd, 0xcd, 0xce, 0xce, 0xce, 0xcf, 0xcf, 0xcf,
|
||||
0xd0, 0xd0, 0xd0, 0xd1, 0xd1, 0xd1, 0xd2, 0xd2, 0xd2, 0xd3, 0xd3, 0xd3,
|
||||
0xd4, 0xd4, 0xd4, 0xd5, 0xd5, 0xd5, 0xd6, 0xd6, 0xd6, 0xd7, 0xd7, 0xd7,
|
||||
0xd8, 0xd8, 0xd8, 0xd9, 0xd9, 0xd9, 0xda, 0xda, 0xda, 0xdb, 0xdb, 0xdb,
|
||||
0xdc, 0xdc, 0xdc, 0xdd, 0xdd, 0xdd, 0xde, 0xde, 0xde, 0xdf, 0xdf, 0xdf,
|
||||
0xe0, 0xe0, 0xe0, 0xe1, 0xe1, 0xe1, 0xe2, 0xe2, 0xe2, 0xe3, 0xe3, 0xe3,
|
||||
0xe4, 0xe4, 0xe4, 0xe5, 0xe5, 0xe5, 0xe6, 0xe6, 0xe6, 0xe7, 0xe7, 0xe7,
|
||||
0xe8, 0xe8, 0xe8, 0xe9, 0xe9, 0xe9, 0xea, 0xea, 0xea, 0xeb, 0xeb, 0xeb,
|
||||
0xec, 0xec, 0xec, 0xed, 0xed, 0xed, 0xee, 0xee, 0xee, 0xef, 0xef, 0xef,
|
||||
0xf0, 0xf0, 0xf0, 0xf1, 0xf1, 0xf1, 0xf2, 0xf2, 0xf2, 0xf3, 0xf3, 0xf3,
|
||||
0xf4, 0xf4, 0xf4, 0xf5, 0xf5, 0xf5, 0xf6, 0xf6, 0xf6, 0xf7, 0xf7, 0xf7,
|
||||
0xf8, 0xf8, 0xf8, 0xf9, 0xf9, 0xf9, 0xfa, 0xfa, 0xfa, 0xfb, 0xfb, 0xfb,
|
||||
0xfc, 0xfc, 0xfc, 0xfd, 0xfd, 0xfd, 0xfe, 0xfe, 0xfe, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_xterm256[] = {
|
||||
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x80, 0x00,
|
||||
0x00, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xc0,
|
||||
0x80, 0x80, 0x80, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00,
|
||||
0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x87, 0x00, 0x00, 0xaf,
|
||||
0x00, 0x00, 0xd7, 0x00, 0x00, 0xff, 0x00, 0x5f, 0x00, 0x00, 0x5f, 0x5f,
|
||||
0x00, 0x5f, 0x87, 0x00, 0x5f, 0xaf, 0x00, 0x5f, 0xd7, 0x00, 0x5f, 0xff,
|
||||
0x00, 0x87, 0x00, 0x00, 0x87, 0x5f, 0x00, 0x87, 0x87, 0x00, 0x87, 0xaf,
|
||||
0x00, 0x87, 0xd7, 0x00, 0x87, 0xff, 0x00, 0xaf, 0x00, 0x00, 0xaf, 0x5f,
|
||||
0x00, 0xaf, 0x87, 0x00, 0xaf, 0xaf, 0x00, 0xaf, 0xd7, 0x00, 0xaf, 0xff,
|
||||
0x00, 0xd7, 0x00, 0x00, 0xd7, 0x5f, 0x00, 0xd7, 0x87, 0x00, 0xd7, 0xaf,
|
||||
0x00, 0xd7, 0xd7, 0x00, 0xd7, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0x5f,
|
||||
0x00, 0xff, 0x87, 0x00, 0xff, 0xaf, 0x00, 0xff, 0xd7, 0x00, 0xff, 0xff,
|
||||
0x5f, 0x00, 0x00, 0x5f, 0x00, 0x5f, 0x5f, 0x00, 0x87, 0x5f, 0x00, 0xaf,
|
||||
0x5f, 0x00, 0xd7, 0x5f, 0x00, 0xff, 0x5f, 0x5f, 0x00, 0x5f, 0x5f, 0x5f,
|
||||
0x5f, 0x5f, 0x87, 0x5f, 0x5f, 0xaf, 0x5f, 0x5f, 0xd7, 0x5f, 0x5f, 0xff,
|
||||
0x5f, 0x87, 0x00, 0x5f, 0x87, 0x5f, 0x5f, 0x87, 0x87, 0x5f, 0x87, 0xaf,
|
||||
0x5f, 0x87, 0xd7, 0x5f, 0x87, 0xff, 0x5f, 0xaf, 0x00, 0x5f, 0xaf, 0x5f,
|
||||
0x5f, 0xaf, 0x87, 0x5f, 0xaf, 0xaf, 0x5f, 0xaf, 0xd7, 0x5f, 0xaf, 0xff,
|
||||
0x5f, 0xd7, 0x00, 0x5f, 0xd7, 0x5f, 0x5f, 0xd7, 0x87, 0x5f, 0xd7, 0xaf,
|
||||
0x5f, 0xd7, 0xd7, 0x5f, 0xd7, 0xff, 0x5f, 0xff, 0x00, 0x5f, 0xff, 0x5f,
|
||||
0x5f, 0xff, 0x87, 0x5f, 0xff, 0xaf, 0x5f, 0xff, 0xd7, 0x5f, 0xff, 0xff,
|
||||
0x87, 0x00, 0x00, 0x87, 0x00, 0x5f, 0x87, 0x00, 0x87, 0x87, 0x00, 0xaf,
|
||||
0x87, 0x00, 0xd7, 0x87, 0x00, 0xff, 0x87, 0x5f, 0x00, 0x87, 0x5f, 0x5f,
|
||||
0x87, 0x5f, 0x87, 0x87, 0x5f, 0xaf, 0x87, 0x5f, 0xd7, 0x87, 0x5f, 0xff,
|
||||
0x87, 0x87, 0x00, 0x87, 0x87, 0x5f, 0x87, 0x87, 0x87, 0x87, 0x87, 0xaf,
|
||||
0x87, 0x87, 0xd7, 0x87, 0x87, 0xff, 0x87, 0xaf, 0x00, 0x87, 0xaf, 0x5f,
|
||||
0x87, 0xaf, 0x87, 0x87, 0xaf, 0xaf, 0x87, 0xaf, 0xd7, 0x87, 0xaf, 0xff,
|
||||
0x87, 0xd7, 0x00, 0x87, 0xd7, 0x5f, 0x87, 0xd7, 0x87, 0x87, 0xd7, 0xaf,
|
||||
0x87, 0xd7, 0xd7, 0x87, 0xd7, 0xff, 0x87, 0xff, 0x00, 0x87, 0xff, 0x5f,
|
||||
0x87, 0xff, 0x87, 0x87, 0xff, 0xaf, 0x87, 0xff, 0xd7, 0x87, 0xff, 0xff,
|
||||
0xaf, 0x00, 0x00, 0xaf, 0x00, 0x5f, 0xaf, 0x00, 0x87, 0xaf, 0x00, 0xaf,
|
||||
0xaf, 0x00, 0xd7, 0xaf, 0x00, 0xff, 0xaf, 0x5f, 0x00, 0xaf, 0x5f, 0x5f,
|
||||
0xaf, 0x5f, 0x87, 0xaf, 0x5f, 0xaf, 0xaf, 0x5f, 0xd7, 0xaf, 0x5f, 0xff,
|
||||
0xaf, 0x87, 0x00, 0xaf, 0x87, 0x5f, 0xaf, 0x87, 0x87, 0xaf, 0x87, 0xaf,
|
||||
0xaf, 0x87, 0xd7, 0xaf, 0x87, 0xff, 0xaf, 0xaf, 0x00, 0xaf, 0xaf, 0x5f,
|
||||
0xaf, 0xaf, 0x87, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xd7, 0xaf, 0xaf, 0xff,
|
||||
0xaf, 0xd7, 0x00, 0xaf, 0xd7, 0x5f, 0xaf, 0xd7, 0x87, 0xaf, 0xd7, 0xaf,
|
||||
0xaf, 0xd7, 0xd7, 0xaf, 0xd7, 0xff, 0xaf, 0xff, 0x00, 0xaf, 0xff, 0x5f,
|
||||
0xaf, 0xff, 0x87, 0xaf, 0xff, 0xaf, 0xaf, 0xff, 0xd7, 0xaf, 0xff, 0xff,
|
||||
0xd7, 0x00, 0x00, 0xd7, 0x00, 0x5f, 0xd7, 0x00, 0x87, 0xd7, 0x00, 0xaf,
|
||||
0xd7, 0x00, 0xd7, 0xd7, 0x00, 0xff, 0xd7, 0x5f, 0x00, 0xd7, 0x5f, 0x5f,
|
||||
0xd7, 0x5f, 0x87, 0xd7, 0x5f, 0xaf, 0xd7, 0x5f, 0xd7, 0xd7, 0x5f, 0xff,
|
||||
0xd7, 0x87, 0x00, 0xd7, 0x87, 0x5f, 0xd7, 0x87, 0x87, 0xd7, 0x87, 0xaf,
|
||||
0xd7, 0x87, 0xd7, 0xd7, 0x87, 0xff, 0xd7, 0xaf, 0x00, 0xd7, 0xaf, 0x5f,
|
||||
0xd7, 0xaf, 0x87, 0xd7, 0xaf, 0xaf, 0xd7, 0xaf, 0xd7, 0xd7, 0xaf, 0xff,
|
||||
0xd7, 0xd7, 0x00, 0xd7, 0xd7, 0x5f, 0xd7, 0xd7, 0x87, 0xd7, 0xd7, 0xaf,
|
||||
0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xff, 0xd7, 0xff, 0x00, 0xd7, 0xff, 0x5f,
|
||||
0xd7, 0xff, 0x87, 0xd7, 0xff, 0xaf, 0xd7, 0xff, 0xd7, 0xd7, 0xff, 0xff,
|
||||
0xff, 0x00, 0x00, 0xff, 0x00, 0x5f, 0xff, 0x00, 0x87, 0xff, 0x00, 0xaf,
|
||||
0xff, 0x00, 0xd7, 0xff, 0x00, 0xff, 0xff, 0x5f, 0x00, 0xff, 0x5f, 0x5f,
|
||||
0xff, 0x5f, 0x87, 0xff, 0x5f, 0xaf, 0xff, 0x5f, 0xd7, 0xff, 0x5f, 0xff,
|
||||
0xff, 0x87, 0x00, 0xff, 0x87, 0x5f, 0xff, 0x87, 0x87, 0xff, 0x87, 0xaf,
|
||||
0xff, 0x87, 0xd7, 0xff, 0x87, 0xff, 0xff, 0xaf, 0x00, 0xff, 0xaf, 0x5f,
|
||||
0xff, 0xaf, 0x87, 0xff, 0xaf, 0xaf, 0xff, 0xaf, 0xd7, 0xff, 0xaf, 0xff,
|
||||
0xff, 0xd7, 0x00, 0xff, 0xd7, 0x5f, 0xff, 0xd7, 0x87, 0xff, 0xd7, 0xaf,
|
||||
0xff, 0xd7, 0xd7, 0xff, 0xd7, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x5f,
|
||||
0xff, 0xff, 0x87, 0xff, 0xff, 0xaf, 0xff, 0xff, 0xd7, 0xff, 0xff, 0xff,
|
||||
0x08, 0x08, 0x08, 0x12, 0x12, 0x12, 0x1c, 0x1c, 0x1c, 0x26, 0x26, 0x26,
|
||||
0x30, 0x30, 0x30, 0x3a, 0x3a, 0x3a, 0x44, 0x44, 0x44, 0x4e, 0x4e, 0x4e,
|
||||
0x58, 0x58, 0x58, 0x62, 0x62, 0x62, 0x6c, 0x6c, 0x6c, 0x76, 0x76, 0x76,
|
||||
0x80, 0x80, 0x80, 0x8a, 0x8a, 0x8a, 0x94, 0x94, 0x94, 0x9e, 0x9e, 0x9e,
|
||||
0xa8, 0xa8, 0xa8, 0xb2, 0xb2, 0xb2, 0xbc, 0xbc, 0xbc, 0xc6, 0xc6, 0xc6,
|
||||
0xd0, 0xd0, 0xd0, 0xda, 0xda, 0xda, 0xe4, 0xe4, 0xe4, 0xee, 0xee, 0xee,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* VT340 undocumented behavior regarding the color palette reported
|
||||
* by Vertis Sidus(@vrtsds):
|
||||
* it loads the first fifteen colors as 1 through 15, and loads the
|
||||
* sixteenth color as 0.
|
||||
*/
|
||||
static const unsigned char pal_vt340_mono[] = {
|
||||
/* 1 Gray-2 */ 13 * 255 / 100, 13 * 255 / 100, 13 * 255 / 100,
|
||||
/* 2 Gray-4 */ 26 * 255 / 100, 26 * 255 / 100, 26 * 255 / 100,
|
||||
/* 3 Gray-6 */ 40 * 255 / 100, 40 * 255 / 100, 40 * 255 / 100,
|
||||
/* 4 Gray-1 */ 6 * 255 / 100, 6 * 255 / 100, 6 * 255 / 100,
|
||||
/* 5 Gray-3 */ 20 * 255 / 100, 20 * 255 / 100, 20 * 255 / 100,
|
||||
/* 6 Gray-5 */ 33 * 255 / 100, 33 * 255 / 100, 33 * 255 / 100,
|
||||
/* 7 White 7 */ 46 * 255 / 100, 46 * 255 / 100, 46 * 255 / 100,
|
||||
/* 8 Black 0 */ 0 * 255 / 100, 0 * 255 / 100, 0 * 255 / 100,
|
||||
/* 9 Gray-2 */ 13 * 255 / 100, 13 * 255 / 100, 13 * 255 / 100,
|
||||
/* 10 Gray-4 */ 26 * 255 / 100, 26 * 255 / 100, 26 * 255 / 100,
|
||||
/* 11 Gray-6 */ 40 * 255 / 100, 40 * 255 / 100, 40 * 255 / 100,
|
||||
/* 12 Gray-1 */ 6 * 255 / 100, 6 * 255 / 100, 6 * 255 / 100,
|
||||
/* 13 Gray-3 */ 20 * 255 / 100, 20 * 255 / 100, 20 * 255 / 100,
|
||||
/* 14 Gray-5 */ 33 * 255 / 100, 33 * 255 / 100, 33 * 255 / 100,
|
||||
/* 15 White 7 */ 46 * 255 / 100, 46 * 255 / 100, 46 * 255 / 100,
|
||||
/* 0 Black */ 0 * 255 / 100, 0 * 255 / 100, 0 * 255 / 100,
|
||||
};
|
||||
|
||||
|
||||
static const unsigned char pal_vt340_color[] = {
|
||||
/* 1 Blue */ 20 * 255 / 100, 20 * 255 / 100, 80 * 255 / 100,
|
||||
/* 2 Red */ 80 * 255 / 100, 13 * 255 / 100, 13 * 255 / 100,
|
||||
/* 3 Green */ 20 * 255 / 100, 80 * 255 / 100, 20 * 255 / 100,
|
||||
/* 4 Magenta */ 80 * 255 / 100, 20 * 255 / 100, 80 * 255 / 100,
|
||||
/* 5 Cyan */ 20 * 255 / 100, 80 * 255 / 100, 80 * 255 / 100,
|
||||
/* 6 Yellow */ 80 * 255 / 100, 80 * 255 / 100, 20 * 255 / 100,
|
||||
/* 7 Gray 50% */ 53 * 255 / 100, 53 * 255 / 100, 53 * 255 / 100,
|
||||
/* 8 Gray 25% */ 26 * 255 / 100, 26 * 255 / 100, 26 * 255 / 100,
|
||||
/* 9 Blue* */ 33 * 255 / 100, 33 * 255 / 100, 60 * 255 / 100,
|
||||
/* 10 Red* */ 60 * 255 / 100, 26 * 255 / 100, 26 * 255 / 100,
|
||||
/* 11 Green* */ 33 * 255 / 100, 60 * 255 / 100, 33 * 255 / 100,
|
||||
/* 12 Magenta* */ 60 * 255 / 100, 33 * 255 / 100, 60 * 255 / 100,
|
||||
/* 13 Cyan* */ 33 * 255 / 100, 60 * 255 / 100, 60 * 255 / 100,
|
||||
/* 14 Yellow* */ 60 * 255 / 100, 60 * 255 / 100, 33 * 255 / 100,
|
||||
/* 15 Gray 75% */ 80 * 255 / 100, 80 * 255 / 100, 80 * 255 / 100,
|
||||
/* 0 Black */ 0 * 255 / 100, 0 * 255 / 100, 0 * 255 / 100,
|
||||
};
|
||||
|
||||
|
||||
/* create dither context object */
|
||||
SIXELAPI SIXELSTATUS
|
||||
sixel_dither_new(
|
||||
sixel_dither_t /* out */ **ppdither, /* dither object to be created */
|
||||
int /* in */ ncolors, /* required colors */
|
||||
sixel_allocator_t /* in */ *allocator) /* allocator, null if you use
|
||||
default allocator */
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
size_t headsize;
|
||||
size_t datasize;
|
||||
size_t wholesize;
|
||||
int quality_mode;
|
||||
|
||||
/* ensure given pointer is not null */
|
||||
if (ppdither == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: ppdither is null.");
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (allocator == NULL) {
|
||||
status = sixel_allocator_new(&allocator, NULL, NULL, NULL, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
*ppdither = NULL;
|
||||
goto end;
|
||||
}
|
||||
} else {
|
||||
sixel_allocator_ref(allocator);
|
||||
}
|
||||
|
||||
if (ncolors < 0) {
|
||||
ncolors = SIXEL_PALETTE_MAX;
|
||||
quality_mode = SIXEL_QUALITY_HIGHCOLOR;
|
||||
} else {
|
||||
if (ncolors > SIXEL_PALETTE_MAX) {
|
||||
status = SIXEL_BAD_INPUT;
|
||||
goto end;
|
||||
} else if (ncolors < 1) {
|
||||
status = SIXEL_BAD_INPUT;
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: palette colors must be more than 0");
|
||||
goto end;
|
||||
}
|
||||
quality_mode = SIXEL_QUALITY_LOW;
|
||||
}
|
||||
headsize = sizeof(sixel_dither_t);
|
||||
datasize = (size_t)(ncolors * 3);
|
||||
wholesize = headsize + datasize;
|
||||
|
||||
*ppdither = (sixel_dither_t *)sixel_allocator_malloc(allocator, wholesize);
|
||||
if (*ppdither == NULL) {
|
||||
sixel_allocator_unref(allocator);
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
|
||||
(*ppdither)->ref = 1;
|
||||
(*ppdither)->palette = (unsigned char*)(*ppdither + 1);
|
||||
(*ppdither)->cachetable = NULL;
|
||||
(*ppdither)->reqcolors = ncolors;
|
||||
(*ppdither)->ncolors = ncolors;
|
||||
(*ppdither)->origcolors = (-1);
|
||||
(*ppdither)->keycolor = (-1);
|
||||
(*ppdither)->optimized = 0;
|
||||
(*ppdither)->optimize_palette = 0;
|
||||
(*ppdither)->complexion = 1;
|
||||
(*ppdither)->bodyonly = 0;
|
||||
(*ppdither)->method_for_largest = SIXEL_LARGE_NORM;
|
||||
(*ppdither)->method_for_rep = SIXEL_REP_CENTER_BOX;
|
||||
(*ppdither)->method_for_diffuse = SIXEL_DIFFUSE_FS;
|
||||
(*ppdither)->quality_mode = quality_mode;
|
||||
(*ppdither)->pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
(*ppdither)->allocator = allocator;
|
||||
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
/* create dither context object (deprecated) */
|
||||
SIXELAPI sixel_dither_t *
|
||||
sixel_dither_create(
|
||||
int /* in */ ncolors)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
sixel_dither_t *dither = NULL;
|
||||
|
||||
status = sixel_dither_new(&dither, ncolors, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
end:
|
||||
return dither;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI void
|
||||
sixel_dither_destroy(
|
||||
sixel_dither_t /* in */ *dither)
|
||||
{
|
||||
sixel_allocator_t *allocator;
|
||||
|
||||
if (dither) {
|
||||
allocator = dither->allocator;
|
||||
sixel_allocator_free(allocator, dither->cachetable);
|
||||
dither->cachetable = NULL;
|
||||
sixel_allocator_free(allocator, dither);
|
||||
sixel_allocator_unref(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI void
|
||||
sixel_dither_ref(
|
||||
sixel_dither_t /* in */ *dither)
|
||||
{
|
||||
/* TODO: be thread safe */
|
||||
++dither->ref;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI void
|
||||
sixel_dither_unref(
|
||||
sixel_dither_t /* in */ *dither)
|
||||
{
|
||||
/* TODO: be thread safe */
|
||||
if (dither != NULL && --dither->ref == 0) {
|
||||
sixel_dither_destroy(dither);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI sixel_dither_t *
|
||||
sixel_dither_get(
|
||||
int /* in */ builtin_dither)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
unsigned char *palette;
|
||||
int ncolors;
|
||||
int keycolor;
|
||||
sixel_dither_t *dither = NULL;
|
||||
|
||||
switch (builtin_dither) {
|
||||
case SIXEL_BUILTIN_MONO_DARK:
|
||||
ncolors = 2;
|
||||
palette = (unsigned char *)pal_mono_dark;
|
||||
keycolor = 0;
|
||||
break;
|
||||
case SIXEL_BUILTIN_MONO_LIGHT:
|
||||
ncolors = 2;
|
||||
palette = (unsigned char *)pal_mono_light;
|
||||
keycolor = 0;
|
||||
break;
|
||||
case SIXEL_BUILTIN_XTERM16:
|
||||
ncolors = 16;
|
||||
palette = (unsigned char *)pal_xterm256;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_XTERM256:
|
||||
ncolors = 256;
|
||||
palette = (unsigned char *)pal_xterm256;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_VT340_MONO:
|
||||
ncolors = 16;
|
||||
palette = (unsigned char *)pal_vt340_mono;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_VT340_COLOR:
|
||||
ncolors = 16;
|
||||
palette = (unsigned char *)pal_vt340_color;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_G1:
|
||||
ncolors = 2;
|
||||
palette = (unsigned char *)pal_gray_1bit;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_G2:
|
||||
ncolors = 4;
|
||||
palette = (unsigned char *)pal_gray_2bit;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_G4:
|
||||
ncolors = 16;
|
||||
palette = (unsigned char *)pal_gray_4bit;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
case SIXEL_BUILTIN_G8:
|
||||
ncolors = 256;
|
||||
palette = (unsigned char *)pal_gray_8bit;
|
||||
keycolor = (-1);
|
||||
break;
|
||||
default:
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = sixel_dither_new(&dither, ncolors, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
dither = NULL;
|
||||
goto end;
|
||||
}
|
||||
|
||||
dither->palette = palette;
|
||||
dither->keycolor = keycolor;
|
||||
dither->optimized = 1;
|
||||
dither->optimize_palette = 0;
|
||||
|
||||
end:
|
||||
return dither;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
sixel_dither_set_method_for_largest(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
int /* in */ method_for_largest)
|
||||
{
|
||||
if (method_for_largest == SIXEL_LARGE_AUTO) {
|
||||
method_for_largest = SIXEL_LARGE_NORM;
|
||||
}
|
||||
dither->method_for_largest = method_for_largest;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
sixel_dither_set_method_for_rep(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
int /* in */ method_for_rep)
|
||||
{
|
||||
if (method_for_rep == SIXEL_REP_AUTO) {
|
||||
method_for_rep = SIXEL_REP_CENTER_BOX;
|
||||
}
|
||||
dither->method_for_rep = method_for_rep;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
sixel_dither_set_quality_mode(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
int /* in */ quality_mode)
|
||||
{
|
||||
if (quality_mode == SIXEL_QUALITY_AUTO) {
|
||||
if (dither->ncolors <= 8) {
|
||||
quality_mode = SIXEL_QUALITY_HIGH;
|
||||
} else {
|
||||
quality_mode = SIXEL_QUALITY_LOW;
|
||||
}
|
||||
}
|
||||
dither->quality_mode = quality_mode;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI SIXELSTATUS
|
||||
sixel_dither_initialize(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
unsigned char /* in */ *data,
|
||||
int /* in */ width,
|
||||
int /* in */ height,
|
||||
int /* in */ pixelformat,
|
||||
int /* in */ method_for_largest,
|
||||
int /* in */ method_for_rep,
|
||||
int /* in */ quality_mode)
|
||||
{
|
||||
unsigned char *buf = NULL;
|
||||
unsigned char *normalized_pixels = NULL;
|
||||
unsigned char *input_pixels;
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
|
||||
/* ensure dither object is not null */
|
||||
if (dither == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: dither is null.");
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* increment ref count */
|
||||
sixel_dither_ref(dither);
|
||||
|
||||
sixel_dither_set_pixelformat(dither, pixelformat);
|
||||
|
||||
switch (pixelformat) {
|
||||
case SIXEL_PIXELFORMAT_RGB888:
|
||||
input_pixels = data;
|
||||
break;
|
||||
default:
|
||||
/* normalize pixelformat */
|
||||
normalized_pixels
|
||||
= (unsigned char *)sixel_allocator_malloc(dither->allocator, (size_t)(width * height * 3));
|
||||
if (normalized_pixels == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_initialize: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = sixel_helper_normalize_pixelformat(
|
||||
normalized_pixels,
|
||||
&pixelformat,
|
||||
data,
|
||||
pixelformat,
|
||||
width,
|
||||
height);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
input_pixels = normalized_pixels;
|
||||
break;
|
||||
}
|
||||
|
||||
sixel_dither_set_method_for_largest(dither, method_for_largest);
|
||||
sixel_dither_set_method_for_rep(dither, method_for_rep);
|
||||
sixel_dither_set_quality_mode(dither, quality_mode);
|
||||
|
||||
status = sixel_quant_make_palette(&buf,
|
||||
input_pixels,
|
||||
(unsigned int)(width * height * 3),
|
||||
SIXEL_PIXELFORMAT_RGB888,
|
||||
(unsigned int)dither->reqcolors,
|
||||
(unsigned int *)&dither->ncolors,
|
||||
(unsigned int *)&dither->origcolors,
|
||||
dither->method_for_largest,
|
||||
dither->method_for_rep,
|
||||
dither->quality_mode,
|
||||
dither->allocator);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
memcpy(dither->palette, buf, (size_t)(dither->ncolors * 3));
|
||||
|
||||
dither->optimized = 1;
|
||||
if (dither->origcolors <= dither->ncolors) {
|
||||
dither->method_for_diffuse = SIXEL_DIFFUSE_NONE;
|
||||
}
|
||||
|
||||
sixel_quant_free_palette(buf, dither->allocator);
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
free(normalized_pixels);
|
||||
|
||||
/* decrement ref count */
|
||||
sixel_dither_unref(dither);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
/* set diffusion type, choose from enum methodForDiffuse */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_diffusion_type(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
int /* in */ method_for_diffuse)
|
||||
{
|
||||
if (method_for_diffuse == SIXEL_DIFFUSE_AUTO) {
|
||||
if (dither->ncolors > 16) {
|
||||
method_for_diffuse = SIXEL_DIFFUSE_FS;
|
||||
} else {
|
||||
method_for_diffuse = SIXEL_DIFFUSE_ATKINSON;
|
||||
}
|
||||
}
|
||||
dither->method_for_diffuse = method_for_diffuse;
|
||||
}
|
||||
|
||||
|
||||
/* get number of palette colors */
|
||||
SIXELAPI int
|
||||
sixel_dither_get_num_of_palette_colors(
|
||||
sixel_dither_t /* in */ *dither)
|
||||
{
|
||||
return dither->ncolors;
|
||||
}
|
||||
|
||||
|
||||
/* get number of histogram colors */
|
||||
SIXELAPI int
|
||||
sixel_dither_get_num_of_histogram_colors(
|
||||
sixel_dither_t /* in */ *dither) /* dither context object */
|
||||
{
|
||||
return dither->origcolors;
|
||||
}
|
||||
|
||||
|
||||
/* typoed: remained for keeping compatibility */
|
||||
SIXELAPI int
|
||||
sixel_dither_get_num_of_histgram_colors(
|
||||
sixel_dither_t /* in */ *dither) /* dither context object */
|
||||
{
|
||||
return sixel_dither_get_num_of_histogram_colors(dither);
|
||||
}
|
||||
|
||||
|
||||
/* get palette */
|
||||
SIXELAPI unsigned char *
|
||||
sixel_dither_get_palette(
|
||||
sixel_dither_t /* in */ *dither) /* dither context object */
|
||||
{
|
||||
return dither->palette;
|
||||
}
|
||||
|
||||
|
||||
/* set palette */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_palette(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
unsigned char /* in */ *palette)
|
||||
{
|
||||
memcpy(dither->palette, palette, (size_t)(dither->ncolors * 3));
|
||||
}
|
||||
|
||||
|
||||
/* set the factor of complexion color correcting */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_complexion_score(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
int /* in */ score) /* complexion score (>= 1) */
|
||||
{
|
||||
dither->complexion = score;
|
||||
}
|
||||
|
||||
|
||||
/* set whether omitting palette difinition */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_body_only(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
int /* in */ bodyonly) /* 0: output palette section
|
||||
1: do not output palette section */
|
||||
{
|
||||
dither->bodyonly = bodyonly;
|
||||
}
|
||||
|
||||
|
||||
/* set whether optimize palette size */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_optimize_palette(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
int /* in */ do_opt) /* 0: optimize palette size
|
||||
1: don't optimize palette size */
|
||||
{
|
||||
dither->optimize_palette = do_opt;
|
||||
}
|
||||
|
||||
|
||||
/* set pixelformat */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_pixelformat(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
int /* in */ pixelformat) /* one of enum pixelFormat */
|
||||
{
|
||||
dither->pixelformat = pixelformat;
|
||||
}
|
||||
|
||||
|
||||
/* set transparent */
|
||||
SIXELAPI void
|
||||
sixel_dither_set_transparent(
|
||||
sixel_dither_t /* in */ *dither, /* dither context object */
|
||||
int /* in */ transparent) /* transparent color index */
|
||||
{
|
||||
dither->keycolor = transparent;
|
||||
}
|
||||
|
||||
|
||||
/* set transparent */
|
||||
SIXELAPI sixel_index_t *
|
||||
sixel_dither_apply_palette(
|
||||
sixel_dither_t /* in */ *dither,
|
||||
unsigned char /* in */ *pixels,
|
||||
int /* in */ width,
|
||||
int /* in */ height)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
size_t bufsize;
|
||||
sixel_index_t *dest = NULL;
|
||||
int ncolors;
|
||||
unsigned char *normalized_pixels = NULL;
|
||||
unsigned char *input_pixels;
|
||||
|
||||
/* ensure dither object is not null */
|
||||
if (dither == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_apply_palette: dither is null.");
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
goto end;
|
||||
}
|
||||
|
||||
sixel_dither_ref(dither);
|
||||
|
||||
bufsize = (size_t)(width * height) * sizeof(sixel_index_t);
|
||||
dest = (sixel_index_t *)sixel_allocator_malloc(dither->allocator, bufsize);
|
||||
if (dest == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* if quality_mode is full, do not use palette caching */
|
||||
if (dither->quality_mode == SIXEL_QUALITY_FULL) {
|
||||
dither->optimized = 0;
|
||||
}
|
||||
|
||||
if (dither->cachetable == NULL && dither->optimized) {
|
||||
if (dither->palette != pal_mono_dark && dither->palette != pal_mono_light) {
|
||||
dither->cachetable = (unsigned short *)sixel_allocator_calloc(dither->allocator,
|
||||
(size_t)(1 << 3 * 5),
|
||||
sizeof(unsigned short));
|
||||
if (dither->cachetable == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: sixel_allocator_calloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dither->pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
/* normalize pixelformat */
|
||||
normalized_pixels
|
||||
= (unsigned char *)sixel_allocator_malloc(dither->allocator, (size_t)(width * height * 3));
|
||||
if (normalized_pixels == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_dither_new: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
goto end;
|
||||
}
|
||||
status = sixel_helper_normalize_pixelformat(normalized_pixels,
|
||||
&dither->pixelformat,
|
||||
pixels, dither->pixelformat,
|
||||
width, height);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
input_pixels = normalized_pixels;
|
||||
} else {
|
||||
input_pixels = pixels;
|
||||
}
|
||||
|
||||
status = sixel_quant_apply_palette(dest,
|
||||
input_pixels,
|
||||
width, height, 3,
|
||||
dither->palette,
|
||||
dither->ncolors,
|
||||
dither->method_for_diffuse,
|
||||
dither->optimized,
|
||||
dither->optimize_palette,
|
||||
dither->complexion,
|
||||
dither->cachetable,
|
||||
&ncolors,
|
||||
dither->allocator);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
free(dest);
|
||||
dest = NULL;
|
||||
goto end;
|
||||
}
|
||||
|
||||
dither->ncolors = ncolors;
|
||||
|
||||
end:
|
||||
free(normalized_pixels);
|
||||
sixel_dither_unref(dither);
|
||||
return dest;
|
||||
}
|
||||
|
||||
|
||||
#if HAVE_TESTS
|
||||
static int
|
||||
test1(void)
|
||||
{
|
||||
sixel_dither_t *dither = NULL;
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
dither = sixel_dither_create(2);
|
||||
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
|
||||
# pragma GCC diagnostic pop
|
||||
#endif
|
||||
if (dither == NULL) {
|
||||
goto error;
|
||||
}
|
||||
sixel_dither_ref(dither);
|
||||
sixel_dither_unref(dither);
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
sixel_dither_unref(dither);
|
||||
return nret;
|
||||
}
|
||||
|
||||
static int
|
||||
test2(void)
|
||||
{
|
||||
sixel_dither_t *dither = NULL;
|
||||
int colors;
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
dither = sixel_dither_create(INT_MAX);
|
||||
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
|
||||
# pragma GCC diagnostic pop
|
||||
#endif
|
||||
if (dither == NULL) {
|
||||
goto error;
|
||||
}
|
||||
sixel_dither_set_body_only(dither, 1);
|
||||
colors = sixel_dither_get_num_of_histogram_colors(dither);
|
||||
if (colors != -1) {
|
||||
goto error;
|
||||
}
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
sixel_dither_unref(dither);
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI int
|
||||
sixel_dither_tests_main(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
size_t i;
|
||||
typedef int (* testcase)(void);
|
||||
|
||||
static testcase const testcases[] = {
|
||||
test1,
|
||||
test2,
|
||||
};
|
||||
|
||||
for (i = 0; i < sizeof(testcases) / sizeof(testcase); ++i) {
|
||||
nret = testcases[i]();
|
||||
if (nret != EXIT_SUCCESS) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
#endif /* HAVE_TESTS */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_DITHER_H
|
||||
#define LIBSIXEL_DITHER_H
|
||||
|
||||
#include <sixel.h>
|
||||
|
||||
/* dither context object */
|
||||
struct sixel_dither {
|
||||
unsigned int ref; /* reference counter */
|
||||
unsigned char *palette; /* palette definition */
|
||||
unsigned short *cachetable; /* cache table */
|
||||
int reqcolors; /* requested colors */
|
||||
int ncolors; /* active colors */
|
||||
int origcolors; /* original colors */
|
||||
int optimized; /* pixel is 15bpp compressable */
|
||||
int optimize_palette; /* minimize palette size */
|
||||
int complexion; /* for complexion correction */
|
||||
int bodyonly; /* do not output palette section if true */
|
||||
int method_for_largest; /* method for finding the largest dimention
|
||||
for splitting */
|
||||
int method_for_rep; /* method for choosing a color from the box */
|
||||
int method_for_diffuse; /* method for diffusing */
|
||||
int quality_mode; /* quality of histogram */
|
||||
int keycolor; /* background color */
|
||||
int pixelformat; /* pixelformat for internal processing */
|
||||
sixel_allocator_t *allocator; /* allocator */
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* apply palette */
|
||||
sixel_index_t *
|
||||
sixel_dither_apply_palette(struct sixel_dither /* in */ *dither,
|
||||
unsigned char /* in */ *pixels,
|
||||
int /* in */ width,
|
||||
int /* in */ height);
|
||||
|
||||
#if HAVE_TESTS
|
||||
int
|
||||
sixel_frame_tests_main(void);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LIBSIXEL_DITHER_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef MALLOC_STUB_H
|
||||
#define MALLOC_STUB_H
|
||||
|
||||
#if HAVE_SYS_TYPES_H
|
||||
#include <sys/types.h>
|
||||
#endif /* HAVE_SYS_TYPES_H */
|
||||
|
||||
#if !HAVE_MALLOC
|
||||
void * rpl_malloc(size_t n);
|
||||
#endif /* !HAVE_MALLOC */
|
||||
|
||||
#if !HAVE_REALLOC
|
||||
void * rpl_realloc(void *p, size_t n);
|
||||
#endif /* !HAVE_REALLOC */
|
||||
|
||||
#if 0
|
||||
int rpl_posix_memalign(void **memptr, size_t alignment, size_t size);
|
||||
#endif
|
||||
|
||||
#endif /* MALLOC_STUB_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+211
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2019 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
/* STDC_HEADERS */
|
||||
# include <stdio.h>
|
||||
# include <stdlib.h>
|
||||
|
||||
#if HAVE_ASSERT_H
|
||||
# include <assert.h>
|
||||
#endif /* HAVE_ASSERT_H */
|
||||
|
||||
#include <sixel.h>
|
||||
#include "output.h"
|
||||
|
||||
|
||||
/* create new output context object */
|
||||
SIXELAPI SIXELSTATUS
|
||||
sixel_output_new(
|
||||
sixel_output_t /* out */ **output,
|
||||
sixel_write_function /* in */ fn_write,
|
||||
void /* in */ *priv,
|
||||
sixel_allocator_t /* in */ *allocator)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
size_t size;
|
||||
|
||||
if (allocator == NULL) {
|
||||
status = sixel_allocator_new(&allocator, NULL, NULL, NULL, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
} else {
|
||||
sixel_allocator_ref(allocator);
|
||||
}
|
||||
size = sizeof(sixel_output_t) + SIXEL_OUTPUT_PACKET_SIZE * 2;
|
||||
|
||||
*output = (sixel_output_t *)sixel_allocator_malloc(allocator, size);
|
||||
if (*output == NULL) {
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_output_new: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
sixel_allocator_unref(allocator);
|
||||
goto end;
|
||||
}
|
||||
|
||||
(*output)->ref = 1;
|
||||
(*output)->has_8bit_control = 0;
|
||||
(*output)->has_sdm_glitch = 0;
|
||||
(*output)->has_gri_arg_limit = 1;
|
||||
(*output)->skip_dcs_envelope = 0;
|
||||
(*output)->palette_type = SIXEL_PALETTETYPE_AUTO;
|
||||
(*output)->fn_write = fn_write;
|
||||
(*output)->save_pixel = 0;
|
||||
(*output)->save_count = 0;
|
||||
(*output)->active_palette = (-1);
|
||||
(*output)->node_top = NULL;
|
||||
(*output)->node_free = NULL;
|
||||
(*output)->priv = priv;
|
||||
(*output)->pos = 0;
|
||||
(*output)->penetrate_multiplexer = 0;
|
||||
(*output)->encode_policy = SIXEL_ENCODEPOLICY_AUTO;
|
||||
(*output)->allocator = allocator;
|
||||
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
/* deprecated: create an output object */
|
||||
SIXELAPI sixel_output_t *
|
||||
sixel_output_create(sixel_write_function fn_write, void *priv)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
sixel_output_t *output = NULL;
|
||||
|
||||
status = sixel_output_new(&output, fn_write, priv, NULL);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
end:
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/* destroy output context object */
|
||||
SIXELAPI void
|
||||
sixel_output_destroy(sixel_output_t *output)
|
||||
{
|
||||
sixel_allocator_t *allocator;
|
||||
|
||||
if (output) {
|
||||
allocator = output->allocator;
|
||||
sixel_allocator_free(allocator, output);
|
||||
sixel_allocator_unref(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* increase reference count of output context object (thread-unsafe) */
|
||||
SIXELAPI void
|
||||
sixel_output_ref(sixel_output_t *output)
|
||||
{
|
||||
/* TODO: be thread-safe */
|
||||
++output->ref;
|
||||
}
|
||||
|
||||
|
||||
/* decrease reference count of output context object (thread-unsafe) */
|
||||
SIXELAPI void
|
||||
sixel_output_unref(sixel_output_t *output)
|
||||
{
|
||||
/* TODO: be thread-safe */
|
||||
if (output) {
|
||||
assert(output->ref > 0);
|
||||
output->ref--;
|
||||
if (output->ref == 0) {
|
||||
sixel_output_destroy(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* get 8bit output mode which indicates whether it uses C1 control characters */
|
||||
SIXELAPI int
|
||||
sixel_output_get_8bit_availability(sixel_output_t *output)
|
||||
{
|
||||
return output->has_8bit_control;
|
||||
}
|
||||
|
||||
|
||||
/* set 8bit output mode state */
|
||||
SIXELAPI void
|
||||
sixel_output_set_8bit_availability(sixel_output_t *output, int availability)
|
||||
{
|
||||
output->has_8bit_control = availability;
|
||||
}
|
||||
|
||||
|
||||
/* set whether limit arguments of DECGRI('!') to 255 */
|
||||
SIXELAPI void
|
||||
sixel_output_set_gri_arg_limit(
|
||||
sixel_output_t /* in */ *output, /* output context */
|
||||
int /* in */ value) /* 0: don't limit arguments of DECGRI
|
||||
1: limit arguments of DECGRI to 255 */
|
||||
{
|
||||
output->has_gri_arg_limit = value;
|
||||
}
|
||||
|
||||
|
||||
/* set GNU Screen penetration feature enable or disable */
|
||||
SIXELAPI void
|
||||
sixel_output_set_penetrate_multiplexer(sixel_output_t *output, int penetrate)
|
||||
{
|
||||
output->penetrate_multiplexer = penetrate;
|
||||
}
|
||||
|
||||
|
||||
/* set whether we skip DCS envelope */
|
||||
SIXELAPI void
|
||||
sixel_output_set_skip_dcs_envelope(sixel_output_t *output, int skip)
|
||||
{
|
||||
output->skip_dcs_envelope = skip;
|
||||
}
|
||||
|
||||
|
||||
/* set palette type: RGB or HLS */
|
||||
SIXELAPI void
|
||||
sixel_output_set_palette_type(sixel_output_t *output, int palettetype)
|
||||
{
|
||||
output->palette_type = palettetype;
|
||||
}
|
||||
|
||||
|
||||
/* set encodeing policy: auto, fast or size */
|
||||
SIXELAPI void
|
||||
sixel_output_set_encode_policy(sixel_output_t *output, int encode_policy)
|
||||
{
|
||||
output->encode_policy = encode_policy;
|
||||
}
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_OUTPUT_H
|
||||
#define LIBSIXEL_OUTPUT_H
|
||||
|
||||
typedef struct sixel_node {
|
||||
struct sixel_node *next;
|
||||
int pal;
|
||||
int sx;
|
||||
int mx;
|
||||
char *map;
|
||||
} sixel_node_t;
|
||||
|
||||
struct sixel_output {
|
||||
|
||||
int ref;
|
||||
sixel_allocator_t *allocator;
|
||||
|
||||
/* compatiblity flags */
|
||||
|
||||
/* 0: 7bit terminal,
|
||||
* 1: 8bit terminal */
|
||||
unsigned char has_8bit_control;
|
||||
|
||||
/* 0: the terminal has sixel scrolling
|
||||
* 1: the terminal does not have sixel scrolling */
|
||||
unsigned char has_sixel_scrolling;
|
||||
|
||||
/* 1: the argument of repeat introducer(DECGRI) is not limitted
|
||||
0: the argument of repeat introducer(DECGRI) is limitted 255 */
|
||||
unsigned char has_gri_arg_limit;
|
||||
|
||||
/* 0: DECSDM set (CSI ? 80 h) enables sixel scrolling
|
||||
1: DECSDM set (CSI ? 80 h) disables sixel scrolling */
|
||||
unsigned char has_sdm_glitch;
|
||||
|
||||
/* 0: do not skip DCS envelope
|
||||
* 1: skip DCS envelope */
|
||||
unsigned char skip_dcs_envelope;
|
||||
|
||||
/* PALETTETYPE_AUTO: select palette type automatically
|
||||
* PALETTETYPE_HLS : HLS color space
|
||||
* PALETTETYPE_RGB : RGB color space */
|
||||
unsigned char palette_type;
|
||||
|
||||
sixel_write_function fn_write;
|
||||
|
||||
int save_pixel;
|
||||
int save_count;
|
||||
int active_palette;
|
||||
|
||||
sixel_node_t *node_top;
|
||||
sixel_node_t *node_free;
|
||||
|
||||
int penetrate_multiplexer;
|
||||
int encode_policy;
|
||||
|
||||
void *priv;
|
||||
int pos;
|
||||
unsigned char buffer[1];
|
||||
};
|
||||
|
||||
#endif /* LIBSIXEL_OUTPUT_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+725
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2019 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
/* STDC_HEADERS */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if HAVE_MEMORY_H
|
||||
# include <memory.h>
|
||||
#endif /* HAVE_MEMORY_H */
|
||||
|
||||
#include <sixel.h>
|
||||
|
||||
static void
|
||||
get_rgb(unsigned char const *data,
|
||||
int const pixelformat,
|
||||
int depth,
|
||||
unsigned char *r,
|
||||
unsigned char *g,
|
||||
unsigned char *b)
|
||||
{
|
||||
unsigned int pixels = 0;
|
||||
#if SWAP_BYTES
|
||||
unsigned int low;
|
||||
unsigned int high;
|
||||
#endif
|
||||
int count = 0;
|
||||
|
||||
while (count < depth) {
|
||||
pixels = *(data + count) | (pixels << 8);
|
||||
count++;
|
||||
}
|
||||
|
||||
/* TODO: we should swap bytes (only necessary on LSByte first hardware?) */
|
||||
#if SWAP_BYTES
|
||||
if (depth == 2) {
|
||||
low = pixels & 0xff;
|
||||
high = (pixels >> 8) & 0xff;
|
||||
pixels = (low << 8) | high;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (pixelformat) {
|
||||
case SIXEL_PIXELFORMAT_RGB555:
|
||||
*r = ((pixels >> 10) & 0x1f) << 3;
|
||||
*g = ((pixels >> 5) & 0x1f) << 3;
|
||||
*b = ((pixels >> 0) & 0x1f) << 3;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB565:
|
||||
*r = ((pixels >> 11) & 0x1f) << 3;
|
||||
*g = ((pixels >> 5) & 0x3f) << 2;
|
||||
*b = ((pixels >> 0) & 0x1f) << 3;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB888:
|
||||
*r = (pixels >> 16) & 0xff;
|
||||
*g = (pixels >> 8) & 0xff;
|
||||
*b = (pixels >> 0) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_BGR555:
|
||||
*r = ((pixels >> 0) & 0x1f) << 3;
|
||||
*g = ((pixels >> 5) & 0x1f) << 3;
|
||||
*b = ((pixels >> 10) & 0x1f) << 3;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_BGR565:
|
||||
*r = ((pixels >> 0) & 0x1f) << 3;
|
||||
*g = ((pixels >> 5) & 0x3f) << 2;
|
||||
*b = ((pixels >> 11) & 0x1f) << 3;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_BGR888:
|
||||
*r = (pixels >> 0) & 0xff;
|
||||
*g = (pixels >> 8) & 0xff;
|
||||
*b = (pixels >> 16) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGBA8888:
|
||||
*r = (pixels >> 24) & 0xff;
|
||||
*g = (pixels >> 16) & 0xff;
|
||||
*b = (pixels >> 8) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_ARGB8888:
|
||||
*r = (pixels >> 16) & 0xff;
|
||||
*g = (pixels >> 8) & 0xff;
|
||||
*b = (pixels >> 0) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_BGRA8888:
|
||||
*r = (pixels >> 8) & 0xff;
|
||||
*g = (pixels >> 16) & 0xff;
|
||||
*b = (pixels >> 24) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_ABGR8888:
|
||||
*r = (pixels >> 0) & 0xff;
|
||||
*g = (pixels >> 8) & 0xff;
|
||||
*b = (pixels >> 16) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_GA88:
|
||||
*r = *g = *b = (pixels >> 8) & 0xff;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_G8:
|
||||
case SIXEL_PIXELFORMAT_AG88:
|
||||
*r = *g = *b = pixels & 0xff;
|
||||
break;
|
||||
default:
|
||||
*r = *g = *b = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI int
|
||||
sixel_helper_compute_depth(int pixelformat)
|
||||
{
|
||||
int depth = (-1); /* unknown */
|
||||
|
||||
switch (pixelformat) {
|
||||
case SIXEL_PIXELFORMAT_ARGB8888:
|
||||
case SIXEL_PIXELFORMAT_RGBA8888:
|
||||
case SIXEL_PIXELFORMAT_ABGR8888:
|
||||
case SIXEL_PIXELFORMAT_BGRA8888:
|
||||
depth = 4;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB888:
|
||||
case SIXEL_PIXELFORMAT_BGR888:
|
||||
depth = 3;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB555:
|
||||
case SIXEL_PIXELFORMAT_RGB565:
|
||||
case SIXEL_PIXELFORMAT_BGR555:
|
||||
case SIXEL_PIXELFORMAT_BGR565:
|
||||
case SIXEL_PIXELFORMAT_AG88:
|
||||
case SIXEL_PIXELFORMAT_GA88:
|
||||
depth = 2;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_G1:
|
||||
case SIXEL_PIXELFORMAT_G2:
|
||||
case SIXEL_PIXELFORMAT_G4:
|
||||
case SIXEL_PIXELFORMAT_G8:
|
||||
case SIXEL_PIXELFORMAT_PAL1:
|
||||
case SIXEL_PIXELFORMAT_PAL2:
|
||||
case SIXEL_PIXELFORMAT_PAL4:
|
||||
case SIXEL_PIXELFORMAT_PAL8:
|
||||
depth = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
expand_rgb(unsigned char *dst,
|
||||
unsigned char const *src,
|
||||
int width, int height,
|
||||
int pixelformat, int depth)
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
int dst_offset;
|
||||
int src_offset;
|
||||
unsigned char r, g, b;
|
||||
|
||||
for (y = 0; y < height; y++) {
|
||||
for (x = 0; x < width; x++) {
|
||||
src_offset = depth * (y * width + x);
|
||||
dst_offset = 3 * (y * width + x);
|
||||
get_rgb(src + src_offset, pixelformat, depth, &r, &g, &b);
|
||||
|
||||
*(dst + dst_offset + 0) = r;
|
||||
*(dst + dst_offset + 1) = g;
|
||||
*(dst + dst_offset + 2) = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static SIXELSTATUS
|
||||
expand_palette(unsigned char *dst, unsigned char const *src,
|
||||
int width, int height, int const pixelformat)
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
int x;
|
||||
int y;
|
||||
int i;
|
||||
int bpp; /* bit per plane */
|
||||
|
||||
switch (pixelformat) {
|
||||
case SIXEL_PIXELFORMAT_PAL1:
|
||||
case SIXEL_PIXELFORMAT_G1:
|
||||
bpp = 1;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_PAL2:
|
||||
case SIXEL_PIXELFORMAT_G2:
|
||||
bpp = 2;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_PAL4:
|
||||
case SIXEL_PIXELFORMAT_G4:
|
||||
bpp = 4;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_PAL8:
|
||||
case SIXEL_PIXELFORMAT_G8:
|
||||
for (i = 0; i < width * height; ++i, ++src) {
|
||||
*dst++ = *src;
|
||||
}
|
||||
status = SIXEL_OK;
|
||||
goto end;
|
||||
default:
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
sixel_helper_set_additional_message(
|
||||
"expand_palette: invalid pixelformat.");
|
||||
goto end;
|
||||
}
|
||||
|
||||
#if HAVE_DEBUG
|
||||
fprintf(stderr, "expanding PAL%d to PAL8...\n", bpp);
|
||||
#endif
|
||||
|
||||
for (y = 0; y < height; ++y) {
|
||||
for (x = 0; x < width * bpp / 8; ++x) {
|
||||
for (i = 0; i < 8 / bpp; ++i) {
|
||||
*dst++ = *src >> (8 / bpp - 1 - i) * bpp & ((1 << bpp) - 1);
|
||||
}
|
||||
src++;
|
||||
}
|
||||
x = width - x * 8 / bpp;
|
||||
if (x > 0) {
|
||||
for (i = 0; i < x; ++i) {
|
||||
*dst++ = *src >> (8 - (i + 1) * bpp) & ((1 << bpp) - 1);
|
||||
}
|
||||
src++;
|
||||
}
|
||||
}
|
||||
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI SIXELSTATUS
|
||||
sixel_helper_normalize_pixelformat(
|
||||
unsigned char /* out */ *dst, /* destination buffer */
|
||||
int /* out */ *dst_pixelformat, /* converted pixelformat */
|
||||
unsigned char const /* in */ *src, /* source pixels */
|
||||
int /* in */ src_pixelformat, /* format of source image */
|
||||
int /* in */ width, /* width of source image */
|
||||
int /* in */ height) /* height of source image */
|
||||
{
|
||||
SIXELSTATUS status = SIXEL_FALSE;
|
||||
|
||||
switch (src_pixelformat) {
|
||||
case SIXEL_PIXELFORMAT_G8:
|
||||
expand_rgb(dst, src, width, height, src_pixelformat, 1);
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB565:
|
||||
case SIXEL_PIXELFORMAT_RGB555:
|
||||
case SIXEL_PIXELFORMAT_BGR565:
|
||||
case SIXEL_PIXELFORMAT_BGR555:
|
||||
case SIXEL_PIXELFORMAT_GA88:
|
||||
case SIXEL_PIXELFORMAT_AG88:
|
||||
expand_rgb(dst, src, width, height, src_pixelformat, 2);
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGB888:
|
||||
case SIXEL_PIXELFORMAT_BGR888:
|
||||
expand_rgb(dst, src, width, height, src_pixelformat, 3);
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_RGBA8888:
|
||||
case SIXEL_PIXELFORMAT_ARGB8888:
|
||||
case SIXEL_PIXELFORMAT_BGRA8888:
|
||||
case SIXEL_PIXELFORMAT_ABGR8888:
|
||||
expand_rgb(dst, src, width, height, src_pixelformat, 4);
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_PAL1:
|
||||
case SIXEL_PIXELFORMAT_PAL2:
|
||||
case SIXEL_PIXELFORMAT_PAL4:
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_PAL8;
|
||||
status = expand_palette(dst, src, width, height, src_pixelformat);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_G1:
|
||||
case SIXEL_PIXELFORMAT_G2:
|
||||
case SIXEL_PIXELFORMAT_G4:
|
||||
*dst_pixelformat = SIXEL_PIXELFORMAT_G8;
|
||||
status = expand_palette(dst, src, width, height, src_pixelformat);
|
||||
if (SIXEL_FAILED(status)) {
|
||||
goto end;
|
||||
}
|
||||
break;
|
||||
case SIXEL_PIXELFORMAT_PAL8:
|
||||
memcpy(dst, src, (size_t)(width * height));
|
||||
*dst_pixelformat = src_pixelformat;
|
||||
break;
|
||||
default:
|
||||
status = SIXEL_BAD_ARGUMENT;
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = SIXEL_OK;
|
||||
|
||||
end:
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
#if HAVE_TESTS
|
||||
static int
|
||||
test1(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
unsigned char src[] = { 0x46, 0xf3, 0xe5 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[0] << 16 | dst[1] << 8 | dst[2]) != (src[0] << 16 | src[1] << 8 | src[2])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test1");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test2(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_RGB555;
|
||||
unsigned char src[] = { 0x47, 0x9c };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[0] >> 3 << 10 | dst[1] >> 3 << 5 | dst[2] >> 3) != (src[0] << 8 | src[1])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test2");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test3(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_RGB565;
|
||||
unsigned char src[] = { 0x47, 0x9c };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[0] >> 3 << 11 | dst[1] >> 2 << 5 | dst[2] >> 3) != (src[0] << 8 | src[1])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test3");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test4(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_BGR888;
|
||||
unsigned char src[] = { 0x46, 0xf3, 0xe5 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[2] << 16 | dst[1] << 8 | dst[0]) != (src[0] << 16 | src[1] << 8 | src[2])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test4");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test5(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_BGR555;
|
||||
unsigned char src[] = { 0x23, 0xc8 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[2] >> 3 << 10 | dst[1] >> 3 << 5 | dst[0] >> 3) != (src[0] << 8 | src[1])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test5");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test6(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_BGR565;
|
||||
unsigned char src[] = { 0x47, 0x88 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if ((dst[2] >> 3 << 11 | dst[1] >> 2 << 5 | dst[0] >> 3) != (src[0] << 8 | src[1])) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test6");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test7(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_AG88;
|
||||
unsigned char src[] = { 0x47, 0x88 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[0] != src[1]) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test7");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test8(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_GA88;
|
||||
unsigned char src[] = { 0x47, 0x88 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[0] != src[0]) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test8");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test9(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_RGBA8888;
|
||||
unsigned char src[] = { 0x46, 0xf3, 0xe5, 0xf0 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[0] != src[0]) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[1] != src[1]) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[2] != src[2]) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test8");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test10(void)
|
||||
{
|
||||
unsigned char dst[3];
|
||||
int dst_pixelformat = SIXEL_PIXELFORMAT_RGB888;
|
||||
int src_pixelformat = SIXEL_PIXELFORMAT_ARGB8888;
|
||||
unsigned char src[] = { 0x46, 0xf3, 0xe5, 0xf0 };
|
||||
int ret = 0;
|
||||
|
||||
int nret = EXIT_FAILURE;
|
||||
|
||||
ret = sixel_helper_normalize_pixelformat(dst,
|
||||
&dst_pixelformat,
|
||||
src,
|
||||
src_pixelformat,
|
||||
1,
|
||||
1);
|
||||
if (ret != 0) {
|
||||
goto error;
|
||||
}
|
||||
if (dst_pixelformat != SIXEL_PIXELFORMAT_RGB888) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[0] != src[1]) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[1] != src[2]) {
|
||||
goto error;
|
||||
}
|
||||
if (dst[2] != src[3]) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
perror("test8");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI int
|
||||
sixel_pixelformat_tests_main(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
size_t i;
|
||||
typedef int (* testcase)(void);
|
||||
|
||||
static testcase const testcases[] = {
|
||||
test1,
|
||||
test2,
|
||||
test3,
|
||||
test4,
|
||||
test5,
|
||||
test6,
|
||||
test7,
|
||||
test8,
|
||||
test9,
|
||||
test10,
|
||||
};
|
||||
|
||||
for (i = 0; i < sizeof(testcases) / sizeof(testcase); ++i) {
|
||||
nret = testcases[i]();
|
||||
if (nret != EXIT_SUCCESS) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
#endif /* HAVE_TESTS */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+1549
File diff suppressed because it is too large
Load Diff
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_QUANT_H
|
||||
#define LIBSIXEL_QUANT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <sixel.h>
|
||||
|
||||
/* choose colors using median-cut method */
|
||||
SIXELSTATUS
|
||||
sixel_quant_make_palette(
|
||||
unsigned char /* out */ **result,
|
||||
unsigned const char /* in */ *data, /* data for sampling */
|
||||
unsigned int /* in */ length, /* data size */
|
||||
int /* in */ pixelformat,
|
||||
unsigned int /* in */ reqcolors,
|
||||
unsigned int /* in */ *ncolors,
|
||||
unsigned int /* in */ *origcolors,
|
||||
int /* in */ methodForLargest,
|
||||
int /* in */ methodForRep,
|
||||
int /* in */ qualityMode,
|
||||
sixel_allocator_t /* in */ *allocator);
|
||||
|
||||
|
||||
/* apply color palette into specified pixel buffers */
|
||||
SIXELSTATUS
|
||||
sixel_quant_apply_palette(
|
||||
sixel_index_t /* out */ *result,
|
||||
unsigned char /* in */ *data,
|
||||
int /* in */ width,
|
||||
int /* in */ height,
|
||||
int /* in */ pixelformat,
|
||||
unsigned char /* in */ *palette,
|
||||
int /* in */ reqcolor,
|
||||
int const /* in */ methodForDiffuse,
|
||||
int /* in */ foptimize,
|
||||
int /* in */ foptimize_palette,
|
||||
int /* in */ complexion,
|
||||
unsigned short /* in */ *cachetable,
|
||||
int /* in */ *ncolor,
|
||||
sixel_allocator_t /* in */ *allocator);
|
||||
|
||||
|
||||
/* deallocate specified palette */
|
||||
void
|
||||
sixel_quant_free_palette(
|
||||
unsigned char /* in */ *data,
|
||||
sixel_allocator_t /* in */ *allocator);
|
||||
|
||||
#if HAVE_TESTS
|
||||
int
|
||||
sixel_quant_tests_main(void);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LIBSIXEL_QUANT_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "https://github.com/saitoha/libsixel",
|
||||
"license": "MIT ( embed in source )",
|
||||
"version": "1.8.7-r2",
|
||||
"author": "Hayaki Saito",
|
||||
"modifications": [
|
||||
"sixel.h: SIXELAPI is empty, so the statically linked subset exports nothing",
|
||||
"config.h: hand-written, because only the vendored files read it",
|
||||
"tosixel.c: DCS P2 initialised to 1, so unset pixels are not filled with colour-table entry 0",
|
||||
"output.c: release the allocator reference if output-context allocation fails"
|
||||
]
|
||||
}
|
||||
Vendored
+1173
File diff suppressed because it is too large
Load Diff
Vendored
+355
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
/* STDC_HEADERS */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if HAVE_MEMORY_H
|
||||
# include <memory.h>
|
||||
#endif /* HAVE_MEMORY_H */
|
||||
#ifdef HAVE_STRING_H
|
||||
# include <string.h>
|
||||
#endif /* HAVE_STRING_H */
|
||||
#ifdef HAVE_ERRNO_H
|
||||
# include <errno.h>
|
||||
#endif /* HAVE_ERRNO_H */
|
||||
#ifdef HAVE_LIBCURL
|
||||
# include <curl/curl.h>
|
||||
#endif /* HAVE_LIBCURL */
|
||||
|
||||
#include <sixel.h>
|
||||
#include "status.h"
|
||||
|
||||
#define SIXEL_MESSAGE_OK ("succeeded")
|
||||
#define SIXEL_MESSAGE_FALSE ("unexpected error (SIXEL_FALSE)");
|
||||
#define SIXEL_MESSAGE_UNEXPECTED ("unexpected error")
|
||||
#define SIXEL_MESSAGE_INTERRUPTED ("interrupted by a signal")
|
||||
#define SIXEL_MESSAGE_BAD_ALLOCATION ("runtime error: bad allocation error")
|
||||
#define SIXEL_MESSAGE_BAD_ARGUMENT ("runtime error: bad argument detected")
|
||||
#define SIXEL_MESSAGE_BAD_INPUT ("runtime error: bad input detected")
|
||||
#define SIXEL_MESSAGE_BAD_INTEGER_OVERFLOW ("runtime error: integer overflow")
|
||||
#define SIXEL_MESSAGE_RUNTIME_ERROR ("runtime error")
|
||||
#define SIXEL_MESSAGE_LOGIC_ERROR ("logic error")
|
||||
#define SIXEL_MESSAGE_NOT_IMPLEMENTED ("feature error: not implemented")
|
||||
#define SIXEL_MESSAGE_FEATURE_ERROR ("feature error")
|
||||
#define SIXEL_MESSAGE_STBI_ERROR ("stb_image error")
|
||||
#define SIXEL_MESSAGE_STBIW_ERROR ("stb_image_write error")
|
||||
#define SIXEL_MESSAGE_JPEG_ERROR ("libjpeg error")
|
||||
#define SIXEL_MESSAGE_PNG_ERROR ("libpng error")
|
||||
#define SIXEL_MESSAGE_GDK_ERROR ("GDK error")
|
||||
#define SIXEL_MESSAGE_GD_ERROR ("GD error")
|
||||
|
||||
|
||||
static char g_buffer[1024] = { 0x0 };
|
||||
|
||||
/* set detailed error message (thread-unsafe) */
|
||||
SIXELAPI void
|
||||
sixel_helper_set_additional_message(
|
||||
const char /* in */ *message /* error message */
|
||||
)
|
||||
{
|
||||
size_t len;
|
||||
|
||||
if (message == 0)
|
||||
return;
|
||||
len = strlen(message);
|
||||
memcpy(g_buffer, message, len < sizeof(g_buffer) ? len: sizeof(g_buffer) - 1);
|
||||
g_buffer[sizeof(g_buffer) - 1] = 0;
|
||||
}
|
||||
|
||||
|
||||
/* get detailed error message (thread-unsafe) */
|
||||
SIXELAPI char const *
|
||||
sixel_helper_get_additional_message(void)
|
||||
{
|
||||
return g_buffer;
|
||||
}
|
||||
|
||||
|
||||
/* convert error status code int formatted string */
|
||||
SIXELAPI char const *
|
||||
sixel_helper_format_error(
|
||||
SIXELSTATUS /* in */ status /* status code */
|
||||
)
|
||||
{
|
||||
static char buffer[1024];
|
||||
char const *error_string;
|
||||
char *p;
|
||||
size_t len;
|
||||
|
||||
switch (status & 0x1000) {
|
||||
case SIXEL_OK:
|
||||
switch (status) {
|
||||
case SIXEL_INTERRUPTED:
|
||||
error_string = SIXEL_MESSAGE_INTERRUPTED;
|
||||
break;
|
||||
case SIXEL_OK:
|
||||
default:
|
||||
error_string = SIXEL_MESSAGE_OK;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SIXEL_FALSE:
|
||||
switch (status & 0x1f00) {
|
||||
case SIXEL_RUNTIME_ERROR:
|
||||
switch (status) {
|
||||
case SIXEL_BAD_ALLOCATION:
|
||||
error_string = SIXEL_MESSAGE_BAD_ALLOCATION;
|
||||
break;
|
||||
case SIXEL_BAD_ARGUMENT:
|
||||
error_string = SIXEL_MESSAGE_BAD_ARGUMENT;
|
||||
break;
|
||||
case SIXEL_BAD_INPUT:
|
||||
error_string = SIXEL_MESSAGE_BAD_INPUT;
|
||||
break;
|
||||
case SIXEL_BAD_INTEGER_OVERFLOW:
|
||||
error_string = SIXEL_MESSAGE_BAD_INTEGER_OVERFLOW;
|
||||
break;
|
||||
default:
|
||||
error_string = SIXEL_MESSAGE_RUNTIME_ERROR;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SIXEL_LOGIC_ERROR:
|
||||
error_string = SIXEL_MESSAGE_LOGIC_ERROR;
|
||||
break;
|
||||
case SIXEL_FEATURE_ERROR:
|
||||
switch (status) {
|
||||
case SIXEL_NOT_IMPLEMENTED:
|
||||
error_string = SIXEL_MESSAGE_NOT_IMPLEMENTED;
|
||||
break;
|
||||
default:
|
||||
error_string = SIXEL_MESSAGE_FEATURE_ERROR;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SIXEL_LIBC_ERROR:
|
||||
p = strerror(errno);
|
||||
len = strlen(p) + 1;
|
||||
memcpy(buffer, p, len < sizeof(buffer) ? len: sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = 0;
|
||||
error_string = buffer;
|
||||
break;
|
||||
#ifdef HAVE_LIBCURL
|
||||
case SIXEL_CURL_ERROR:
|
||||
error_string = curl_easy_strerror(status & 0xff);
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAVE_JPEG
|
||||
case SIXEL_JPEG_ERROR:
|
||||
error_string = SIXEL_MESSAGE_JPEG_ERROR;
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAVE_LIBPNG
|
||||
case SIXEL_PNG_ERROR:
|
||||
error_string = SIXEL_MESSAGE_PNG_ERROR;
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAVE_GDK_PIXBUF2
|
||||
case SIXEL_GDK_ERROR:
|
||||
error_string = SIXEL_MESSAGE_GDK_ERROR;
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAVE_GD
|
||||
case SIXEL_GD_ERROR:
|
||||
error_string = SIXEL_MESSAGE_GD_ERROR;
|
||||
break;
|
||||
#endif
|
||||
case SIXEL_STBI_ERROR:
|
||||
error_string = SIXEL_MESSAGE_STBI_ERROR;
|
||||
break;
|
||||
case SIXEL_STBIW_ERROR:
|
||||
error_string = SIXEL_MESSAGE_STBIW_ERROR;
|
||||
break;
|
||||
case SIXEL_FALSE:
|
||||
error_string = SIXEL_MESSAGE_FALSE;
|
||||
break;
|
||||
default:
|
||||
error_string = SIXEL_MESSAGE_UNEXPECTED;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
error_string = SIXEL_MESSAGE_UNEXPECTED;
|
||||
break;
|
||||
}
|
||||
return error_string;
|
||||
}
|
||||
|
||||
|
||||
#if HAVE_TESTS
|
||||
static int
|
||||
test1(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
char const *message;
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_OK);
|
||||
if (strcmp(message, SIXEL_MESSAGE_OK) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_INTERRUPTED);
|
||||
if (strcmp(message, SIXEL_MESSAGE_INTERRUPTED) != 0) {
|
||||
goto error;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
error:
|
||||
perror("test1");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
test2(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
char const *message;
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_BAD_ALLOCATION);
|
||||
if (strcmp(message, SIXEL_MESSAGE_BAD_ALLOCATION) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_BAD_ARGUMENT);
|
||||
if (strcmp(message, SIXEL_MESSAGE_BAD_ARGUMENT) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_BAD_INPUT);
|
||||
if (strcmp(message, SIXEL_MESSAGE_BAD_INPUT) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_RUNTIME_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_RUNTIME_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_LOGIC_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_LOGIC_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_NOT_IMPLEMENTED);
|
||||
if (strcmp(message, SIXEL_MESSAGE_NOT_IMPLEMENTED) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_FEATURE_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_FEATURE_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_LIBC_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_UNEXPECTED) == 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBCURL
|
||||
message = sixel_helper_format_error(SIXEL_CURL_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_UNEXPECTED) == 0) {
|
||||
goto error;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_JPEG
|
||||
message = sixel_helper_format_error(SIXEL_JPEG_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_JPEG_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_LIBPNG
|
||||
message = sixel_helper_format_error(SIXEL_PNG_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_PNG_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_GD
|
||||
message = sixel_helper_format_error(SIXEL_GD_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_GD_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_GDK_PIXBUF2
|
||||
message = sixel_helper_format_error(SIXEL_GDK_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_GDK_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
#endif
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_STBI_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_STBI_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
message = sixel_helper_format_error(SIXEL_STBIW_ERROR);
|
||||
if (strcmp(message, SIXEL_MESSAGE_STBIW_ERROR) != 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
error:
|
||||
perror("test2");
|
||||
return nret;
|
||||
}
|
||||
|
||||
|
||||
SIXELAPI int
|
||||
sixel_status_tests_main(void)
|
||||
{
|
||||
int nret = EXIT_FAILURE;
|
||||
size_t i;
|
||||
typedef int (* testcase)(void);
|
||||
|
||||
static testcase const testcases[] = {
|
||||
test1,
|
||||
test2,
|
||||
};
|
||||
|
||||
for (i = 0; i < sizeof(testcases) / sizeof(testcase); ++i) {
|
||||
nret = testcases[i]();
|
||||
if (nret != EXIT_SUCCESS) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
nret = EXIT_SUCCESS;
|
||||
|
||||
error:
|
||||
return nret;
|
||||
}
|
||||
#endif /* HAVE_TESTS */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Hayaki Saito
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBSIXEL_STATUS_H
|
||||
#define LIBSIXEL_STATUS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if HAVE_TESTS
|
||||
int
|
||||
sixel_status_tests_main(void);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LIBSIXEL_STATUS_H */
|
||||
|
||||
/* emacs Local Variables: */
|
||||
/* emacs mode: c */
|
||||
/* emacs tab-width: 4 */
|
||||
/* emacs indent-tabs-mode: nil */
|
||||
/* emacs c-basic-offset: 4 */
|
||||
/* emacs End: */
|
||||
/* vim: set expandtab ts=4 sts=4 sw=4 : */
|
||||
/* EOF */
|
||||
Vendored
+1635
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"home": "https://github.com/ibireme/yyjson",
|
||||
"license": "MIT ( embed in source )",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"author": "ibireme"
|
||||
}
|
||||
|
||||
Vendored
+700
-285
File diff suppressed because it is too large
Load Diff
Vendored
+911
-674
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,10 @@ typedef struct FFPlatformSysinfo {
|
||||
FFstrbuf release;
|
||||
FFstrbuf version;
|
||||
FFstrbuf architecture;
|
||||
uint32_t pageSize;
|
||||
// The base-2 logarithm of the memory page size, i.e. a page is `1 << pageSizeShift` bytes.
|
||||
// Page sizes are powers of two on every supported platform. Storing the exponent instead of
|
||||
// the byte count keeps page count arithmetic in 64 bits and makes the invariant explicit.
|
||||
uint32_t pageSizeShift;
|
||||
} FFPlatformSysinfo;
|
||||
|
||||
typedef struct FFPlatform {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// A cache for a value that is built at most once per generation.
|
||||
//
|
||||
// The value itself is owned by the entry owner (usually a file scope `static`); the entry only
|
||||
// remembers whether that value is still valid and knows how to drop it. `--dynamic-interval`
|
||||
// starts a new generation for every round, dropping all the values that have been built so far,
|
||||
// so the modules re-detect them instead of replaying the first round's snapshot.
|
||||
//
|
||||
// An entry registers itself the first time it is used. There is therefore no central list to keep
|
||||
// in sync, and a cache cannot be forgotten in it. Do not use this type for state that is carried
|
||||
// across calls to derive a rate (cpuusage, diskio, netio, top): that is a baseline rather than a
|
||||
// snapshot, and dropping it makes the first sample of a round read as zero.
|
||||
//
|
||||
// The storage pointed to by `storage` must stay valid for the whole run; only the value behind it
|
||||
// is dropped by an invalidation.
|
||||
//
|
||||
// Not thread safe: entries are only touched from a single thread, on a frame boundary, while the
|
||||
// modules are not running.
|
||||
typedef struct FFcacheEntry {
|
||||
const char* name; // For diagnostics.
|
||||
void* storage; // Owned by the entry owner. May be nullptr.
|
||||
void (*init)(void* storage); // Optional. Runs whenever the entry is stale.
|
||||
void (*destroy)(void* storage); // Optional. Must be idempotent, and tolerate a partially initialized storage.
|
||||
bool initialized;
|
||||
bool registered;
|
||||
struct FFcacheEntry* next; // Internal: registry link.
|
||||
} FFcacheEntry;
|
||||
|
||||
// Returns `entry->storage`, running `init` on it first when the entry is stale. The entry is marked
|
||||
// initialized before `init` runs, so a call made from within `init` returns the partially built
|
||||
// value instead of recursing.
|
||||
[[gnu::nonnull(1)]] void* ffCacheGet(FFcacheEntry* entry);
|
||||
|
||||
// Staleness test for entries whose initialization needs arguments that are not part of the cache
|
||||
// identity (such as the cover file request of `ffDetectMedia`) and therefore cannot live behind
|
||||
// `init(void*)`. Returns true exactly once per generation: the caller must then build the value
|
||||
// itself.
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffCacheBeginInit(FFcacheEntry* entry);
|
||||
|
||||
// Drops the cached value of one entry, so that the next request builds it again. Idempotent, and
|
||||
// safe on an entry that has never been used.
|
||||
[[gnu::nonnull(1)]] void ffCacheInvalidate(FFcacheEntry* entry);
|
||||
|
||||
// Drops every value that has been built so far. Called once per `--dynamic-interval` round.
|
||||
void ffCacheInvalidateAll(void);
|
||||
+18
-15
@@ -15,17 +15,18 @@ typedef struct FFlist {
|
||||
} FFlist;
|
||||
|
||||
// Removes the first element, and copy its value to `*result`
|
||||
bool ffListShift(FFlist* list, uint32_t elementSize, void* __restrict result);
|
||||
// `result` is only written when the list is not empty, so it may be null for an empty list
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffListShift(FFlist* list, uint32_t elementSize, void* __restrict result);
|
||||
// Removes the last element, and copy its value to `*result`
|
||||
bool ffListPop(FFlist* list, uint32_t elementSize, void* __restrict result);
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffListPop(FFlist* list, uint32_t elementSize, void* __restrict result);
|
||||
|
||||
static inline void ffListInit(FFlist* list) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListInit(FFlist* list) {
|
||||
list->capacity = 0;
|
||||
list->length = 0;
|
||||
list->data = nullptr;
|
||||
}
|
||||
|
||||
static inline void ffListInitA(FFlist* list, uint32_t elementSize, uint32_t capacity) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListInitA(FFlist* list, uint32_t elementSize, uint32_t capacity) {
|
||||
ffListInit(list);
|
||||
list->capacity = capacity;
|
||||
list->data = __builtin_expect(capacity == 0, 0) ? nullptr : (uint8_t*) malloc((size_t) capacity * elementSize);
|
||||
@@ -43,12 +44,13 @@ static inline void ffListInitA(FFlist* list, uint32_t elementSize, uint32_t capa
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline void* ffListGet(const FFlist* list, uint32_t elementSize, uint32_t index) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline void* ffListGet(const FFlist* list, uint32_t elementSize, uint32_t index) {
|
||||
assert(list->capacity > index);
|
||||
return list->data + (index * elementSize);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffListFirstIndexComp(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
|
||||
// Not `pure`: `compFunc` is caller-supplied and may have side effects
|
||||
[[gnu::nonnull(1, 3, 4), nodiscard]] static inline uint32_t ffListFirstIndexComp(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
|
||||
for (uint32_t i = 0; i < list->length; i++) {
|
||||
if (compFunc(ffListGet(list, elementSize, i), compElement)) {
|
||||
return i;
|
||||
@@ -58,16 +60,17 @@ static inline void ffListInitA(FFlist* list, uint32_t elementSize, uint32_t capa
|
||||
return list->length;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffListContains(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
|
||||
// Not `pure`, same reason as ffListFirstIndexComp
|
||||
[[gnu::nonnull(1, 3, 4), nodiscard]] static inline bool ffListContains(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
|
||||
return ffListFirstIndexComp(list, elementSize, compElement, compFunc) != list->length;
|
||||
}
|
||||
|
||||
static inline void ffListSort(FFlist* list, uint32_t elementSize, int (*compar)(const void*, const void*)) {
|
||||
[[gnu::nonnull(1, 3)]] static inline void ffListSort(FFlist* list, uint32_t elementSize, int (*compar)(const void*, const void*)) {
|
||||
qsort(list->data, list->length, elementSize, compar);
|
||||
}
|
||||
|
||||
// Move the contents of `src` into `list`, and left `src` empty
|
||||
static inline void ffListInitMove(FFlist* list, FFlist* src) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListInitMove(FFlist* list, FFlist* src) {
|
||||
if (src) {
|
||||
list->capacity = src->capacity;
|
||||
list->length = src->length;
|
||||
@@ -78,7 +81,7 @@ static inline void ffListInitMove(FFlist* list, FFlist* src) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline void ffListDestroy(FFlist* list) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListDestroy(FFlist* list) {
|
||||
if (!list->data) {
|
||||
return;
|
||||
}
|
||||
@@ -89,11 +92,11 @@ static inline void ffListDestroy(FFlist* list) {
|
||||
list->data = nullptr;
|
||||
}
|
||||
|
||||
static inline void ffListClear(FFlist* list) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListClear(FFlist* list) {
|
||||
list->length = 0;
|
||||
}
|
||||
|
||||
static inline void ffListReserve(FFlist* list, uint32_t elementSize, uint32_t newCapacity) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListReserve(FFlist* list, uint32_t elementSize, uint32_t newCapacity) {
|
||||
if (__builtin_expect(newCapacity <= list->capacity, false)) {
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +105,7 @@ static inline void ffListReserve(FFlist* list, uint32_t elementSize, uint32_t ne
|
||||
list->capacity = newCapacity;
|
||||
}
|
||||
|
||||
static inline void* ffListAdd(FFlist* list, uint32_t elementSize) {
|
||||
[[gnu::nonnull(1)]] static inline void* ffListAdd(FFlist* list, uint32_t elementSize) {
|
||||
if (__builtin_expect(list->length == list->capacity, false)) {
|
||||
ffListReserve(list, elementSize, list->capacity == 0 ? FF_LIST_DEFAULT_ALLOC : list->capacity * 2);
|
||||
}
|
||||
@@ -111,13 +114,13 @@ static inline void* ffListAdd(FFlist* list, uint32_t elementSize) {
|
||||
return ffListGet(list, elementSize, list->length - 1);
|
||||
}
|
||||
|
||||
static inline void ffListRemoveAt(FFlist* list, uint32_t elementSize, uint32_t index) {
|
||||
[[gnu::nonnull(1)]] static inline void ffListRemoveAt(FFlist* list, uint32_t elementSize, uint32_t index) {
|
||||
assert(list->length > index);
|
||||
memmove(list->data + (index * elementSize), list->data + ((index + 1) * elementSize), (size_t) (list->length - index - 1) * elementSize);
|
||||
--list->length;
|
||||
}
|
||||
|
||||
static inline void ffListInsertAt(FFlist* list, uint32_t elementSize, uint32_t index, const void* element) {
|
||||
[[gnu::nonnull(1, 4)]] static inline void ffListInsertAt(FFlist* list, uint32_t elementSize, uint32_t index, const void* element) {
|
||||
assert(list->length >= index);
|
||||
ffListAdd(list, elementSize);
|
||||
memmove(list->data + ((index + 1) * elementSize), list->data + (index * elementSize), (size_t) (list->length - index - 1) * elementSize);
|
||||
|
||||
+162
-132
@@ -24,6 +24,29 @@ __stdcall char* StrStrIA(const char* lpFirst, const char* lpSrch);
|
||||
|
||||
#define FASTFETCH_STRBUF_DEFAULT_ALLOC 32
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Contract attributes used throughout this header:
|
||||
//
|
||||
// `gnu::pure` marks a function that reads memory (through its arguments and through globals) but
|
||||
// writes nothing observable, so the compiler may cache and reorder calls to it. It is applied to
|
||||
// the comparison / search accessors, and deliberately NOT to:
|
||||
// * the `To*` converters -- `strtod` / `strtoull` / `strtoll` write `errno`;
|
||||
// * `ffStrbufEndsWithFn` -- the caller-supplied `fn` may have side effects;
|
||||
// * `ffStrbufWriteTo` / `ffStrbufPutTo` -- they write to a `FILE*`.
|
||||
//
|
||||
// `gnu::nonnull(N)` is added wherever the function dereferences argument N unconditionally. The
|
||||
// two cannot be combined with a null check: clang reports `-Wtautological-pointer-compare` /
|
||||
// `-Wpointer-bool-conversion` (both in `-Wall`) when a `nonnull` parameter is compared against
|
||||
// null, so a redundant `assert(p != nullptr)` is dropped in favour of the attribute. Asserts that
|
||||
// state something `nonnull` cannot express -- pointer aliasing (`value != strbuf`), index ranges
|
||||
// (`start <= strbuf->length`), or a non-pointer invariant -- are kept.
|
||||
//
|
||||
// `nodiscard` is NOT applied to the "was the buffer modified" boolean returns
|
||||
// (`ffStrbufSubstr*`, `ffStrbufRemoveSubstr`, `ffStrbufEnsureEndsWithC`, ...). Ignoring that
|
||||
// result is a normal, intended use of those functions -- `ffStrbufSubstrBefore` alone has 153 call
|
||||
// sites and ~147 of them discard the result -- so `nodiscard` would only produce noise.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// static string (allocated == 0), chars points to a string literal
|
||||
// dynamic string (allocated > 0), chars points to a heap allocated buffer
|
||||
typedef struct FFstrbuf {
|
||||
@@ -32,67 +55,70 @@ typedef struct FFstrbuf {
|
||||
char* chars;
|
||||
} FFstrbuf;
|
||||
|
||||
static inline void ffStrbufInit(FFstrbuf* strbuf);
|
||||
void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate);
|
||||
void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments);
|
||||
void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr);
|
||||
[[gnu::format(printf, 2, 3)]] void ffStrbufInitF(FFstrbuf* strbuf, const char* format, ...);
|
||||
[[gnu::format(printf, 1, 2)]] [[nodiscard]] FFstrbuf ffStrbufCreateF(const char* format, ...);
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufInit(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate);
|
||||
[[gnu::nonnull(1, 2), gnu::format(printf, 2, 0)]] void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments);
|
||||
[[gnu::nonnull(1, 3)]] void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr);
|
||||
[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufInitF(FFstrbuf* strbuf, const char* format, ...);
|
||||
[[gnu::format(printf, 1, 2), gnu::nonnull(1), nodiscard]] FFstrbuf ffStrbufCreateF(const char* format, ...);
|
||||
|
||||
void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free);
|
||||
void ffStrbufEnsureFreeNoCheck(FFstrbuf* strbuf, uint32_t free);
|
||||
[[gnu::nonnull(1)]] void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free);
|
||||
[[gnu::nonnull(1)]] void ffStrbufEnsureFreeNoCheck(FFstrbuf* strbuf, uint32_t free);
|
||||
|
||||
static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value);
|
||||
void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int));
|
||||
[[gnu::format(printf, 2, 3)]] void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...);
|
||||
void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments);
|
||||
const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until);
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value);
|
||||
[[gnu::nonnull(1, 3)]] void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int));
|
||||
[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...);
|
||||
[[gnu::nonnull(1, 2), gnu::format(printf, 2, 0)]] void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments);
|
||||
// Returns the pointer to the terminator, or nullptr if `value` was nullptr; callers are free to
|
||||
// ignore it, so not `nodiscard`.
|
||||
[[gnu::nonnull(1)]] const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until);
|
||||
|
||||
void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value);
|
||||
void ffStrbufPrependC(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1)]] void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value);
|
||||
[[gnu::nonnull(1)]] void ffStrbufPrependC(FFstrbuf* strbuf, char c);
|
||||
|
||||
void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c);
|
||||
[[gnu::nonnull(1)]] void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c);
|
||||
|
||||
// Clear the content of strbuf and set new value
|
||||
// NOTE: Unlike ffStrbufAppend*, ffStrbufSet* functions may NOT reserve extra space
|
||||
void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value);
|
||||
void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value);
|
||||
[[gnu::format(printf, 2, 3)]] void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...);
|
||||
[[gnu::nonnull(1, 2)]] void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value);
|
||||
[[gnu::nonnull(1, 3)]] void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value);
|
||||
[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...);
|
||||
|
||||
void ffStrbufTrimLeft(FFstrbuf* strbuf, char c);
|
||||
void ffStrbufTrimRight(FFstrbuf* strbuf, char c);
|
||||
void ffStrbufTrimLeftSpace(FFstrbuf* strbuf);
|
||||
void ffStrbufTrimRightSpace(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] void ffStrbufTrimLeft(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1)]] void ffStrbufTrimRight(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1)]] void ffStrbufTrimLeftSpace(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] void ffStrbufTrimRightSpace(FFstrbuf* strbuf);
|
||||
|
||||
bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex);
|
||||
void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str);
|
||||
void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex);
|
||||
[[gnu::nonnull(1, 2)]] void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str);
|
||||
// `strings` is only dereferenced when `numStrings > 0`, so it is intentionally not `nonnull(3)`
|
||||
[[gnu::nonnull(1)]] void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]);
|
||||
|
||||
void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace);
|
||||
[[gnu::nonnull(1)]] void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace);
|
||||
|
||||
// Returns true if the strbuf is modified
|
||||
bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index);
|
||||
bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index); // Not including the index
|
||||
bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c);
|
||||
bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str);
|
||||
bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c);
|
||||
bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index); // Not including the index
|
||||
[[gnu::nonnull(1)]] bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1, 2)]] bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end);
|
||||
|
||||
[[nodiscard]] uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c);
|
||||
|
||||
bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end);
|
||||
[[gnu::nonnull(1, 2)]] bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end);
|
||||
|
||||
bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c);
|
||||
|
||||
void ffStrbufUpperCase(FFstrbuf* strbuf);
|
||||
void ffStrbufLowerCase(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] void ffStrbufUpperCase(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] void ffStrbufLowerCase(FFstrbuf* strbuf);
|
||||
|
||||
// Function alters the buffer to extract lines or delimited segments (replaces the delimiter with '\0')
|
||||
// so that buffer MUST be heap allocated (NOT a static string)
|
||||
// `lineptr` must be `nullptr` and `n` MUST be `0` for the first call
|
||||
// Caller MUST NOT free `*lineptr`
|
||||
bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
|
||||
void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
|
||||
[[gnu::nonnull(1, 2, 4)]] bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
|
||||
[[gnu::nonnull(1, 2, 4)]] void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
|
||||
|
||||
/**
|
||||
* @brief Read a line from a FFstrbuf.
|
||||
@@ -110,29 +136,31 @@ void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf
|
||||
*
|
||||
* @return true if a line has been read, false if the end of the buffer has been reached.
|
||||
*/
|
||||
static inline bool ffStrbufGetline(char** lineptr, size_t* n, FFstrbuf* buffer) {
|
||||
[[gnu::nonnull(1, 2, 3), nodiscard]] static inline bool ffStrbufGetline(char** lineptr, size_t* n, FFstrbuf* buffer) {
|
||||
return ffStrbufGetdelim(lineptr, n, '\n', buffer);
|
||||
}
|
||||
/**
|
||||
* @brief Restore the end of a line that was modified by ffStrbufGetline.
|
||||
* @warning This function should be called before breaking an ffStrbufGetline loop if `buffer` will be used later.
|
||||
*/
|
||||
static inline void ffStrbufGetlineRestore(char** lineptr, size_t* n, FFstrbuf* buffer) {
|
||||
[[gnu::nonnull(1, 2, 3)]] static inline void ffStrbufGetlineRestore(char** lineptr, size_t* n, FFstrbuf* buffer) {
|
||||
ffStrbufGetdelimRestore(lineptr, n, '\n', buffer);
|
||||
}
|
||||
bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf);
|
||||
bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
[[gnu::nonnull(1)]] bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf);
|
||||
// `comp` is only dereferenced when `compLength > 0` in the first pair, and is never dereferenced
|
||||
// when `strbuf` is empty in the second pair, so neither takes `nonnull(3)`
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
|
||||
|
||||
int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint);
|
||||
[[gnu::nonnull(1)]] int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint);
|
||||
|
||||
void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value);
|
||||
void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value);
|
||||
[[gnu::nonnull(1)]] void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value);
|
||||
[[gnu::nonnull(1)]] void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value);
|
||||
// Appends a double value to the string buffer with the specified precision (0~15).
|
||||
// if `precision < 0`, let yyjson decide the precision
|
||||
void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros);
|
||||
[[gnu::nonnull(1)]] void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros);
|
||||
|
||||
[[nodiscard]] static inline FFstrbuf ffStrbufCreateA(uint32_t allocate) {
|
||||
FFstrbuf strbuf;
|
||||
@@ -140,7 +168,7 @@ void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
static inline void ffStrbufInitCopy(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict src) {
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffStrbufInitCopy(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict src) {
|
||||
if (src->allocated == 0) { // static string
|
||||
*strbuf = *src;
|
||||
} else {
|
||||
@@ -149,14 +177,14 @@ static inline void ffStrbufInitCopy(FFstrbuf* __restrict strbuf, const FFstrbuf*
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline FFstrbuf ffStrbufCreateCopy(const FFstrbuf* src) {
|
||||
[[gnu::nonnull(1), nodiscard]] static inline FFstrbuf ffStrbufCreateCopy(const FFstrbuf* src) {
|
||||
FFstrbuf strbuf;
|
||||
ffStrbufInitCopy(&strbuf, src);
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
// Move the content of `src` into `strbuf`, and left `src` empty
|
||||
static inline void ffStrbufInitMove(FFstrbuf* strbuf, FFstrbuf* src) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufInitMove(FFstrbuf* strbuf, FFstrbuf* src) {
|
||||
if (src) {
|
||||
*strbuf = *src;
|
||||
ffStrbufInit(src);
|
||||
@@ -171,12 +199,12 @@ static inline void ffStrbufInitMove(FFstrbuf* strbuf, FFstrbuf* src) {
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
static inline void ffStrbufInitMoveS(FFstrbuf* strbuf, char* heapStr) {
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffStrbufInitMoveS(FFstrbuf* strbuf, char* heapStr) {
|
||||
ffStrbufInitMoveNS(strbuf, (uint32_t) strlen(heapStr), heapStr);
|
||||
}
|
||||
|
||||
// Despite the name, this function resets strbuf to the initial/unallocated state
|
||||
static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
|
||||
if (strbuf->allocated > 0) {
|
||||
free(strbuf->chars);
|
||||
}
|
||||
@@ -184,8 +212,7 @@ static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
|
||||
ffStrbufInit(strbuf);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufGetFree(const FFstrbuf* strbuf) {
|
||||
assert(strbuf != nullptr);
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufGetFree(const FFstrbuf* strbuf) {
|
||||
if (strbuf->allocated == 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -193,7 +220,7 @@ static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
|
||||
return strbuf->allocated - strbuf->length - 1; // - 1 for the null byte
|
||||
}
|
||||
|
||||
static inline void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
|
||||
if (__builtin_expect(free == 0, false)) {
|
||||
if (__builtin_expect(!(strbuf->allocated == 0 && strbuf->length > 0), true)) {
|
||||
return;
|
||||
@@ -208,8 +235,7 @@ static inline void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
|
||||
}
|
||||
|
||||
|
||||
static inline void ffStrbufClear(FFstrbuf* strbuf) {
|
||||
assert(strbuf != nullptr);
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufClear(FFstrbuf* strbuf) {
|
||||
extern char* CHAR_NULL_PTR;
|
||||
|
||||
if (strbuf->allocated == 0) {
|
||||
@@ -221,13 +247,13 @@ static inline void ffStrbufClear(FFstrbuf* strbuf) {
|
||||
strbuf->length = 0;
|
||||
}
|
||||
|
||||
static inline void ffStrbufAppendC(FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppendC(FFstrbuf* strbuf, char c) {
|
||||
ffStrbufEnsureFree(strbuf, 1);
|
||||
strbuf->chars[strbuf->length++] = c;
|
||||
strbuf->chars[strbuf->length] = '\0';
|
||||
}
|
||||
|
||||
static inline void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
|
||||
if (__builtin_expect(num == 0, false)) {
|
||||
return;
|
||||
}
|
||||
@@ -238,7 +264,7 @@ static inline void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
|
||||
strbuf->chars[strbuf->length] = '\0';
|
||||
}
|
||||
|
||||
static inline void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
|
||||
if (__builtin_expect(value == nullptr || length == 0, false)) {
|
||||
return;
|
||||
}
|
||||
@@ -249,7 +275,7 @@ static inline void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const cha
|
||||
strbuf->chars[strbuf->length] = '\0';
|
||||
}
|
||||
|
||||
static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value) {
|
||||
assert(value != strbuf);
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
@@ -257,13 +283,12 @@ static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* _
|
||||
ffStrbufAppendNS(strbuf, value->length, value->chars);
|
||||
}
|
||||
|
||||
static inline void ffStrbufRecalculateLength(FFstrbuf* strbuf) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufRecalculateLength(FFstrbuf* strbuf) {
|
||||
strbuf->length = (uint32_t) strlen(strbuf->chars);
|
||||
}
|
||||
|
||||
static inline void ffStrbufSetS(FFstrbuf* strbuf, const char* value) {
|
||||
assert(strbuf != nullptr);
|
||||
|
||||
// `value` may be null (clears the buffer); `strbuf` may not
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufSetS(FFstrbuf* strbuf, const char* value) {
|
||||
if (value == nullptr) {
|
||||
ffStrbufClear(strbuf);
|
||||
} else {
|
||||
@@ -271,9 +296,7 @@ static inline void ffStrbufSetS(FFstrbuf* strbuf, const char* value) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSetJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
assert(strbuf != nullptr);
|
||||
|
||||
[[gnu::nonnull(1)]] static inline bool ffStrbufSetJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
if (yyjson_is_str(jsonVal)) {
|
||||
ffStrbufSetNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
|
||||
return true;
|
||||
@@ -283,13 +306,15 @@ static inline bool ffStrbufSetJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline void ffStrbufAppendS(FFstrbuf* strbuf, const char* value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufAppendS(FFstrbuf* strbuf, const char* value) {
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
ffStrbufAppendNS(strbuf, (uint32_t) strlen(value), value);
|
||||
}
|
||||
|
||||
// Returns whether `jsonVal` was a string. Callers routinely pre-check with `yyjson_is_str`, so the
|
||||
// result is not `nodiscard`.
|
||||
static inline bool ffStrbufAppendJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
if (yyjson_is_str(jsonVal)) {
|
||||
ffStrbufAppendNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
|
||||
@@ -298,7 +323,7 @@ static inline bool ffStrbufAppendJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal)
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline void ffStrbufInit(FFstrbuf* strbuf) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufInit(FFstrbuf* strbuf) {
|
||||
extern char* CHAR_NULL_PTR;
|
||||
strbuf->allocated = strbuf->length = 0;
|
||||
strbuf->chars = CHAR_NULL_PTR;
|
||||
@@ -327,7 +352,7 @@ static inline void ffStrbufInitStatic(FFstrbuf* strbuf, const char* str) {
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
static inline void ffStrbufSetStatic(FFstrbuf* strbuf, const char* value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufSetStatic(FFstrbuf* strbuf, const char* value) {
|
||||
if (strbuf->allocated > 0) {
|
||||
free(strbuf->chars);
|
||||
}
|
||||
@@ -339,7 +364,7 @@ static inline void ffStrbufSetStatic(FFstrbuf* strbuf, const char* value) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline void ffStrbufInitNS(FFstrbuf* strbuf, uint32_t length, const char* str) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufInitNS(FFstrbuf* strbuf, uint32_t length, const char* str) {
|
||||
ffStrbufInit(strbuf);
|
||||
ffStrbufAppendNS(strbuf, length, str);
|
||||
}
|
||||
@@ -350,12 +375,13 @@ static inline void ffStrbufInitNS(FFstrbuf* strbuf, uint32_t length, const char*
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
// Returns whether `jsonVal` was a string; not `nodiscard`, same reason as ffStrbufAppendJsonVal
|
||||
static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
ffStrbufInit(strbuf);
|
||||
return ffStrbufAppendJsonVal(strbuf, jsonVal);
|
||||
}
|
||||
|
||||
static inline void ffStrbufInitS(FFstrbuf* strbuf, const char* str) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufInitS(FFstrbuf* strbuf, const char* str) {
|
||||
ffStrbufInit(strbuf);
|
||||
ffStrbufAppendS(strbuf, str);
|
||||
}
|
||||
@@ -366,107 +392,107 @@ static inline void ffStrbufInitS(FFstrbuf* strbuf, const char* str) {
|
||||
return strbuf;
|
||||
}
|
||||
|
||||
static inline void ffStrbufPrepend(FFstrbuf* strbuf, FFstrbuf* value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufPrepend(FFstrbuf* strbuf, FFstrbuf* value) {
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
ffStrbufPrependNS(strbuf, value->length, value->chars);
|
||||
}
|
||||
|
||||
static inline void ffStrbufPrependS(FFstrbuf* strbuf, const char* value) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufPrependS(FFstrbuf* strbuf, const char* value) {
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
ffStrbufPrependNS(strbuf, (uint32_t) strlen(value), value);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline int ffStrbufComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
uint32_t length = strbuf->length > comp->length ? comp->length : strbuf->length;
|
||||
return memcmp(strbuf->chars, comp->chars, length + 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
return ffStrbufComp(strbuf, comp) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline int ffStrbufCompS(const FFstrbuf* strbuf, const char* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufCompS(const FFstrbuf* strbuf, const char* comp) {
|
||||
return strcmp(strbuf->chars, comp);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEqualS(const FFstrbuf* strbuf, const char* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEqualS(const FFstrbuf* strbuf, const char* comp) {
|
||||
return ffStrbufCompS(strbuf, comp) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline int ffStrbufIgnCaseCompS(const FFstrbuf* strbuf, const char* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufIgnCaseCompS(const FFstrbuf* strbuf, const char* comp) {
|
||||
return strcasecmp(strbuf->chars, comp);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufIgnCaseEqualS(const FFstrbuf* strbuf, const char* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufIgnCaseEqualS(const FFstrbuf* strbuf, const char* comp) {
|
||||
return ffStrbufIgnCaseCompS(strbuf, comp) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline int ffStrbufIgnCaseComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufIgnCaseComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
return ffStrbufIgnCaseCompS(strbuf, comp->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufIgnCaseEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufIgnCaseEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
|
||||
return ffStrbufIgnCaseComp(strbuf, comp) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufContainC(const FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainC(const FFstrbuf* strbuf, char c) {
|
||||
return memchr(strbuf->chars, c, strbuf->length) != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufContainS(const FFstrbuf* strbuf, const char* str) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainS(const FFstrbuf* strbuf, const char* str) {
|
||||
return strstr(strbuf->chars, str) != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufContain(const FFstrbuf* strbuf, const FFstrbuf* str) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContain(const FFstrbuf* strbuf, const FFstrbuf* str) {
|
||||
return ffStrbufContainS(strbuf, str->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufContainIgnCaseS(const FFstrbuf* strbuf, const char* str) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainIgnCaseS(const FFstrbuf* strbuf, const char* str) {
|
||||
return strcasestr(strbuf->chars, str) != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* str) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* str) {
|
||||
return ffStrbufContainIgnCaseS(strbuf, str->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufNextIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
|
||||
assert(start <= strbuf->length);
|
||||
|
||||
const char* ptr = (const char*) memchr(strbuf->chars + start, c, strbuf->length - start);
|
||||
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
|
||||
[[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);
|
||||
|
||||
const char* ptr = strstr(strbuf->chars + start, str);
|
||||
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufPreviousIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufPreviousIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
|
||||
assert(start <= strbuf->length);
|
||||
|
||||
const char* ptr = (const char*) memrchr(strbuf->chars, c, start + 1);
|
||||
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufFirstIndexC(const FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndexC(const FFstrbuf* strbuf, char c) {
|
||||
return ffStrbufNextIndexC(strbuf, 0, c);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufFirstIndex(const FFstrbuf* strbuf, const FFstrbuf* searched) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndex(const FFstrbuf* strbuf, const FFstrbuf* searched) {
|
||||
return ffStrbufNextIndexS(strbuf, 0, searched->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufFirstIndexS(const FFstrbuf* strbuf, const char* str) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndexS(const FFstrbuf* strbuf, const char* str) {
|
||||
return ffStrbufNextIndexS(strbuf, 0, str);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint32_t ffStrbufLastIndexC(const FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufLastIndexC(const FFstrbuf* strbuf, char c) {
|
||||
if (strbuf->length == 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -474,19 +500,19 @@ static inline void ffStrbufPrependS(FFstrbuf* strbuf, const char* value) {
|
||||
return ffStrbufPreviousIndexC(strbuf, strbuf->length - 1, c);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSubstrBeforeFirstC(FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1)]] static inline bool ffStrbufSubstrBeforeFirstC(FFstrbuf* strbuf, char c) {
|
||||
return ffStrbufSubstrBefore(strbuf, ffStrbufFirstIndexC(strbuf, c));
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1)]] static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
|
||||
return ffStrbufSubstrBefore(strbuf, ffStrbufLastIndexC(strbuf, c));
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithC(const FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithC(const FFstrbuf* strbuf, char c) {
|
||||
return strbuf->chars[0] == c;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithSN(const FFstrbuf* strbuf, const char* start, uint32_t length) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithSN(const FFstrbuf* strbuf, const char* start, uint32_t length) {
|
||||
if (length > strbuf->length) {
|
||||
return false;
|
||||
}
|
||||
@@ -494,34 +520,34 @@ static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
|
||||
return memcmp(strbuf->chars, start, length) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithS(const FFstrbuf* strbuf, const char* start) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithS(const FFstrbuf* strbuf, const char* start) {
|
||||
return ffStrbufStartsWithSN(strbuf, start, (uint32_t) strlen(start));
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWith(const FFstrbuf* strbuf, const FFstrbuf* start) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWith(const FFstrbuf* strbuf, const FFstrbuf* start) {
|
||||
return ffStrbufStartsWithSN(strbuf, start->chars, start->length);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t length, const char* start) {
|
||||
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t length, const char* start) {
|
||||
if (length > strbuf->length) {
|
||||
return false;
|
||||
}
|
||||
return strncasecmp(strbuf->chars, start, length) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithIgnCaseS(const FFstrbuf* strbuf, const char* start) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCaseS(const FFstrbuf* strbuf, const char* start) {
|
||||
return ffStrbufStartsWithIgnCaseNS(strbuf, (uint32_t) strlen(start), start);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufStartsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* start) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* start) {
|
||||
return ffStrbufStartsWithIgnCaseNS(strbuf, start->length, start->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithC(const FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithC(const FFstrbuf* strbuf, char c) {
|
||||
return strbuf->length == 0 ? false : strbuf->chars[strbuf->length - 1] == c;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
|
||||
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
|
||||
if (endLength > strbuf->length) {
|
||||
return false;
|
||||
}
|
||||
@@ -529,104 +555,108 @@ static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
|
||||
return memcmp(strbuf->chars + strbuf->length - endLength, end, endLength) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithS(const FFstrbuf* strbuf, const char* end) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithS(const FFstrbuf* strbuf, const char* end) {
|
||||
return ffStrbufEndsWithNS(strbuf, (uint32_t) strlen(end), end);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithFn(const FFstrbuf* strbuf, int (*const fn)(int)) {
|
||||
// Not `pure`: the caller-supplied `fn` may have side effects
|
||||
[[gnu::nonnull(1, 2), nodiscard]] static inline bool ffStrbufEndsWithFn(const FFstrbuf* strbuf, int (*const fn)(int)) {
|
||||
return strbuf->length == 0 ? false : fn(strbuf->chars[strbuf->length - 1]);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWith(const FFstrbuf* strbuf, const FFstrbuf* end) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWith(const FFstrbuf* strbuf, const FFstrbuf* end) {
|
||||
return ffStrbufEndsWithNS(strbuf, end->length, end->chars);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
|
||||
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
|
||||
if (endLength > strbuf->length) {
|
||||
return false;
|
||||
}
|
||||
return strcasecmp(strbuf->chars + strbuf->length - endLength, end) == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithIgnCaseS(const FFstrbuf* strbuf, const char* end) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCaseS(const FFstrbuf* strbuf, const char* end) {
|
||||
return ffStrbufEndsWithIgnCaseNS(strbuf, (uint32_t) strlen(end), end);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline bool ffStrbufEndsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* end) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* end) {
|
||||
return ffStrbufEndsWithIgnCaseNS(strbuf, end->length, end->chars);
|
||||
}
|
||||
|
||||
static inline void ffStrbufTrim(FFstrbuf* strbuf, char c) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufTrim(FFstrbuf* strbuf, char c) {
|
||||
ffStrbufTrimRight(strbuf, c);
|
||||
ffStrbufTrimLeft(strbuf, c);
|
||||
}
|
||||
|
||||
static inline void ffStrbufTrimSpace(FFstrbuf* strbuf) {
|
||||
[[gnu::nonnull(1)]] static inline void ffStrbufTrimSpace(FFstrbuf* strbuf) {
|
||||
ffStrbufTrimRightSpace(strbuf);
|
||||
ffStrbufTrimLeftSpace(strbuf);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufMatchSeparatedS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
return ffStrbufMatchSeparatedNS(strbuf, (uint32_t) strlen(comp), comp, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufMatchSeparated(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparated(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
return ffStrbufMatchSeparatedNS(strbuf, comp->length, comp->chars, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufMatchSeparatedIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
return ffStrbufMatchSeparatedIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufMatchSeparatedIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
return ffStrbufMatchSeparatedIgnCaseNS(strbuf, comp->length, comp->chars, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSeparatedContainS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
return ffStrbufSeparatedContainNS(strbuf, (uint32_t) strlen(comp), comp, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSeparatedContain(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContain(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
return ffStrbufSeparatedContainNS(strbuf, comp->length, comp->chars, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSeparatedContainIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
|
||||
return ffStrbufSeparatedContainIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
|
||||
}
|
||||
|
||||
static inline bool ffStrbufSeparatedContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
|
||||
return ffStrbufSeparatedContainIgnCaseNS(strbuf, comp->length, comp->chars, separator);
|
||||
}
|
||||
|
||||
static inline void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) {
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) {
|
||||
fwrite(strbuf->chars, sizeof(*strbuf->chars), strbuf->length, file);
|
||||
}
|
||||
|
||||
static inline void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) {
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) {
|
||||
ffStrbufWriteTo(strbuf, file);
|
||||
fputc('\n', file);
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) {
|
||||
// Not `pure`: `strtod` reads the LC_NUMERIC locale and writes errno
|
||||
[[gnu::nonnull(1), nodiscard]] static inline double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) {
|
||||
char* str_end;
|
||||
double result = strtod(strbuf->chars, &str_end);
|
||||
return str_end == strbuf->chars ? defaultValue : result;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) {
|
||||
// Not `pure`: `strtoull` reads the LC_NUMERIC locale and writes errno
|
||||
[[gnu::nonnull(1), nodiscard]] static inline uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) {
|
||||
char* str_end;
|
||||
unsigned long long result = strtoull(strbuf->chars, &str_end, 10);
|
||||
return str_end == strbuf->chars ? defaultValue : (uint64_t) result;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) {
|
||||
// Not `pure`: `strtoll` reads the LC_NUMERIC locale and writes errno
|
||||
[[gnu::nonnull(1), nodiscard]] static inline int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) {
|
||||
char* str_end;
|
||||
long long result = strtoll(strbuf->chars, &str_end, 10);
|
||||
return str_end == strbuf->chars ? defaultValue : (int64_t) result;
|
||||
}
|
||||
|
||||
// Returns true if the strbuf is modified
|
||||
bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf);
|
||||
[[gnu::nonnull(1)]] [[gnu::nonnull(1)]] bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf);
|
||||
|
||||
#define FF_STRBUF_AUTO_DESTROY [[gnu::cleanup(ffStrbufDestroy)]] FFstrbuf
|
||||
#define FF_STRBUF_STATIC(str) { .allocated = 0, .length = (uint32_t) sizeof(str) - 1, .chars = str }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The NDK marks an API unavailable whenever it is newer than the API level the build targets, and
|
||||
// fastfetch keeps running on devices below the levels it uses: AImageDecoder is 30, decoding past
|
||||
// the first frame is 31, and AMediaCodec_getName is 28. CMake compiles the files that reach for
|
||||
// them with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability, which turns
|
||||
// those entry points into weak references and makes every unguarded use an error. Two things follow:
|
||||
//
|
||||
// * A weak reference resolves to null on an older device, so every use needs a run-time check --
|
||||
// that is FF_ANDROID_API_AT_LEAST. Unguarded, the call would jump to a null pointer.
|
||||
// * Annotating a helper with FF_ANDROID_REQUIRES_API keeps the check in one place: the compiler rejects
|
||||
// any call of that helper which is not itself inside a guard.
|
||||
//
|
||||
// https://developer.android.com/ndk/guides/using-newer-apis
|
||||
#define FF_ANDROID_REQUIRES_API(x) [[clang::availability(android, introduced = x)]]
|
||||
#define FF_ANDROID_API_AT_LEAST(x) __builtin_available(android x, *)
|
||||
|
||||
// The UIDs that hold android.permission.DUMP, which is what `dumpsys` checks before it prints
|
||||
// anything: every other UID is refused on stdout and the command still exits 0, which a caller cannot
|
||||
// tell apart from a command that printed nothing useful. Forking a `dumpsys` from anywhere else is
|
||||
// therefore pure waste, and for some fields `dumpsys` is the only route there is.
|
||||
//
|
||||
// The refusal is not always the permission, and the text differs per service and per release. On
|
||||
// an Android 16 device an app UID gets `Permission Denial: can't dump DisplayManagerService ... due to
|
||||
// missing android.permission.DUMP permission` for `display`, but `Can't find service: battery` for
|
||||
// `battery` -- that service is not in the list `dumpsys -l` shows an app at all, while `display` is.
|
||||
// The UID is what decides, so one check covers both.
|
||||
//
|
||||
// android.os.Process.ROOT_UID and SHELL_UID. The shell UID has a second use: it owns exactly one
|
||||
// package name, which the system services accept, so a process that cannot derive its own package
|
||||
// can answer with that one instead.
|
||||
typedef enum FFAndroidPrivilegedUid : uint32_t {
|
||||
FF_ANDROID_PRIVILEGED_UID_ROOT = 0,
|
||||
FF_ANDROID_PRIVILEGED_UID_SHELL = 2000,
|
||||
} FFAndroidPrivilegedUid;
|
||||
|
||||
// Whether the given UID is allowed to run `dumpsys`. The UID is passed in rather than read here, so
|
||||
// that the caller can take it from `instance.state.platform.uid` -- which is what makes the
|
||||
// privileged path reachable from a test that does not run as root or as the shell.
|
||||
static inline bool ffAndroidIsRootOrShell(uint32_t uid) {
|
||||
return uid == FF_ANDROID_PRIVILEGED_UID_ROOT || uid == FF_ANDROID_PRIVILEGED_UID_SHELL;
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
#include "common/android/binder.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
const char* ffBinderOpen(FFBinder* binder) {
|
||||
*binder = (FFBinder) { .fd = -1, .shared = nullptr, .sharedSize = 0, .protocolVersion = 0 };
|
||||
|
||||
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 };
|
||||
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";
|
||||
}
|
||||
|
||||
// The region is read-only: the kernel writes reply payloads into it and hands out their
|
||||
// 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) {
|
||||
FF_DEBUG("mmap(" FF_BINDER_DEVICE ") failed: %s", strerror(errno));
|
||||
return "Failed to mmap " FF_BINDER_DEVICE;
|
||||
}
|
||||
|
||||
*binder = (FFBinder) {
|
||||
.fd = fd,
|
||||
.shared = (uint8_t*) shared,
|
||||
.sharedSize = FF_BINDER_SHARED_SIZE,
|
||||
.protocolVersion = version.protocol_version,
|
||||
};
|
||||
fd = -1; // ownership moved into *binder, which ffBinderClose() lets go
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ffBinderClose(FFBinder* binder) {
|
||||
if (binder->shared != nullptr) {
|
||||
munmap(binder->shared, binder->sharedSize);
|
||||
}
|
||||
if (binder->fd >= 0) {
|
||||
close(binder->fd);
|
||||
}
|
||||
binder->fd = -1;
|
||||
binder->shared = nullptr;
|
||||
binder->sharedSize = 0;
|
||||
}
|
||||
|
||||
const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply) {
|
||||
reply->size = 0;
|
||||
reply->code = 0;
|
||||
reply->flags = 0;
|
||||
reply->handleCount = 0;
|
||||
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";
|
||||
}
|
||||
|
||||
uint8_t writeBuffer[sizeof(uint32_t) + sizeof(struct binder_transaction_data)];
|
||||
const struct binder_transaction_data transaction = {
|
||||
.target = { .handle = handle },
|
||||
.cookie = 0,
|
||||
.code = code,
|
||||
.flags = flags,
|
||||
.sender_pid = 0,
|
||||
.sender_euid = 0,
|
||||
.data_size = parcel->size,
|
||||
.offsets_size = 0,
|
||||
.data = { .ptr = { .buffer = (binder_uintptr_t) (uintptr_t) parcel->data, .offsets = 0 } },
|
||||
};
|
||||
const uint32_t transactionCommand = BC_TRANSACTION;
|
||||
memcpy(writeBuffer, &transactionCommand, sizeof(uint32_t));
|
||||
memcpy(writeBuffer + sizeof(uint32_t), &transaction, sizeof(transaction));
|
||||
|
||||
// The read buffer only has to hold the command words and the binder_transaction_data that
|
||||
// follows BR_REPLY; the payload itself lives in the mmap region.
|
||||
uint8_t readBuffer[FF_BINDER_REPLY_BUFFER_SIZE];
|
||||
struct binder_write_read exchange = {
|
||||
.write_size = sizeof(writeBuffer),
|
||||
.write_consumed = 0,
|
||||
.write_buffer = (binder_uintptr_t) (uintptr_t) writeBuffer,
|
||||
.read_size = sizeof(readBuffer),
|
||||
.read_consumed = 0,
|
||||
.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";
|
||||
}
|
||||
|
||||
const uint8_t* cursor = readBuffer;
|
||||
const uint8_t* end = readBuffer + (size_t) exchange.read_consumed;
|
||||
while ((size_t) (end - cursor) >= sizeof(uint32_t)) {
|
||||
uint32_t command = 0;
|
||||
memcpy(&command, cursor, sizeof(uint32_t));
|
||||
cursor += sizeof(uint32_t);
|
||||
|
||||
switch (command) {
|
||||
case BR_NOOP:
|
||||
case BR_TRANSACTION_COMPLETE:
|
||||
case BR_SPAWN_LOOPER:
|
||||
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:
|
||||
case BR_ACQUIRE:
|
||||
case BR_RELEASE:
|
||||
case BR_DECREFS:
|
||||
cursor += sizeof(struct binder_ptr_cookie);
|
||||
break;
|
||||
case BR_DEAD_BINDER:
|
||||
case BR_CLEAR_DEATH_NOTIFICATION_DONE:
|
||||
cursor += sizeof(binder_uintptr_t);
|
||||
break;
|
||||
case BR_ERROR:
|
||||
cursor += sizeof(int32_t);
|
||||
break;
|
||||
|
||||
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;
|
||||
memcpy(&data, cursor, sizeof(data));
|
||||
cursor += sizeof(data);
|
||||
|
||||
reply->code = data.code;
|
||||
reply->flags = data.flags;
|
||||
|
||||
const uint8_t* payload = (const uint8_t*) (uintptr_t) data.data.ptr.buffer;
|
||||
const size_t payloadSize = (size_t) data.data_size;
|
||||
const uint8_t* offsets = (const uint8_t*) (uintptr_t) data.data.ptr.offsets;
|
||||
const size_t offsetsSize = (size_t) data.offsets_size;
|
||||
|
||||
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);
|
||||
reply->size = payloadSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the offset table for the flat objects the service sent us. A reply may carry
|
||||
// both kinds at once and they are not interchangeable: a handle is a reference that
|
||||
// has to be acquired (see below) to outlive the reply buffer, while a file
|
||||
// descriptor was installed in our own fd table by the kernel and is simply ours.
|
||||
for (size_t offset = 0; offset + sizeof(binder_size_t) <= offsetsSize; offset += sizeof(binder_size_t)) {
|
||||
binder_size_t objectOffset = 0;
|
||||
memcpy(&objectOffset, offsets + offset, sizeof(objectOffset));
|
||||
if (objectOffset + sizeof(struct flat_binder_object) > payloadSize) {
|
||||
continue;
|
||||
}
|
||||
struct flat_binder_object object;
|
||||
memcpy(&object, payload + objectOffset, sizeof(object));
|
||||
if (object.hdr.type == BINDER_TYPE_HANDLE) {
|
||||
if (reply->handleCount < FF_BINDER_MAX_HANDLES) {
|
||||
reply->handles[reply->handleCount++] = object.handle;
|
||||
}
|
||||
} else if (object.hdr.type == BINDER_TYPE_FD) {
|
||||
if (reply->fdCount < FF_BINDER_MAX_FDS) {
|
||||
reply->fds[reply->fdCount++] = (int32_t) object.handle;
|
||||
} else {
|
||||
// Nothing is going to hold this one and nothing is going to close it.
|
||||
close((int) object.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The kernel gave us a new reference whose only strong reference belongs to the
|
||||
// reply buffer, so it has to be acquired before that buffer is freed -- otherwise
|
||||
// the handle silently degrades to a weak reference and every later transaction on
|
||||
// it fails with BR_FAILED_REPLY. libbinder does the same in acquire_object().
|
||||
uint8_t tail[FF_BINDER_MAX_HANDLES * 4 * sizeof(uint32_t) + sizeof(uint32_t) + sizeof(binder_uintptr_t)];
|
||||
size_t tailSize = 0;
|
||||
for (uint32_t i = 0; i < reply->handleCount; i++) {
|
||||
const uint32_t acquire = BC_ACQUIRE;
|
||||
const uint32_t increfs = BC_INCREFS;
|
||||
memcpy(tail + tailSize, &acquire, sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &reply->handles[i], sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &increfs, sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &reply->handles[i], sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
}
|
||||
const uint32_t freeCommand = BC_FREE_BUFFER;
|
||||
const binder_uintptr_t freeBuffer = data.data.ptr.buffer;
|
||||
memcpy(tail + tailSize, &freeCommand, sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &freeBuffer, sizeof(freeBuffer));
|
||||
tailSize += sizeof(freeBuffer);
|
||||
|
||||
struct binder_write_read release = {
|
||||
.write_size = tailSize,
|
||||
.write_consumed = 0,
|
||||
.write_buffer = (binder_uintptr_t) (uintptr_t) tail,
|
||||
.read_size = 0,
|
||||
.read_consumed = 0,
|
||||
.read_buffer = 0,
|
||||
};
|
||||
// This one ioctl carries the whole tail, so its failure is not cosmetic: without
|
||||
// BC_ACQUIRE the handles handed to the caller are only weakly referenced and every
|
||||
// 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";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle) {
|
||||
uint8_t parcelBuffer[256];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_BINDER_SM_DESCRIPTOR);
|
||||
ffBinderParcelPutString16(&parcel, name);
|
||||
|
||||
uint8_t replyBuffer[256];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
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";
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
*handle = reply.handles[0];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ffBinderServiceHandleRelease(FFBinderServiceHandle* service) {
|
||||
if (service->binder == nullptr || service->handle == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The mirror of the acquire at the end of ffBinderTransact(): one strong and one weak reference
|
||||
// each, dropped in the same order they were taken. The kernel keeps the service node alive until
|
||||
// the last one goes, so this is what stops a short-lived fastfetch from holding one for the whole
|
||||
// of its life.
|
||||
const uint32_t commands[] = { BC_RELEASE, BC_DECREFS };
|
||||
uint8_t tail[2 * 2 * sizeof(uint32_t)];
|
||||
size_t tailSize = 0;
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
memcpy(tail + tailSize, &commands[i], sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &service->handle, sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
}
|
||||
|
||||
struct binder_write_read release = {
|
||||
.write_size = tailSize,
|
||||
.write_consumed = 0,
|
||||
.write_buffer = (binder_uintptr_t) (uintptr_t) tail,
|
||||
.read_size = 0,
|
||||
.read_consumed = 0,
|
||||
.read_buffer = 0,
|
||||
};
|
||||
// Not reported: this runs from a cleanup attribute, where there is no caller left to report to,
|
||||
// and the only way for it to fail is a binder that is already dead -- which every earlier call on
|
||||
// the same binder would have said already.
|
||||
ioctl(service->binder->fd, BINDER_WRITE_READ, &release);
|
||||
service->handle = 0;
|
||||
}
|
||||
|
||||
void ffBinderAcquiredRelease(FFBinderAcquired* acquired) {
|
||||
if (acquired->replied == nullptr || *acquired->replied == false) {
|
||||
return; // nothing was acquired
|
||||
}
|
||||
|
||||
// A descriptor is not a reference: the kernel installed it in our own fd table when it delivered
|
||||
// the reply, and nobody else holds it, so closing it is the whole of giving it back.
|
||||
for (uint32_t i = 0; i < acquired->fdCount; i++) {
|
||||
close(acquired->fds[i]);
|
||||
}
|
||||
|
||||
// The mirror of the acquire the transaction appended to its tail, in the same order it took them.
|
||||
// No binder to talk through means the handles went with it when it was closed.
|
||||
if (acquired->binder == nullptr || acquired->binder->fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < acquired->handleCount; i++) {
|
||||
FFBinderServiceHandle handle = { .binder = acquired->binder, .handle = acquired->handles[i] };
|
||||
ffBinderServiceHandleRelease(&handle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#pragma once
|
||||
|
||||
// Android exposes no public C API for the system services that carry the data fastfetch wants: the
|
||||
// NDK has no BatteryManager / WifiManager, libbinder_ndk only speaks to AIDL HALs (it refuses to
|
||||
// prepare a transaction for a binder that has no NDK class), and `/system/bin/service call` costs a
|
||||
// fork/exec worth 15-20 ms. What is left is /dev/binder itself, which is all that `service` uses
|
||||
// underneath. This header speaks that protocol directly: open() + mmap() + BINDER_WRITE_READ, no
|
||||
// extra shared library, no child process, ~0.13 ms per call once a handle is cached.
|
||||
//
|
||||
// Three details are easy to get wrong. All three were confirmed against the device's own
|
||||
// libbinder.so and with a ptrace trace of `service`, and getting any of them wrong makes the
|
||||
// service answer BAD_TYPE (0x80000001) or BR_FAILED_REPLY (0x7211):
|
||||
//
|
||||
// * A request parcel must start with three int32 values -- strict mode policy, work source and
|
||||
// 'SYST'. Parcel::markForBinder() writes nothing for kernel binder, but the receiving side's
|
||||
// Parcel::enforceInterface() still compares the third word against its own header.
|
||||
// * Parcel::writeString16() writes the length, the UTF-16 code units, a UTF-16 NUL terminator and
|
||||
// then pads to four bytes. Skipping the terminator shifts every following field by four bytes.
|
||||
// * A handle arriving in a reply is owned by that reply buffer. It has to be acquired
|
||||
// (BC_ACQUIRE + BC_INCREFS) before BC_FREE_BUFFER; otherwise only a weak reference survives and
|
||||
// every later transaction on it fails, because the kernel looks up a strong reference.
|
||||
|
||||
#include "fastfetch.h" // IWYU pragma: keep
|
||||
|
||||
// The structures and command words below are not defined here: bionic generates this header from
|
||||
// bionic/libc/kernel/uapi/linux/android/binder.h and every NDK ships it, so it is the
|
||||
// authoritative copy of the binder ABI -- struct binder_write_read, struct binder_transaction_data,
|
||||
// struct flat_binder_object, BINDER_WRITE_READ / BINDER_VERSION, the BC_* and BR_* command words,
|
||||
// TF_* and BINDER_TYPE_*. Using it beats transcribing it: a second copy can only be wrong, and if
|
||||
// the header itself is wrong that is the NDK's problem to fix, not something fastfetch could have
|
||||
// caught by keeping its own.
|
||||
#include <linux/android/binder.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#define FF_BINDER_DEVICE "/dev/binder"
|
||||
|
||||
// The kernel allocates reply payloads out of the region handed to mmap(); 1 MiB is what libbinder
|
||||
// asks for. Replies themselves are small -- `listServices` on this device is the largest at a few
|
||||
// hundred bytes -- but the region also carries the per-process buffer pool.
|
||||
#define FF_BINDER_SHARED_SIZE (1024 * 1024)
|
||||
|
||||
// A command word plus the binder_transaction_data that follows it. The payload never lands here:
|
||||
// it lives in the mmap region, and BR_REPLY only hands over a pointer to it.
|
||||
#define FF_BINDER_REPLY_BUFFER_SIZE 1024
|
||||
|
||||
// How many handles a reply may transfer to us. Replies that carry more than this still succeed, but
|
||||
// the surplus handles are left unacquired and will fail on first use.
|
||||
#define FF_BINDER_MAX_HANDLES 4
|
||||
|
||||
// How many file descriptors a reply may hand over. A descriptor is not a handle: the kernel installs
|
||||
// it in the caller's own fd table as it delivers the reply, so it is owned rather than borrowed and
|
||||
// the caller closes it. A reply carrying more than this still succeeds, and the surplus is closed
|
||||
// again by ffBinderTransact() because nothing would be left holding it.
|
||||
#define FF_BINDER_MAX_FDS 4
|
||||
|
||||
// The three int32 values Parcel::writeInterfaceToken() puts in front of the descriptor, and that
|
||||
// Parcel::enforceInterface() checks the last of. These come from libbinder rather than from the
|
||||
// kernel, which is why they are spelled out here.
|
||||
#define FF_BINDER_STRICT_MODE_PENALTY_GATHER 0x80000000u // (1 << 31), Parcel.cpp
|
||||
#define FF_BINDER_UNSET_WORK_SOURCE 0xffffffffu // IPCThreadState::kUnsetWorkSource
|
||||
#define FF_BINDER_HEADER 0x53595354u // B_PACK_CHARS('S', 'Y', 'S', 'T')
|
||||
|
||||
typedef struct FFBinder {
|
||||
int fd;
|
||||
uint8_t* shared;
|
||||
size_t sharedSize;
|
||||
int32_t protocolVersion;
|
||||
} FFBinder;
|
||||
|
||||
// Opens /dev/binder, checks the protocol version and maps the shared region. Returns nullptr on
|
||||
// success, a static message otherwise; the device is left closed on failure.
|
||||
[[gnu::nonnull(1), nodiscard]] const char* ffBinderOpen(FFBinder* binder);
|
||||
|
||||
// Usable as a cleanup attribute. Safe on a zeroed or already closed FFBinder.
|
||||
[[gnu::nonnull(1)]] void ffBinderClose(FFBinder* binder);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Parcel writing. The caller owns the buffer; anything that does not fit is dropped and recorded
|
||||
// in `truncated`, which ffBinderTransact() then reports as an error.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct FFBinderParcel {
|
||||
uint8_t* data;
|
||||
size_t capacity;
|
||||
size_t size;
|
||||
bool truncated;
|
||||
} FFBinderParcel;
|
||||
|
||||
[[nodiscard]] static inline FFBinderParcel ffBinderParcelCreate(uint8_t* data, size_t capacity) {
|
||||
return (FFBinderParcel) { .data = data, .capacity = capacity, .size = 0, .truncated = false };
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), nodiscard]] static inline uint8_t* ffBinderParcelReserve(FFBinderParcel* parcel, size_t bytes) {
|
||||
if (bytes > parcel->capacity - parcel->size) {
|
||||
parcel->truncated = true;
|
||||
return nullptr;
|
||||
}
|
||||
uint8_t* result = parcel->data + parcel->size;
|
||||
parcel->size += bytes;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU32(FFBinderParcel* parcel, uint32_t value) {
|
||||
uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint32_t));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, &value, sizeof(uint32_t));
|
||||
}
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1)]] static inline void ffBinderParcelPutI32(FFBinderParcel* parcel, int32_t value) {
|
||||
uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(int32_t));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, &value, sizeof(int32_t));
|
||||
}
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU64(FFBinderParcel* parcel, uint64_t value) {
|
||||
uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint64_t));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, &value, sizeof(uint64_t));
|
||||
}
|
||||
}
|
||||
|
||||
// AIDL string16. The descriptors and service names fastfetch asks for are ASCII, so one byte is one
|
||||
// UTF-16 code unit; anything else would have to be encoded properly here.
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutString16(FFBinderParcel* parcel, const char* value) {
|
||||
const size_t length = strlen(value);
|
||||
const size_t payload = sizeof(uint32_t) + (length + 1) * 2; // length, code units, NUL terminator
|
||||
const size_t padded = (payload + 3) & ~(size_t) 3;
|
||||
uint8_t* dst = ffBinderParcelReserve(parcel, padded);
|
||||
if (dst == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(dst, 0, padded);
|
||||
const uint32_t length32 = (uint32_t) length;
|
||||
memcpy(dst, &length32, sizeof(uint32_t));
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
dst[sizeof(uint32_t) + i * 2] = (uint8_t) value[i];
|
||||
}
|
||||
// The two bytes after the last code unit stay zero: that is the UTF-16 NUL terminator, and
|
||||
// Parcel::readInplace() advances by (length + 1) * sizeof(char16_t) to account for it.
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutInterfaceToken(FFBinderParcel* parcel, const char* descriptor) {
|
||||
ffBinderParcelPutU32(parcel, FF_BINDER_STRICT_MODE_PENALTY_GATHER);
|
||||
ffBinderParcelPutU32(parcel, FF_BINDER_UNSET_WORK_SOURCE);
|
||||
ffBinderParcelPutU32(parcel, FF_BINDER_HEADER);
|
||||
ffBinderParcelPutString16(parcel, descriptor);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Transactions
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct FFBinderReply {
|
||||
uint8_t* data; // caller owned; receives the payload
|
||||
size_t capacity; // ditto
|
||||
size_t size; // payload length in bytes
|
||||
uint32_t code; // transaction code the service echoed back
|
||||
uint32_t flags; // TF_* of the reply
|
||||
uint32_t handleCount;
|
||||
uint32_t handles[FF_BINDER_MAX_HANDLES];
|
||||
uint32_t fdCount;
|
||||
int32_t fds[FF_BINDER_MAX_FDS]; // owned by the caller, which closes them
|
||||
} FFBinderReply;
|
||||
|
||||
[[nodiscard]] static inline FFBinderReply ffBinderReplyCreate(uint8_t* data, size_t capacity) {
|
||||
return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {}, .fdCount = 0, .fds = {} };
|
||||
}
|
||||
|
||||
// A reply carrying TF_STATUS_CODE is not an AIDL parcel at all: the first four bytes are the raw
|
||||
// status_t that onTransact() returned (-74 being UNKNOWN_TRANSACTION, i.e. no such method). Check
|
||||
// this before interpreting anything else.
|
||||
[[gnu::nonnull(1), nodiscard]] static inline bool ffBinderReplyIsStatus(const FFBinderReply* reply) {
|
||||
return (reply->flags & TF_STATUS_CODE) != 0;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), nodiscard]] static inline uint32_t ffBinderReadU32(const uint8_t* data, size_t size, size_t offset) {
|
||||
uint32_t value = 0;
|
||||
if (offset + sizeof(uint32_t) <= size) {
|
||||
memcpy(&value, data + offset, sizeof(uint32_t));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), nodiscard]] static inline int32_t ffBinderReadI32(const uint8_t* data, size_t size, size_t offset) {
|
||||
int32_t value = 0;
|
||||
if (offset + sizeof(int32_t) <= size) {
|
||||
memcpy(&value, data + offset, sizeof(int32_t));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), nodiscard]] static inline uint64_t ffBinderReadU64(const uint8_t* data, size_t size, size_t offset) {
|
||||
uint64_t value = 0;
|
||||
if (offset + sizeof(uint64_t) <= size) {
|
||||
memcpy(&value, data + offset, sizeof(uint64_t));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Synchronous transaction. Returns nullptr on success, a static message otherwise.
|
||||
//
|
||||
// `flags` carries the TF_* values for the request, and one of them is not cosmetic: a reply that
|
||||
// carries a file descriptor is only delivered at all when the request asked with TF_ACCEPT_FDS. A
|
||||
// request that did not gets BR_FAILED_REPLY instead, which reads like a service that is not running
|
||||
// rather than like a missing flag. TF_ONE_WAY is the other one the kernel acts on, and it is what
|
||||
// makes a call fire-and-forget -- there is then no reply to fill `reply` from.
|
||||
//
|
||||
// Handles the reply carries are acquired and added to `reply->handles` before the reply buffer is
|
||||
// freed; descriptors are added to `reply->fds`, which the caller owns and closes.
|
||||
//
|
||||
// `handle` 0 is the service manager; every other handle comes from ffBinderLookupService(). A
|
||||
// service that never replies blocks this call forever -- none of the services fastfetch reads does,
|
||||
// but there is no timeout here to fall back on.
|
||||
[[gnu::nonnull(1, 5, 6), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Service manager
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
#define FF_BINDER_SERVICE_MANAGER_HANDLE 0u
|
||||
#define FF_BINDER_SM_DESCRIPTOR "android.os.IServiceManager"
|
||||
|
||||
// Entry points of android.os.IServiceManager, as this device answers them. The numbers do not match
|
||||
// the AOSP sources: checkService is 4 here, not 3. FF_BINDER_SM_GET_SERVICE returns a nullable
|
||||
// IBinder and is the simplest reply to parse; FF_BINDER_SM_CHECK_SERVICE is what `service` itself
|
||||
// calls and answers with a parcelable instead.
|
||||
#define FF_BINDER_SM_GET_SERVICE 1u
|
||||
#define FF_BINDER_SM_GET_SERVICE2 2u
|
||||
#define FF_BINDER_SM_CHECK_SERVICE 4u
|
||||
#define FF_BINDER_SM_LIST_SERVICES 6u
|
||||
|
||||
// Resolves a service name to a handle through the service manager. Returns nullptr on success.
|
||||
[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle);
|
||||
|
||||
// A handle resolved through the service manager, held together with the binder it came from so that a
|
||||
// single cleanup attribute can give it back on every path out of the function that took it. Zeroed --
|
||||
// or left at handle 0 by a lookup that has not run or did not succeed -- it is a no-op, which is what
|
||||
// makes it safe to declare above the lookup itself.
|
||||
typedef struct FFBinderServiceHandle {
|
||||
FFBinder* binder;
|
||||
uint32_t handle;
|
||||
} FFBinderServiceHandle;
|
||||
|
||||
// Returns the strong and weak references ffBinderLookupService() acquired on the handle. Usable as a
|
||||
// cleanup attribute; safe on a zeroed or already released FFBinderServiceHandle.
|
||||
[[gnu::nonnull(1)]] void ffBinderServiceHandleRelease(FFBinderServiceHandle* service);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Handles a transaction acquired
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Whatever ffBinderTransact() collected for the caller, released together on the way out of a
|
||||
// function -- the handles it acquired, plus the descriptors it delivered, which are owned rather
|
||||
// than borrowed and would otherwise stay open for the life of the process.
|
||||
//
|
||||
// Unlike FFBinderServiceHandle this cannot be declared before the transaction runs, because which
|
||||
// handles came back is only known afterwards. `replied` is therefore a separate flag the caller
|
||||
// raises once ffBinderTransact() has returned: it acquires the handles even when it goes on to fail
|
||||
// (a reply that did not fit the caller buffer still had its BC_ACQUIRE tail sent), so releasing
|
||||
// without it would drop a reference that was never taken.
|
||||
typedef struct FFBinderAcquired {
|
||||
FFBinder* binder;
|
||||
const uint32_t* handles;
|
||||
uint32_t handleCount;
|
||||
const int32_t* fds;
|
||||
uint32_t fdCount;
|
||||
const bool* replied;
|
||||
} FFBinderAcquired;
|
||||
|
||||
// Usable as a cleanup attribute; safe on a zeroed FFBinderAcquired, and on one whose transaction did
|
||||
// not reply.
|
||||
[[gnu::nonnull(1)]] void ffBinderAcquiredRelease(FFBinderAcquired* acquired);
|
||||
@@ -0,0 +1,754 @@
|
||||
#include "common/android/dex.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/mallocHelper.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include "common/library.h"
|
||||
#include <zlib.h>
|
||||
#endif
|
||||
|
||||
// Offsets into the dex header, from https://source.android.com/docs/core/runtime/dex-format.
|
||||
#define FF_DEX_ENDIAN_TAG 0x12345678u
|
||||
#define FF_DEX_HEADER_SIZE 0x70
|
||||
#define FF_DEX_OFF_FILE_SIZE 0x20
|
||||
#define FF_DEX_OFF_ENDIAN_TAG 0x28
|
||||
#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 0x54
|
||||
#define FF_DEX_OFF_CLASS_DEFS_SIZE 0x60
|
||||
#define FF_DEX_OFF_CLASS_DEFS 0x64
|
||||
|
||||
// class_def_item: [u32 class_idx][u32 access_flags][u32 superclass_idx][u32 interfaces_off]
|
||||
// [u32 source_file_idx][u32 annotations_off][u32 class_data_off][u32 static_values_off]
|
||||
#define FF_DEX_CLASS_DEF_SIZE 32
|
||||
#define FF_DEX_OFF_CLASS_DEF_DATA 24
|
||||
#define FF_DEX_OFF_CLASS_DEF_STATIC_VALUES 28
|
||||
|
||||
// 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_NAME 4
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7. Only the two length fields are read from it:
|
||||
// the payload starts right behind them, and the local extra field is allowed to differ from the
|
||||
// central one, so the offset cannot be computed from the directory alone.
|
||||
#define FF_ZIP_LOCAL_HEADER_SIZE 30
|
||||
#define FF_ZIP_OFF_NAME_LENGTH 26
|
||||
#define FF_ZIP_OFF_EXTRA_LENGTH 28
|
||||
|
||||
// The central directory. This is what makes an entry reachable without walking everything in front
|
||||
// of it, and that matters here: framework.jar is 52 MB and its classes2.dex starts 9.6 MB in, so
|
||||
// looking for a local header by scanning the file costs a pass over everything before it -- once per
|
||||
// lookup. Measured on the test device, where the sound module resolves seven codes, that was the
|
||||
// difference between 30 ms and 2 seconds.
|
||||
#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
|
||||
// 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
|
||||
|
||||
#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) {
|
||||
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t dexU32(const uint8_t* p) {
|
||||
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
|
||||
}
|
||||
|
||||
// The dex header points at its tables with offsets that a corrupt or hostile file is free to set
|
||||
// anywhere, including past the end of the mapping. Testing `end - p` alone does not catch that: once
|
||||
// `p` is past `end` the subtraction is negative and casting it to size_t turns it into a huge value,
|
||||
// so the comparison passes and the read goes off the end. Both directions have to be one test.
|
||||
static bool dexInRange(const uint8_t* dex, const uint8_t* end, const uint8_t* p, size_t size) {
|
||||
return p >= dex && p <= end && (size_t) (end - p) >= size;
|
||||
}
|
||||
|
||||
// Same, for a table of `count` elements of `elementSize` bytes. `count` is 64-bit because the
|
||||
// callers add one to an index that came out of the file, and doing that in 32 bits can wrap to zero
|
||||
// and make the check pass for an index that is not in the table. Dividing rather than multiplying is
|
||||
// what keeps a hostile `count` from wrapping the size_t on a 32-bit build -- armeabi-v7a is one, and
|
||||
// there `count * 4` overflows for any count above 0x3FFFFFFF and the check passes again.
|
||||
static bool dexTableInRange(const uint8_t* dex, const uint8_t* end, const uint8_t* p, uint64_t count, size_t elementSize) {
|
||||
if (p < dex || p > end) {
|
||||
return false;
|
||||
}
|
||||
return (size_t) (end - p) / elementSize >= count;
|
||||
}
|
||||
|
||||
// LEB128, at most five bytes for the 32-bit values a dex stores this way. `end` bounds every read:
|
||||
// the lengths and indexes decoded here come out of the file, so a truncated one must not walk off
|
||||
// the end. A read that runs into `end` yields what was decoded so far, and the callers treat the
|
||||
// resulting value as untrusted -- it is either compared against another count or used as an index
|
||||
// that is range checked in turn.
|
||||
static uint32_t dexUleb128(const uint8_t** p, const uint8_t* end) {
|
||||
uint32_t value = 0;
|
||||
for (int shift = 0; shift <= 28; shift += 7) {
|
||||
if (*p >= end) {
|
||||
break;
|
||||
}
|
||||
const uint8_t byte = *(*p)++;
|
||||
value |= (uint32_t) (byte & 0x7f) << shift;
|
||||
if ((byte & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 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_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, 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.
|
||||
//
|
||||
// A payload wider than four bytes is not an int either, and the byte count is what keeps a size
|
||||
// field read out of a corrupt file from shifting past the end. Reading through 64 bits keeps every
|
||||
// shift in range while counting down; `end` is what keeps a truncated payload from running off the
|
||||
// end of the mapping.
|
||||
static bool dexEncodedValue(const uint8_t** p, const uint8_t* end, int32_t* result, bool* isInt) {
|
||||
if (*p >= end) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
uint64_t value = 0;
|
||||
for (uint32_t i = 0; i < size; ++i) {
|
||||
if (*p >= end) {
|
||||
return false;
|
||||
}
|
||||
if (i < sizeof(value)) {
|
||||
value |= (uint64_t) (*(*p)++) << (i * 8);
|
||||
} else {
|
||||
(void) *(*p)++;
|
||||
}
|
||||
}
|
||||
|
||||
*isInt = type == FF_DEX_VALUE_INT && size <= sizeof(uint32_t);
|
||||
if (*isInt) {
|
||||
// VALUE_INT is sign-extended from its own width, not from 32 bits: a one-byte -1 is written
|
||||
// as `21 ff`, one byte of 0xff, and has to come back as -1 rather than 255.
|
||||
const uint32_t bits = size * 8;
|
||||
if ((value & (1ull << (bits - 1))) != 0) {
|
||||
value |= ~((1ull << bits) - 1);
|
||||
}
|
||||
}
|
||||
*result = (int32_t) (uint32_t) value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// string_data_item: a uleb128 length in UTF-16 code units, then MUTF-8 bytes and a NUL terminator.
|
||||
// The descriptors and field names looked up here are ASCII, where MUTF-8 and UTF-8 agree, so the
|
||||
// bytes can be used as they are.
|
||||
static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t index) {
|
||||
const uint8_t* ids = dex + dexU32(dex + FF_DEX_OFF_STRING_IDS);
|
||||
// `string_ids_size` is not read: the check is that the entry `index` names lies inside the
|
||||
// mapping, which is what the read below needs, and the table is the only thing that can say it
|
||||
// without trusting a second field out of the same file.
|
||||
if (!dexTableInRange(dex, end, ids, (uint64_t) index + 1, 4)) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint8_t* p = dex + dexU32(ids + (size_t) index * 4);
|
||||
if (p < dex || p >= end) {
|
||||
return nullptr;
|
||||
}
|
||||
(void) dexUleb128(&p, end);
|
||||
if (p >= end) {
|
||||
return nullptr;
|
||||
}
|
||||
return memchr(p, '\0', (size_t) (end - p)) != nullptr ? (const char*) p : nullptr;
|
||||
}
|
||||
|
||||
// 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.
|
||||
typedef struct FFDexTables {
|
||||
const uint8_t* dex;
|
||||
const uint8_t* end;
|
||||
const uint8_t* types;
|
||||
uint32_t typeCount;
|
||||
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";
|
||||
}
|
||||
const uint32_t fileSize = dexU32(dex + FF_DEX_OFF_FILE_SIZE);
|
||||
if (fileSize > size || fileSize < FF_DEX_HEADER_SIZE) {
|
||||
return "The dex file size is out of range";
|
||||
}
|
||||
const uint8_t* end = dex + fileSize;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS);
|
||||
const uint32_t classCount = dexU32(dex + FF_DEX_OFF_CLASS_DEFS_SIZE);
|
||||
if (!dexTableInRange(dex, end, classes, classCount, FF_DEX_CLASS_DEF_SIZE)) {
|
||||
return "The dex class table is out of range";
|
||||
}
|
||||
|
||||
tables->dex = dex;
|
||||
tables->end = end;
|
||||
tables->types = types;
|
||||
tables->typeCount = typeCount;
|
||||
tables->classes = classes;
|
||||
tables->classCount = classCount;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 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 `requests` 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 FFDexStaticIntRequest* requests, 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, requests[r].classDescriptor) != 0) {
|
||||
continue;
|
||||
}
|
||||
typeIndexes[r] = i;
|
||||
--remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 is not the same as mentioning it: 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 here, with no class_def_item behind it. Only a class_def_item makes the
|
||||
// answer this dex's; without one the caller moves on to the next entry.
|
||||
//
|
||||
// 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) {
|
||||
*defined = false;
|
||||
const uint8_t* dex = tables->dex;
|
||||
const uint8_t* end = tables->end;
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
// Defined here, so from this point on the answer is this dex's, whatever it turns out to be.
|
||||
*defined = true;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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;
|
||||
bool isInt;
|
||||
if (!dexEncodedValue(&values, end, &value, &isInt)) {
|
||||
return "The dex static value list is truncated";
|
||||
}
|
||||
|
||||
if (!dexTableInRange(dex, end, fieldIds, (uint64_t) fieldIndex + 1, FF_DEX_FIELD_ID_SIZE)) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
for (uint32_t r = 0; r < count; ++r) {
|
||||
if (settled[r] || typeIndexes[r] != typeIndex || strcmp(name, requests[r].fieldName) != 0) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// The descriptor is in this dex's type table but the class itself is defined by another entry of
|
||||
// the jar. That is not a failure: `defined` stays false and the caller reads the next entry.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Locating classes.dex
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct FFDexMapping {
|
||||
uint8_t* mapped; // the jar, as mapped; nullptr once released
|
||||
size_t mappedSize;
|
||||
const uint8_t* data; // the dex bytes, inside `mapped`
|
||||
size_t size;
|
||||
} FFDexMapping;
|
||||
|
||||
static void wrapDexMapping(FFDexMapping* mapping) {
|
||||
assert(mapping);
|
||||
if (mapping->mapped != nullptr) {
|
||||
munmap(mapping->mapped, mapping->mappedSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Locates an entry through the jar's central directory.
|
||||
//
|
||||
// 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.
|
||||
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) {
|
||||
const size_t entryLength = strlen(entry);
|
||||
if (jarSize < FF_ZIP_EOCD_SIZE) {
|
||||
return "The jar 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 = jarSize - FF_ZIP_EOCD_SIZE;
|
||||
const size_t oldest = jarSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
|
||||
? jarSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
|
||||
: 0;
|
||||
size_t eocd = SIZE_MAX;
|
||||
for (size_t i = newest;; --i) {
|
||||
if (memcmp(jar + 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 jar",
|
||||
newest - oldest + FF_ZIP_EOCD_SIZE, jarSize);
|
||||
return "The jar has no zip directory";
|
||||
}
|
||||
|
||||
const uint32_t directorySize = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
|
||||
const uint32_t directoryOffset = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
|
||||
if (directoryOffset > jarSize || directorySize > jarSize - directoryOffset) {
|
||||
return "The zip directory is out of range";
|
||||
}
|
||||
|
||||
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
|
||||
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
|
||||
if (memcmp(jar + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
|
||||
return "A zip directory entry is malformed";
|
||||
}
|
||||
const uint16_t nameLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
|
||||
const uint16_t extraLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
|
||||
const uint16_t commentLength = dexU16(jar + 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(jar + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
|
||||
const uint32_t compressedSize = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
|
||||
const uint32_t uncompressed = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
|
||||
const uint32_t localOffset = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
|
||||
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressed == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
|
||||
FF_DEBUG("The dex entry is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
|
||||
compressedSize, uncompressed, localOffset);
|
||||
return "A dex entry is stored as zip64, which is not supported";
|
||||
}
|
||||
if (localOffset > jarSize || FF_ZIP_LOCAL_HEADER_SIZE > jarSize - localOffset) {
|
||||
return "A zip local header is out of range";
|
||||
}
|
||||
|
||||
const uint16_t localNameLength = dexU16(jar + localOffset + FF_ZIP_OFF_NAME_LENGTH);
|
||||
const uint16_t localExtraLength = dexU16(jar + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
|
||||
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
|
||||
if (headerSize > jarSize - localOffset || compressedSize > jarSize - localOffset - headerSize) {
|
||||
return "The zip entry bounds are out of range";
|
||||
}
|
||||
|
||||
*method = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_METHOD);
|
||||
*uncompressedSize = uncompressed;
|
||||
*data = jar + localOffset + headerSize;
|
||||
*dataSize = compressedSize;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
}
|
||||
FF_DEBUG("No entry in the %zu byte jar is named %s", jarSize, entry);
|
||||
return "The jar has no usable dex entry";
|
||||
}
|
||||
|
||||
// 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. 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) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* dex = jar + i;
|
||||
const uint32_t fileSize = dexU32(dex + FF_DEX_OFF_FILE_SIZE);
|
||||
if (dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) {
|
||||
continue;
|
||||
}
|
||||
if (fileSize < FF_DEX_HEADER_SIZE || fileSize > jarSize - i) {
|
||||
continue;
|
||||
}
|
||||
*dexSize = fileSize;
|
||||
return dex;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#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")
|
||||
|
||||
// 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";
|
||||
}
|
||||
|
||||
// `uncompress` is not usable here: a zip entry holds a raw deflate stream, without the two byte
|
||||
// zlib header that entry point insists on. A negative window size tells inflate to skip the
|
||||
// header, which is what the zip format expects.
|
||||
z_stream stream = {};
|
||||
stream.next_in = (Bytef*) data;
|
||||
stream.avail_in = (uInt) dataSize;
|
||||
stream.next_out = buffer;
|
||||
stream.avail_out = (uInt) uncompressedSize;
|
||||
|
||||
const int initStatus = ffinflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream));
|
||||
if (initStatus != Z_OK) {
|
||||
FF_DEBUG("inflateInit2() failed: zlib status %d", initStatus);
|
||||
return "inflateInit2 failed";
|
||||
}
|
||||
const int status = ffinflate(&stream, Z_FINISH);
|
||||
ffinflateEnd(&stream);
|
||||
if (status != Z_STREAM_END || stream.total_out != (uLong) uncompressedSize) {
|
||||
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* 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 = {};
|
||||
|
||||
{
|
||||
FF_AUTO_CLOSE_FD const int fd = open(jarPath, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(%s) failed: %s", jarPath, strerror(errno));
|
||||
return "open(jar) failed";
|
||||
}
|
||||
|
||||
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);
|
||||
// The mapping outlives the descriptor, so the descriptor can go either way from here.
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// One byte per request saying whether it is closed -- answered, or given up on -- and the type
|
||||
// index it was found under in the entry being walked. Both are sized from `count` rather than
|
||||
// from a fixed bound, and both 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));
|
||||
if (settled == nullptr || typeIndexes == nullptr) {
|
||||
FF_DEBUG("Allocating %zu bytes for %u static int requests failed",
|
||||
(size_t) count * (1 + sizeof(uint32_t)), count);
|
||||
return "malloc failed";
|
||||
}
|
||||
|
||||
// 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;
|
||||
const char* error = nullptr;
|
||||
for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) {
|
||||
char entry[sizeof("classes999.dex")];
|
||||
if (index == 1) {
|
||||
memcpy(entry, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY));
|
||||
} else {
|
||||
(void) snprintf(entry, sizeof(entry), "classes%u.dex", index);
|
||||
}
|
||||
|
||||
const uint8_t* data = nullptr;
|
||||
size_t dataSize = 0;
|
||||
uint32_t uncompressedSize = 0;
|
||||
uint16_t method = FF_ZIP_METHOD_STORED;
|
||||
// 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;
|
||||
|
||||
if (findDexEntry(mapping.mapped, mapping.mappedSize, entry, &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.
|
||||
break;
|
||||
}
|
||||
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", entry, mapping.mappedSize);
|
||||
return "No dex in the jar";
|
||||
}
|
||||
} else if (method == FF_ZIP_METHOD_DEFLATED) {
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
error = inflateDex(data, dataSize, uncompressedSize, &inflated);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
data = inflated;
|
||||
dataSize = uncompressedSize;
|
||||
#else
|
||||
return "The jar deflates its dex entries and fastfetch was built without zlib";
|
||||
#endif
|
||||
} else if (method != FF_ZIP_METHOD_STORED) {
|
||||
FF_DEBUG("%s uses compression method %u, only %u (stored) and %u (deflated) are handled",
|
||||
entry, 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
|
||||
// itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds
|
||||
// that 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;
|
||||
|
||||
FFDexTables tables = {};
|
||||
error = dexOpen(mapping.data, mapping.size, &tables);
|
||||
if (error != nullptr) {
|
||||
// The dex cannot be read at all, which no later entry can fix.
|
||||
return error;
|
||||
}
|
||||
|
||||
dexFindTypes(&tables, requests, 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(&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;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
// AIDL gives every method of an interface a transaction code at build time:
|
||||
//
|
||||
// static final int TRANSACTION_<method> = IBinder.FIRST_CALL_TRANSACTION + <declaration index>;
|
||||
//
|
||||
// so the number is a property of the `.aidl` of the release that built the device's jar, not of any
|
||||
// public header. Counting the methods in an AOSP checkout only describes that checkout, and it goes
|
||||
// wrong the moment a release or a vendor fork inserts a method ahead of the one being looked for:
|
||||
// `IWifiManager.getConnectionInfo` is 29 on Android 11 and 41 on Android 16. There is no negotiation
|
||||
// and no discovery -- a wrong code reaches a different method, or none.
|
||||
//
|
||||
// Being a compile-time constant, the value lands in the `static_values` array of the `X$Stub` class
|
||||
// in the jar's dex, paired position by position with that class's static field list. Reading the two
|
||||
// together gives the mapping of the build that is actually on the device, on any release, with
|
||||
// nothing to keep in sync.
|
||||
//
|
||||
// `libdexfile.so` cannot do this for us. It is dlopen-able from an app UID through the ART apex, but
|
||||
// it is not in the NDK and its only C ABI -- the `ADexFile_*` family -- covers methods, not fields;
|
||||
// the field and class-data APIs are C++ with no ABI guarantee.
|
||||
//
|
||||
// `ffReadFileBuffer` would work but pulls the whole jar through the heap for one integer. The jar is
|
||||
// mapped instead, and only the pages actually read are faulted in.
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// What a request is written when the jar does not answer it, on every path out of ffDexStaticInts --
|
||||
// a jar that cannot be opened and a field the class does not declare both leave it behind, so a
|
||||
// caller reads a result rather than what its own stack happened to hold there. No transaction code
|
||||
// is negative: AIDL numbers them from `IBinder.FIRST_CALL_TRANSACTION`, which is 1.
|
||||
//
|
||||
// This is what keeps the reader a lookup and nothing more. Whether a code is one the caller cannot do
|
||||
// without or one it only uses when the build declares it, whether what came back is enough to carry
|
||||
// on, and whether the module reports a value or an error are the caller's decisions, made by reading
|
||||
// the results. The reader finds numbers and says which ones it could not.
|
||||
#define FF_DEX_STATIC_INT_UNRESOLVED (-1)
|
||||
|
||||
// One `<classDescriptor>.<fieldName>` lookup, and where its value is written. `classDescriptor` is
|
||||
// the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
|
||||
typedef struct FFDexStaticIntRequest {
|
||||
const char* classDescriptor;
|
||||
const char* fieldName;
|
||||
int32_t* result;
|
||||
} FFDexStaticIntRequest;
|
||||
|
||||
// Resolves the int value of every request out of the dex entries of `jarPath`, walking `classes.dex`,
|
||||
// `classes2.dex`, ... in order until one of them defines the class.
|
||||
//
|
||||
// Asking for several fields at once is not a convenience. A lookup is dominated by the type table,
|
||||
// which lists every type the dex references -- 8428 descriptors in `framework.jar`'s `classes.dex`,
|
||||
// each one read from a different part of a 51 MB mapping -- and a single pass answers every request
|
||||
// whose class is named in it. Measured over three devices' own `framework.jar`, four fields of two
|
||||
// classes cost one pass rather than four.
|
||||
//
|
||||
// Every request is written a code or FF_DEX_STATIC_INT_UNRESOLVED. A request is never allowed to
|
||||
// take the others down with it: the classes a caller asks for are looked for independently, and one
|
||||
// the jar does not declare leaves its own result at the sentinel while the rest are answered.
|
||||
//
|
||||
// Not finding a code is not a failure. A jar that is readable answers the call, whether it declares
|
||||
// every field asked for, some of them, or none -- the results say which, and what that means for the
|
||||
// module is the caller's to decide. The return value is therefore reserved for the one thing the
|
||||
// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
|
||||
// or does not hold a dex that can be read. nullptr means the lookup ran.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "common/android/package.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// There is no way for a process to ask for its own package name: it is not in /proc/self/status, and
|
||||
// an app cannot list /data/data. The executable path carries it instead -- an app's binaries live
|
||||
// under /data/data/<package>/ or /data/user/<user>/<package>/, and /proc/self/exe resolves there.
|
||||
// Under Termux that is /data/data/com.termux/files/usr/bin/<binary>, whose third component is the
|
||||
// package.
|
||||
//
|
||||
// The memory cgroup carries the same information (4:memory:/apps/com.termux on the device this was
|
||||
// verified on), but the unified hierarchy spells it out as /apps/uid_10478/pid_26943 with no name
|
||||
// left in it, so the executable is the portable half of the two.
|
||||
bool ffAndroidGetOwnPackage(char* buffer, size_t capacity) {
|
||||
char path[4096];
|
||||
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (length <= 0) {
|
||||
FF_DEBUG("Cannot read /proc/self/exe, so the calling package cannot be derived");
|
||||
return false;
|
||||
}
|
||||
path[length] = '\0';
|
||||
|
||||
const char* rest = nullptr;
|
||||
const char* const dataPrefix = "/data/data/";
|
||||
const char* const userPrefix = "/data/user/";
|
||||
if (ffStrStartsWith(path, dataPrefix)) {
|
||||
rest = path + strlen(dataPrefix);
|
||||
} else if (ffStrStartsWith(path, userPrefix)) {
|
||||
// /data/user/<user>/<package>/... -- the user number has to be stepped over before the
|
||||
// package name starts, and only strchr() answers where that is.
|
||||
rest = strchr(path + strlen(userPrefix), '/');
|
||||
if (rest != nullptr) {
|
||||
rest += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (rest == nullptr) {
|
||||
const uint32_t uid = instance.state.platform.uid;
|
||||
FF_DEBUG("The executable is not in an app data directory (\"%s\"), uid %u", path, uid);
|
||||
if (uid != FF_ANDROID_PRIVILEGED_UID_SHELL) {
|
||||
return false;
|
||||
}
|
||||
static const char shellPackage[] = FF_ANDROID_SHELL_PACKAGE;
|
||||
if (sizeof(shellPackage) > capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, shellPackage, sizeof(shellPackage));
|
||||
FF_DEBUG("The calling package is \"%s\" (the shell owns it)", buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* end = strchr(rest, '/');
|
||||
const size_t nameLength = end != nullptr ? (size_t) (end - rest) : strlen(rest);
|
||||
if (nameLength == 0 || nameLength >= capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, rest, nameLength);
|
||||
buffer[nameLength] = '\0';
|
||||
FF_DEBUG("The calling package is \"%s\"", buffer);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#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
|
||||
// pushed to /data/local/tmp is the case that showed up -- can answer with this name and have the
|
||||
// system services accept it. See FF_ANDROID_PRIVILEGED_UID_SHELL in common/android/api.h.
|
||||
#define FF_ANDROID_SHELL_PACKAGE "com.android.shell"
|
||||
|
||||
// Some of the system services take the caller's package name as an argument and check it against the
|
||||
// calling UID rather than against the name being real: a name nobody owns, and a real name belonging
|
||||
// to another app, are refused exactly like a null one. So it has to be the caller's own name.
|
||||
//
|
||||
// Whether the name is checked at all depends on the service and the release -- `getConnectionInfo`
|
||||
// answered "Package com.termux does not belong to <shell uid>" on one Android 16 device while the
|
||||
// same name was accepted on an Android 11 one, and `getWallpaper` refuses a null, empty or foreign
|
||||
// name on that Android 16 device -- so passing the real one is what works everywhere.
|
||||
//
|
||||
// Writes the name into `buffer`, terminator included, and returns whether one was found. `capacity`
|
||||
// is the caller's, because the parcel the name ends up in is sized from it.
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffAndroidGetOwnPackage(char* buffer, size_t capacity);
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "smc_temps.h"
|
||||
#include "common/apple/cf_helpers.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <stdint.h>
|
||||
@@ -83,7 +84,10 @@ static void smcUltostr(char* str, uint32_t val) {
|
||||
static const char* smcCall(io_connect_t conn, uint32_t selector, SmcKeyData_t* inputStructure, SmcKeyData_t* outputStructure) {
|
||||
size_t size = sizeof(SmcKeyData_t);
|
||||
|
||||
if (IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size) != kIOReturnSuccess) {
|
||||
const IOReturn callStatus = IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size);
|
||||
if (callStatus != kIOReturnSuccess) {
|
||||
FF_DEBUG("IOConnectCallStructMethod(selector=%u, command=%u) failed: IOReturn 0x%08x",
|
||||
selector, (uint32_t) (unsigned char) inputStructure->data8, (unsigned) callStatus);
|
||||
return "IOConnectCallStructMethod(conn) failed";
|
||||
}
|
||||
return nullptr;
|
||||
@@ -136,10 +140,13 @@ static const char* smcReadSmcVal(io_connect_t conn, const UInt32Char_t key, SmcV
|
||||
static const char* smcOpen(io_connect_t* conn) {
|
||||
FF_IOOBJECT_AUTO_RELEASE io_object_t device = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMC"));
|
||||
if (!device) {
|
||||
FF_DEBUG("IOKit has no AppleSMC service");
|
||||
return "No SMC device found";
|
||||
}
|
||||
|
||||
if (IOServiceOpen(device, mach_task_self(), 0, conn) != kIOReturnSuccess) {
|
||||
const IOReturn openStatus = IOServiceOpen(device, mach_task_self(), 0, conn);
|
||||
if (openStatus != kIOReturnSuccess) {
|
||||
FF_DEBUG("IOServiceOpen(AppleSMC) failed: IOReturn 0x%08x", (unsigned) openStatus);
|
||||
return "IOServiceOpen() failed";
|
||||
}
|
||||
|
||||
@@ -153,6 +160,7 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
return error;
|
||||
}
|
||||
if (val.dataSize == 0) {
|
||||
FF_DEBUG("The SMC key %s reported a data size of 0", key);
|
||||
return "Empty SMC result";
|
||||
}
|
||||
|
||||
@@ -173,9 +181,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
*value = (double) ntohll(*(uint64_t*) (val.bytes));
|
||||
break;
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: unsigned type %s has an unsupported size of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC unsigned integer data size";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported unsigned type %s", key, val.dataType);
|
||||
return "Unsupported SMC unsigned data type";
|
||||
}
|
||||
break;
|
||||
@@ -204,9 +215,13 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
} else if (ffStrEquals(val.dataType, "fpe2")) {
|
||||
*value = ntohs(*(uint16_t*) (val.bytes)) / 4.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported fixed point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC floating point data type";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported floating point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC floating point data type";
|
||||
}
|
||||
break;
|
||||
@@ -227,6 +242,8 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
*value = (double) ntohll(*(int64_t*) (val.bytes));
|
||||
break;
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: signed type %s has an unsupported size of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC signed integer data size";
|
||||
}
|
||||
} else if (val.dataType[1] == 'p' && val.dataSize == 2) // signed fixed point types
|
||||
@@ -252,9 +269,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
} else if (ffStrEquals(val.dataType, "spf0")) {
|
||||
*value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported signed fixed point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC signed integer data type";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported signed type %s", key, val.dataType);
|
||||
return "Unsupported SMC signed data type";
|
||||
}
|
||||
break;
|
||||
@@ -263,11 +283,15 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
if (ffStrEquals(val.dataType, "{pwm") && val.dataSize == 2) {
|
||||
*value = (double) ntohs(*(uint16_t*) (val.bytes)) * 100 / 65536.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported special type %s of %u bytes", key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC special data type";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: unknown data type '%c%c' (%s) of %u bytes",
|
||||
key, val.dataType[0] ? val.dataType[0] : '?', val.dataType[1] ? val.dataType[1] : '?',
|
||||
val.dataType, val.dataSize);
|
||||
return "Unsupported SMC data type";
|
||||
}
|
||||
return nullptr;
|
||||
@@ -300,6 +324,7 @@ const char* ffDetectSmcSpecificTemp(const char* sensor, double* result) {
|
||||
}
|
||||
|
||||
if (!detectTemp(conn, sensor, result)) {
|
||||
FF_DEBUG("The SMC sensor %s could not be read, or read a value outside 10..120", sensor);
|
||||
return "Could not read SMC temperature";
|
||||
}
|
||||
|
||||
@@ -490,6 +515,7 @@ const char* ffDetectSmcTemps(enum FFTempType type, double* result) {
|
||||
break;
|
||||
}
|
||||
|
||||
FF_DEBUG("None of the SMC sensors for temp type %d reported a usable reading", (int) type);
|
||||
if (count == 0) {
|
||||
return "No temperatures detected";
|
||||
}
|
||||
|
||||
+5
-4
@@ -2,8 +2,9 @@
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
|
||||
static inline FFstrbuf ffBase64EncodeStrbuf(const FFstrbuf* in) {
|
||||
// `str`, `out_size` and `output` are dereferenced unconditionally
|
||||
[[gnu::nonnull(2, 3, 4)]] void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
|
||||
[[gnu::nonnull(1), nodiscard]] static inline FFstrbuf ffBase64EncodeStrbuf(const FFstrbuf* in) {
|
||||
FFstrbuf out = ffStrbufCreateA(10 + in->length * 4 / 3);
|
||||
ffBase64EncodeRaw(in->length, in->chars, &out.length, out.chars);
|
||||
assert(out.length < out.allocated);
|
||||
@@ -11,8 +12,8 @@ static inline FFstrbuf ffBase64EncodeStrbuf(const FFstrbuf* in) {
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
|
||||
static inline FFstrbuf ffBase64DecodeStrbuf(const FFstrbuf* in) {
|
||||
[[gnu::nonnull(2, 3, 4)]] void ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
|
||||
[[gnu::nonnull(1), nodiscard]] static inline FFstrbuf ffBase64DecodeStrbuf(const FFstrbuf* in) {
|
||||
FFstrbuf out = ffStrbufCreateA(10 + in->length * 3 / 4);
|
||||
ffBase64DecodeRaw(in->length, in->chars, &out.length, out.chars);
|
||||
assert(out.length < out.allocated);
|
||||
|
||||
+12
-28
@@ -1,38 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
#include "common/time.h"
|
||||
|
||||
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
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result);
|
||||
[[gnu::nonnull(2)]] void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result);
|
||||
|
||||
+6
-1
@@ -2,7 +2,12 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if __BIG_ENDIAN__
|
||||
// `__BIG_ENDIAN__` alone is not enough: GCC does not define it, it documents only `__BYTE_ORDER__`
|
||||
// and the `__ORDER_*_ENDIAN__` values. Testing the former silently selected the little-endian
|
||||
// branch on s390x, where `FF_READ_BE` then byte-swapped values that were already big-endian.
|
||||
// `defined(__BIG_ENDIAN__)` alone would not do either: a macro that is defined as 0 is not a
|
||||
// big-endian platform.
|
||||
#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || (defined(__BIG_ENDIAN__) && __BIG_ENDIAN__)
|
||||
#define FF_READ_LE(x) _Generic((x), \
|
||||
uint16_t: __builtin_bswap16(x), \
|
||||
uint32_t: __builtin_bswap32(x), \
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
bool ffFreqAppendNum(uint32_t mhz, FFstrbuf* result);
|
||||
// Returns false when nothing was appended (e.g. `mhz == 0`); callers routinely discard that, so it
|
||||
// is not `nodiscard`.
|
||||
[[gnu::nonnull(2)]] bool ffFreqAppendNum(uint32_t mhz, FFstrbuf* result);
|
||||
|
||||
@@ -27,7 +27,7 @@ void ffPlatformInit(FFPlatform* platform) {
|
||||
ffStrbufInit(&info->release);
|
||||
ffStrbufInit(&info->version);
|
||||
ffStrbufInit(&info->architecture);
|
||||
info->pageSize = 0;
|
||||
info->pageSizeShift = 12; // 4096, reasonable fallback
|
||||
|
||||
ffPlatformInitImpl(platform);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "common/strutil.h"
|
||||
#include "common/io.h"
|
||||
#include "common/path.h"
|
||||
#include "common/mallocHelper.h"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
@@ -19,7 +20,6 @@
|
||||
#elif defined(__OpenBSD__)
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <kvm.h>
|
||||
#include "common/path.h"
|
||||
#elif defined(__HAIKU__)
|
||||
#include <image.h>
|
||||
@@ -68,13 +68,14 @@ static void getExePath(FFPlatform* platform) {
|
||||
// Current implementation uses argv[0], which can be easily spoofed.
|
||||
// See #2195
|
||||
size_t exePathLen = 0;
|
||||
kvm_t* kd = kvm_openfiles(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr);
|
||||
if (kd) {
|
||||
int kpCount;
|
||||
struct kinfo_proc* kp = kvm_getprocs(kd, KERN_PROC_PID, (pid_t) platform->pid, sizeof(*kp), &kpCount);
|
||||
if (kp && kpCount == 1) {
|
||||
char** argv = kvm_getargv(kd, kp, 0);
|
||||
if (argv && argv[0]) {
|
||||
{
|
||||
char argvBuf[ARG_MAX];
|
||||
size_t argvSize = sizeof(argvBuf);
|
||||
int argvMib[] = { CTL_KERN, KERN_PROC_ARGS, (pid_t) platform->pid, KERN_PROC_ARGV };
|
||||
if (sysctl(argvMib, ARRAY_SIZE(argvMib), argvBuf, &argvSize, nullptr, 0) == 0) {
|
||||
// The buffer is filled with an array of char pointers followed by the strings themselves
|
||||
char** argv = (char**) argvBuf;
|
||||
if (argv[0] && (char*) argv[0] >= argvBuf && (char*) argv[0] < argvBuf + argvSize) {
|
||||
char* arg0 = argv[0];
|
||||
if (arg0[0]) {
|
||||
if (strchr(arg0, '/') != nullptr) // likely a path (absolute or relative)
|
||||
@@ -97,25 +98,64 @@ static void getExePath(FFPlatform* platform) {
|
||||
if (exePathLen > 0) {
|
||||
struct stat st;
|
||||
if (stat(exePath, &st) == 0 && S_ISREG(st.st_mode)) {
|
||||
int cntp;
|
||||
struct kinfo_file* kf = kvm_getfiles(kd, KERN_FILE_BYPID, (pid_t) platform->pid, sizeof(*kf), &cntp);
|
||||
if (kf) {
|
||||
int i;
|
||||
for (i = 0; i < cntp; i++) {
|
||||
if (kf[i].fd_fd == KERN_FILE_TEXT) {
|
||||
// KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list.
|
||||
if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) {
|
||||
i = -1;
|
||||
// Replicate kvm_getfiles()'s live path: {CTL_KERN, KERN_FILE, KERN_FILE_BYPID, pid, esize, count}
|
||||
int fileMib[6] = { CTL_KERN, KERN_FILE, KERN_FILE_BYPID, (pid_t) platform->pid, (int) sizeof(struct kinfo_file), 0 };
|
||||
size_t fileSize = 0;
|
||||
if (sysctl(fileMib, ARRAY_SIZE(fileMib), nullptr, &fileSize, nullptr, 0) == 0) {
|
||||
fileSize += fileSize / 8; // add 12.5% for the table growing between the two calls
|
||||
size_t fileCapacity = fileSize;
|
||||
FF_AUTO_FREE struct kinfo_file* kf = (struct kinfo_file*) malloc(fileCapacity);
|
||||
if (kf) {
|
||||
int rv;
|
||||
// The table only grows, so the loop terminates as long as it grows by less
|
||||
// than 12.5% per retry. Cap it anyway: spinning on a pathological growth
|
||||
// rate would hang fastfetch, and giving up only costs the executable check
|
||||
// below, which already assumes the path is correct when the list is missing.
|
||||
int retries = 5;
|
||||
while (true) {
|
||||
fileMib[5] = (int) (fileCapacity / sizeof(struct kinfo_file));
|
||||
fileSize = fileCapacity; // sysctl takes the size of the buffer as input
|
||||
rv = sysctl(fileMib, ARRAY_SIZE(fileMib), kf, &fileSize, nullptr, 0);
|
||||
if (rv == 0 || errno != ENOMEM || retries-- == 0) {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
// The file table grew between the two calls. The buffer has to grow
|
||||
// before the retry: the next call advertises fileCapacity as the size
|
||||
// of kf, and the kernel would copy that many bytes into the old
|
||||
// allocation.
|
||||
// OpenBSD writes back the size it needs, FreeBSD only the number of
|
||||
// bytes it managed to copy, so take the larger one and add ~10% on
|
||||
// top -- the growth libprocstat uses for this very sysctl.
|
||||
size_t newCapacity = fileSize > fileCapacity ? fileSize : fileCapacity;
|
||||
newCapacity += newCapacity / 8;
|
||||
struct kinfo_file* newKf = (struct kinfo_file*) realloc(kf, newCapacity);
|
||||
if (newKf == nullptr) {
|
||||
rv = -1;
|
||||
break;
|
||||
}
|
||||
kf = newKf;
|
||||
fileCapacity = newCapacity;
|
||||
}
|
||||
|
||||
if (rv == 0) {
|
||||
int cntp = (int) (fileSize / sizeof(struct kinfo_file));
|
||||
int i;
|
||||
for (i = 0; i < cntp; i++) {
|
||||
if (kf[i].fd_fd == KERN_FILE_TEXT) {
|
||||
// KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list.
|
||||
if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) {
|
||||
i = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i < 0) {
|
||||
exePathLen = 0;
|
||||
}
|
||||
}
|
||||
// If we can't get the list of open files, we can't verify that the file is actually the executable
|
||||
// Assume it is
|
||||
}
|
||||
if (i < 0) {
|
||||
exePathLen = 0;
|
||||
}
|
||||
} else {
|
||||
// If we can't get the list of open files, we can't verify that the file is actually the executable
|
||||
// Assume it is
|
||||
}
|
||||
} else {
|
||||
exePathLen = 0;
|
||||
@@ -124,7 +164,6 @@ static void getExePath(FFPlatform* platform) {
|
||||
}
|
||||
}
|
||||
}
|
||||
kvm_close(kd);
|
||||
}
|
||||
#elif defined(__sun)
|
||||
ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
|
||||
@@ -280,12 +319,10 @@ static void getSysinfo(FFPlatformSysinfo* info, const struct utsname* uts) {
|
||||
#endif
|
||||
ffStrbufAppendS(&info->architecture, uts->machine);
|
||||
|
||||
#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
||||
size_t length = sizeof(info->pageSize);
|
||||
sysctl((int[]) { CTL_HW, HW_PAGESIZE }, 2, &info->pageSize, &length, nullptr, 0);
|
||||
#else
|
||||
info->pageSize = (uint32_t) sysconf(_SC_PAGESIZE);
|
||||
#endif
|
||||
// _SC_PAGESIZE is one of the sysconf names <unistd.h> requires every implementation to define,
|
||||
// and the only failure POSIX defines for sysconf is an invalid name, so this does not fail.
|
||||
// It replaces the former sysctl(CTL_HW, HW_PAGESIZE) call on FreeBSD / macOS / OpenBSD / NetBSD.
|
||||
info->pageSizeShift = (uint32_t) __builtin_ctzl((unsigned long) sysconf(_SC_PAGESIZE));
|
||||
}
|
||||
|
||||
static void getCwd(FFPlatform* platform) {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
#include "common/io.h"
|
||||
#include "common/library.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/windows/folders.h"
|
||||
#include "common/windows/unicode.h"
|
||||
#include "common/windows/registry.h"
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#include <stdalign.h>
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <sddl.h>
|
||||
|
||||
#define SECURITY_WIN32 1 // For secext.h
|
||||
@@ -43,43 +43,27 @@ static void getExePath(FFPlatform* platform) {
|
||||
ffStrbufReplaceAllC(&platform->exePath, '\\', '/');
|
||||
}
|
||||
|
||||
static void getHomeDir(FFPlatform* platform) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
ffStrbufSetWS(&platform->homeDir, pPath);
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
} else {
|
||||
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE"));
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
}
|
||||
CoTaskMemFree(pPath);
|
||||
}
|
||||
|
||||
static void getCacheDir(FFPlatform* platform) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
ffStrbufSetWS(&platform->cacheDir, pPath);
|
||||
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
|
||||
} else {
|
||||
if (!ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &platform->cacheDir)) {
|
||||
ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
|
||||
ffStrbufAppendS(&platform->cacheDir, "AppData/Local/");
|
||||
return;
|
||||
}
|
||||
CoTaskMemFree(pPath);
|
||||
|
||||
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
|
||||
}
|
||||
|
||||
static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath);
|
||||
CoTaskMemFree(pPath);
|
||||
ffStrbufReplaceAllC(&buffer, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&buffer, '/');
|
||||
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
|
||||
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
|
||||
}
|
||||
static void platformPathAddKnownFolder(FFlist* dirs, FFKnownFolder folder) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
if (!ffGetKnownFolderPath(folder, &buffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ffStrbufReplaceAllC(&buffer, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&buffer, '/');
|
||||
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
|
||||
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +96,9 @@ static void getConfigDirs(FFPlatform* platform) {
|
||||
}
|
||||
|
||||
ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
|
||||
ffPlatformPathAddHome(&platform->configDirs, platform, "");
|
||||
}
|
||||
|
||||
@@ -126,9 +110,9 @@ static void getDataDirs(FFPlatform* platform) {
|
||||
platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share");
|
||||
}
|
||||
ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
|
||||
ffPlatformPathAddHome(&platform->dataDirs, platform, "");
|
||||
}
|
||||
|
||||
@@ -156,9 +140,25 @@ static void getUserName(FFPlatform* platform) {
|
||||
TOKEN_USER* tokenUser = (TOKEN_USER*) buf;
|
||||
UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) };
|
||||
if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) {
|
||||
ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer);
|
||||
uint32_t sidLength = sidString.Length / sizeof(wchar_t);
|
||||
ffStrbufSetNWS(&platform->sid, sidLength, sidString.Buffer);
|
||||
|
||||
// The SID is already in hand, so the profile directory costs one registry read rather
|
||||
// than a second NtQueryInformationToken(). ffGetProfilePath() needs a null-terminated
|
||||
// string, which RtlConvertSidToUnicodeString() does not produce.
|
||||
if (sidLength < ARRAY_SIZE(buffer)) {
|
||||
buffer[sidLength] = L'\0';
|
||||
ffGetProfilePath(buffer, &platform->homeDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (platform->homeDir.length == 0) {
|
||||
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); // Last resort; ffGetProfilePath() covers the normal case
|
||||
}
|
||||
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
}
|
||||
|
||||
static void getHostName(FFPlatform* platform) {
|
||||
@@ -229,9 +229,7 @@ static void getSystemReleaseAndVersion(FFPlatformSysinfo* info) {
|
||||
static void getSystemPageSize(FFPlatformSysinfo* info) {
|
||||
SYSTEM_BASIC_INFORMATION sbi;
|
||||
if (NT_SUCCESS(NtQuerySystemInformation(SystemBasicInformation, &sbi, sizeof(sbi), nullptr))) {
|
||||
info->pageSize = sbi.PhysicalPageSize;
|
||||
} else {
|
||||
info->pageSize = 4096;
|
||||
info->pageSizeShift = (uint32_t) __builtin_ctzl(sbi.PhysicalPageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,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);
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "common/FFcache.h"
|
||||
|
||||
// Entries are file scope statics owned by the cache owner, so they can be linked into a list
|
||||
// without any allocation. The list holds every entry that has been used at least once: an entry
|
||||
// that was never used has nothing to drop.
|
||||
static FFcacheEntry* entries = nullptr;
|
||||
|
||||
static void registerEntry(FFcacheEntry* entry) {
|
||||
if (entry->registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry->registered = true;
|
||||
entry->next = entries;
|
||||
entries = entry;
|
||||
}
|
||||
|
||||
void* ffCacheGet(FFcacheEntry* entry) {
|
||||
if (ffCacheBeginInit(entry) && entry->init) {
|
||||
entry->init(entry->storage);
|
||||
}
|
||||
|
||||
return entry->storage;
|
||||
}
|
||||
|
||||
bool ffCacheBeginInit(FFcacheEntry* entry) {
|
||||
registerEntry(entry);
|
||||
|
||||
if (entry->initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ffCacheInvalidate(FFcacheEntry* entry) {
|
||||
if (!entry->initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry->initialized = false;
|
||||
|
||||
if (entry->destroy) {
|
||||
entry->destroy(entry->storage);
|
||||
}
|
||||
}
|
||||
|
||||
void ffCacheInvalidateAll(void) {
|
||||
for (FFcacheEntry* entry = entries; entry; entry = entry->next) {
|
||||
ffCacheInvalidate(entry);
|
||||
}
|
||||
}
|
||||
+25
-35
@@ -21,8 +21,6 @@ void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate) {
|
||||
}
|
||||
|
||||
void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
|
||||
assert(format != nullptr);
|
||||
|
||||
char* buffer = nullptr;
|
||||
int len = vasprintf(&buffer, format, arguments);
|
||||
assert(len >= 0);
|
||||
@@ -33,8 +31,6 @@ void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
|
||||
// Takes ownership of `heapStr`. The caller must not free `heapStr` after calling this
|
||||
// function; the memory will be managed and freed via the associated FFstrbuf.
|
||||
void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr) {
|
||||
assert(heapStr != nullptr);
|
||||
|
||||
strbuf->length = length;
|
||||
size_t allocSize = ffMallocUsableSize(heapStr);
|
||||
if (allocSize == 0) {
|
||||
@@ -157,8 +153,6 @@ void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transfo
|
||||
}
|
||||
|
||||
void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
|
||||
assert(format != nullptr);
|
||||
|
||||
va_list copy;
|
||||
va_copy(copy, arguments);
|
||||
|
||||
@@ -192,8 +186,6 @@ const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char unti
|
||||
}
|
||||
|
||||
void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) {
|
||||
assert(format != nullptr);
|
||||
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
|
||||
@@ -209,8 +201,6 @@ void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) {
|
||||
}
|
||||
|
||||
void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...) {
|
||||
assert(format != nullptr);
|
||||
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
ffStrbufAppendVF(strbuf, format, arguments);
|
||||
@@ -236,15 +226,11 @@ void ffStrbufPrependC(FFstrbuf* strbuf, char c) {
|
||||
}
|
||||
|
||||
void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
|
||||
assert(strbuf != nullptr);
|
||||
|
||||
if (length == 0) {
|
||||
ffStrbufClear(strbuf);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(value != nullptr);
|
||||
|
||||
if (strbuf->allocated <= length) {
|
||||
char* newBuf = malloc(sizeof(char) * (length + 1));
|
||||
memcpy(newBuf, value, length);
|
||||
@@ -262,7 +248,8 @@ void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
|
||||
}
|
||||
|
||||
void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value) {
|
||||
assert(value && value != strbuf);
|
||||
// `value` is non-null per the `nonnull(2)` contract; the aliasing check cannot be expressed by an attribute
|
||||
assert(value != strbuf);
|
||||
|
||||
if (value->length == 0) {
|
||||
ffStrbufClear(strbuf);
|
||||
@@ -647,7 +634,6 @@ void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c) {
|
||||
}
|
||||
|
||||
bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
|
||||
assert(lineptr && n && buffer);
|
||||
assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
|
||||
assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
|
||||
|
||||
@@ -678,7 +664,6 @@ bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffe
|
||||
}
|
||||
|
||||
void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
|
||||
assert(buffer && lineptr && n);
|
||||
assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
|
||||
assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
|
||||
|
||||
@@ -723,16 +708,17 @@ bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) {
|
||||
/// @param compLength The length of the separated string to check.
|
||||
/// @param comp The separated string to check.
|
||||
/// @param separator The separator character.
|
||||
///
|
||||
/// Empty-string handling:
|
||||
/// - If `strbuf` is empty, it matches only if `comp` contains an empty segment
|
||||
/// (e.g. "abc::def", "abc:", ":abc") or `comp` itself is empty.
|
||||
/// - If `comp` is empty (compLength == 0), it matches only if `strbuf` is also empty.
|
||||
bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
|
||||
if (strbuf->length == 0) {
|
||||
return true;
|
||||
if (__builtin_expect(compLength == 0, false)) {
|
||||
return strbuf->length == 0;
|
||||
}
|
||||
|
||||
if (compLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const char* p = comp; p < comp + compLength;) {
|
||||
for (const char* p = comp; p <= comp + compLength;) {
|
||||
const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
|
||||
if (colon == nullptr) {
|
||||
uint32_t remainingLen = (uint32_t) (comp + compLength - p);
|
||||
@@ -752,15 +738,11 @@ bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const
|
||||
|
||||
/// @brief Case insensitive version of ffStrbufMatchSeparatedNS.
|
||||
bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
|
||||
if (strbuf->length == 0) {
|
||||
return true;
|
||||
if (__builtin_expect(compLength == 0, false)) {
|
||||
return strbuf->length == 0;
|
||||
}
|
||||
|
||||
if (compLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const char* p = comp; p < comp + compLength;) {
|
||||
for (const char* p = comp; p <= comp + compLength;) {
|
||||
const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
|
||||
if (colon == nullptr) {
|
||||
uint32_t remainingLen = (uint32_t) (comp + compLength - p);
|
||||
@@ -802,9 +784,15 @@ int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) {
|
||||
/// @param compLength The length of the separated string to check.
|
||||
/// @param comp The substring to check.
|
||||
/// @param separator The separator character.
|
||||
///
|
||||
/// Empty-string handling is symmetric to ffStrbufMatchSeparatedNS
|
||||
bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
|
||||
if (__builtin_expect(strbuf->length == 0, false)) {
|
||||
return compLength == 0; // An empty list contains only the empty segment
|
||||
}
|
||||
|
||||
uint32_t startIndex = 0;
|
||||
while (startIndex < strbuf->length) {
|
||||
while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked
|
||||
uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
|
||||
|
||||
uint32_t folderLength = colonIndex - startIndex;
|
||||
@@ -819,8 +807,12 @@ bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, con
|
||||
}
|
||||
|
||||
bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
|
||||
if (__builtin_expect(strbuf->length == 0, false)) {
|
||||
return compLength == 0; // An empty list contains only the empty segment
|
||||
}
|
||||
|
||||
uint32_t startIndex = 0;
|
||||
while (startIndex < strbuf->length) {
|
||||
while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked
|
||||
uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
|
||||
|
||||
uint32_t folderLength = colonIndex - startIndex;
|
||||
@@ -835,8 +827,6 @@ bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLeng
|
||||
}
|
||||
|
||||
bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) {
|
||||
assert(strbuf);
|
||||
|
||||
if (strbuf->length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,19 @@ void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char*
|
||||
str += 3;
|
||||
}
|
||||
|
||||
// The bytes have to be widened through `uint8_t`: `char` is signed here, so widening a byte
|
||||
// with the high bit set would sign extend it and the `& 63` below would then read the extension
|
||||
// instead of the byte's own bits. The whole groups above are immune because they read through a
|
||||
// `uint32_t` instead.
|
||||
if (size % 3 == 1) {
|
||||
uint64_t n = (uint64_t) *str << 16;
|
||||
uint64_t n = (uint64_t) (uint8_t) *str << 16;
|
||||
*out++ = chars[(n >> 18) & 63];
|
||||
*out++ = chars[(n >> 12) & 63];
|
||||
*out++ = '=';
|
||||
*out++ = '=';
|
||||
} else if (size % 3 == 2) {
|
||||
uint64_t n = (uint64_t) *str++ << 16;
|
||||
n |= (uint64_t) *str << 8;
|
||||
uint64_t n = (uint64_t) (uint8_t) *str++ << 16;
|
||||
n |= (uint64_t) (uint8_t) *str << 8;
|
||||
*out++ = chars[(n >> 18) & 63];
|
||||
*out++ = chars[(n >> 12) & 63];
|
||||
*out++ = chars[(n >> 6) & 63];
|
||||
@@ -62,7 +66,7 @@ static void init_decode_table() {
|
||||
|
||||
#define next_char(x) uint8_t x = decode_table[(uint8_t) *str++];
|
||||
|
||||
bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) {
|
||||
void ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) {
|
||||
if (*(uint64_t*) decode_table == 0) {
|
||||
init_decode_table();
|
||||
}
|
||||
@@ -109,5 +113,4 @@ bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char*
|
||||
|
||||
*out = '\0';
|
||||
*out_size = (uint32_t) (out - output);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "common/binary.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
@@ -108,6 +110,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
if (is_64) {
|
||||
const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
|
||||
if (!header) {
|
||||
FF_DEBUG("Failed to read the 64-bit mach header at offset %lld", (long long) offset);
|
||||
return "read mach header failed";
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
} else {
|
||||
const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
|
||||
if (!header) {
|
||||
FF_DEBUG("Failed to read the mach header at offset %lld", (long long) offset);
|
||||
return "read mach header failed";
|
||||
}
|
||||
|
||||
@@ -200,6 +204,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
|
||||
if (!headerRaw) {
|
||||
FF_DEBUG("Failed to read the fat header");
|
||||
return "read fat header failed";
|
||||
}
|
||||
|
||||
@@ -251,6 +256,7 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
|
||||
return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
|
||||
}
|
||||
}
|
||||
FF_DEBUG("No supported Mach-O slice was found in the fat binary");
|
||||
return "Unsupported fat header";
|
||||
}
|
||||
|
||||
@@ -265,11 +271,13 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
|
||||
const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(\"%s\") failed: %s", machoFile, strerror(errno));
|
||||
return "File could not be opened";
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || st.st_size <= 0) {
|
||||
FF_DEBUG("fstat(\"%s\") failed or the file is empty", machoFile);
|
||||
return "Failed to stat file";
|
||||
}
|
||||
|
||||
@@ -278,12 +286,14 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
|
||||
.length = (size_t) st.st_size,
|
||||
};
|
||||
if (mapping.data == MAP_FAILED) {
|
||||
FF_DEBUG("mmap(\"%s\", %lld bytes) failed: %s", machoFile, (long long) st.st_size, strerror(errno));
|
||||
return "mmap failed";
|
||||
}
|
||||
|
||||
// Read the magic number to determine the type of binary
|
||||
const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
|
||||
if (!magic) {
|
||||
FF_DEBUG("Failed to read the magic number");
|
||||
return "read magic number failed";
|
||||
}
|
||||
|
||||
@@ -291,6 +301,7 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
|
||||
// MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
|
||||
// However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
|
||||
if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
|
||||
FF_DEBUG("Unsupported magic number 0x%08x", (unsigned) *magic);
|
||||
return "Unsupported format or big endian mach-o file";
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -63,6 +63,21 @@ bool ffParseModuleOptions(const char* key, const char* value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static yyjson_val* findStructureModuleConfig(FFdata* data, const char* moduleType);
|
||||
|
||||
// The module options live in the JSON config only. They must be merged here as well as in
|
||||
// `parseStructureCommand`, because `ffPrepareXxx` takes the first snapshot while `ffDetectXxx`
|
||||
// takes the second one. If the two disagree, they list different entries and the deltas are
|
||||
// meaningless. Keeping both call sites on this single helper is what makes them agree.
|
||||
static void initStructureModuleOptions(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
|
||||
baseInfo->initOptions(options);
|
||||
|
||||
yyjson_val* configModule = findStructureModuleConfig(data, baseInfo->name);
|
||||
if (configModule != nullptr) {
|
||||
baseInfo->parseJsonObject(options, configModule);
|
||||
}
|
||||
}
|
||||
|
||||
void ffPrepareCommandOption(FFdata* data) {
|
||||
char* moduleType = nullptr;
|
||||
size_t moduleLen = 0;
|
||||
@@ -70,68 +85,86 @@ void ffPrepareCommandOption(FFdata* data) {
|
||||
#define FF_IF_MODULE_MATCH(moduleNameConstant) if (ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
|
||||
|
||||
switch (moduleType[0]) {
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
case 'C':
|
||||
case 'c':
|
||||
#endif
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name)
|
||||
ffPrepareCPUUsage();
|
||||
break;
|
||||
#endif
|
||||
ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_DISKIO
|
||||
#if !FF_MODULE_DISABLE_COMMAND
|
||||
FF_IF_MODULE_MATCH(ffCommandModuleInfo.name) {
|
||||
// `command` runs its shell commands in parallel: `ffPrepareCommand` spawns them here,
|
||||
// in the prepare pass, and `ffDetectCommand` collects them in the print pass. The
|
||||
// command line itself can only be read from the JSON config, and this code path does
|
||||
// not consult it, so nothing would ever be queued and the module would report an
|
||||
// internal error marker. Refuse it up front instead of letting it get that far.
|
||||
fputs("Error: module `command` is not supported with `--structure`\n", stderr);
|
||||
fputs(" Its options can only be read from the JSON config, which `--structure` bypasses.\n", stderr);
|
||||
fputs(" Add it to the config instead, e.g. `{ \"modules\": [ { \"type\": \"command\", \"text\": \"uname -r\" } ] }`\n", stderr);
|
||||
exit(481);
|
||||
}
|
||||
#endif
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
break;
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_DISKIO
|
||||
case 'D':
|
||||
case 'd':
|
||||
FF_IF_MODULE_MATCH(ffDiskIOModuleInfo.name) {
|
||||
[[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options;
|
||||
ffInitDiskIOOptions(&options);
|
||||
initStructureModuleOptions(data, &ffDiskIOModuleInfo, &options);
|
||||
ffPrepareDiskIO(&options);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_NETIO
|
||||
#if !FF_MODULE_DISABLE_NETIO
|
||||
case 'N':
|
||||
case 'n':
|
||||
FF_IF_MODULE_MATCH(ffNetIOModuleInfo.name) {
|
||||
[[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options;
|
||||
ffInitNetIOOptions(&options);
|
||||
initStructureModuleOptions(data, &ffNetIOModuleInfo, &options);
|
||||
ffPrepareNetIO(&options);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_PUBLICIP
|
||||
#if !FF_MODULE_DISABLE_PUBLICIP
|
||||
case 'P':
|
||||
case 'p':
|
||||
FF_IF_MODULE_MATCH(ffPublicIPModuleInfo.name) {
|
||||
[[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options;
|
||||
ffInitPublicIpOptions(&options);
|
||||
initStructureModuleOptions(data, &ffPublicIPModuleInfo, &options);
|
||||
ffPreparePublicIp(&options);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_TOP
|
||||
#if !FF_MODULE_DISABLE_TOP
|
||||
case 'T':
|
||||
case 't':
|
||||
FF_IF_MODULE_MATCH(ffTopModuleInfo.name) {
|
||||
[[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options;
|
||||
ffInitTopOptions(&options);
|
||||
initStructureModuleOptions(data, &ffTopModuleInfo, &options);
|
||||
ffPrepareTopProcesses(options.showTypes);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_WEATHER
|
||||
#if !FF_MODULE_DISABLE_WEATHER
|
||||
case 'W':
|
||||
case 'w':
|
||||
FF_IF_MODULE_MATCH(ffWeatherModuleInfo.name) {
|
||||
[[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options;
|
||||
ffInitWeatherOptions(&options);
|
||||
initStructureModuleOptions(data, &ffWeatherModuleInfo, &options);
|
||||
ffPrepareWeather(&options);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#undef FF_IF_MODULE_MATCH
|
||||
@@ -178,6 +211,34 @@ static void genJsonResult(FFdata* data, FFModuleBaseInfo* baseInfo, void* option
|
||||
}
|
||||
}
|
||||
|
||||
static yyjson_val* findStructureModuleConfig(FFdata* data, const char* moduleType) {
|
||||
if (data->configDoc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
yyjson_val* root = yyjson_doc_get_root(data->configDoc);
|
||||
if (root == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
yyjson_val* modules = yyjson_obj_get(root, "modules");
|
||||
if (!yyjson_is_arr(modules)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
yyjson_val* item;
|
||||
size_t idx, max;
|
||||
yyjson_arr_foreach (modules, idx, max, item) {
|
||||
if (yyjson_is_obj(item)) {
|
||||
const char* type = yyjson_get_str(yyjson_obj_get(item, "type"));
|
||||
if (type != nullptr && ffStrEqualsIgnCase(type, moduleType)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool parseStructureCommand(
|
||||
FFdata* data,
|
||||
const char* line,
|
||||
@@ -187,7 +248,7 @@ static bool parseStructureCommand(
|
||||
FFModuleBaseInfo* baseInfo = *modules;
|
||||
if (ffStrEqualsIgnCase(line, baseInfo->name)) {
|
||||
alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE];
|
||||
baseInfo->initOptions(optionBuf);
|
||||
initStructureModuleOptions(data, baseInfo, optionBuf);
|
||||
if (data->resultDoc != nullptr) {
|
||||
fn(data, baseInfo, optionBuf);
|
||||
} else {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,50 @@
|
||||
#include "common/debug.h"
|
||||
#include "common/windows/nt.h"
|
||||
#include <errno.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <cfgmgr32.h>
|
||||
#ifndef NDEBUG
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/thread.h"
|
||||
|
||||
[[gnu::nonnull(1), gnu::pure, gnu::always_inline, nodiscard]]
|
||||
static inline const char* ffFindFileName(const char* file) {
|
||||
const char* lastSlash = __builtin_strrchr(file, '/');
|
||||
#ifdef _WIN32
|
||||
if (lastSlash == nullptr) {
|
||||
lastSlash = __builtin_strrchr(file, '\\');
|
||||
}
|
||||
#endif
|
||||
if (lastSlash != nullptr) {
|
||||
return lastSlash + 1;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
#if __GNUC__ && !__clang__
|
||||
[[gnu::optimize("O2")]]
|
||||
#endif
|
||||
void ffDebugPrint(const char* file, int line, const char* format, ...) {
|
||||
if (!instance.config.display.debugMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER;
|
||||
ffThreadMutexLock(&debugMutex);
|
||||
int errno_ = errno;
|
||||
static char errmsg_[1024];
|
||||
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_);
|
||||
errno = errno_;
|
||||
ffThreadMutexUnlock(&debugMutex);
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cfgmgr32.h>
|
||||
|
||||
const char* ffDebugWin32Error(DWORD errorCode) {
|
||||
static char buffer[512];
|
||||
@@ -58,3 +100,6 @@ static inline DWORD HRESULTToWin32Error(HRESULT hr) {
|
||||
const char* ffDebugHResult(HRESULT hr) {
|
||||
return ffDebugWin32Error(HRESULTToWin32Error(hr));
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+77
-205
@@ -147,11 +147,26 @@ static inline bool formatArgSet(const FFformatarg* arg) {
|
||||
static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) {
|
||||
const char* err = lua_tolstring(L, -1, nullptr);
|
||||
if (err) {
|
||||
const char* tmp = strchr(err, ':');
|
||||
if (tmp) {
|
||||
err = tmp + 1;
|
||||
while (*err == ' ') {
|
||||
++err;
|
||||
// Drop the position Lua puts in front of an error and keep the message only. Both places
|
||||
// that build one -- `luaG_addinfo` and `luaL_where` -- format it as `%s:%d: %s`, so a
|
||||
// position is a colon, a line number, a colon and a space:
|
||||
// general.preload.lua:1: <name> expected near '=' -> <name> expected near '='
|
||||
// [string ""]:1: x -> x
|
||||
// Not every error has one, and cutting at the first colon mangled those. `luaL_loadfilex`
|
||||
// reports `cannot open <path>: <strerror>`, where that colon is the one after the path, so
|
||||
// the path -- the only part naming the problem -- was dropped and `No such file or
|
||||
// directory` was left on its own. A colon followed by a line number does not identify a
|
||||
// position either, because a path can contain one: `cannot open /tmp/x:12:y/nope.lua: ...`
|
||||
// has to survive whole. The trailing space is what separates the two, and a real position
|
||||
// always has it.
|
||||
for (const char* colon = strchr(err, ':'); colon; colon = strchr(colon + 1, ':')) {
|
||||
const char* line = colon + 1;
|
||||
while (*line >= '0' && *line <= '9') {
|
||||
++line;
|
||||
}
|
||||
if (line > colon + 1 && line[0] == ':' && line[1] == ' ') {
|
||||
err = line + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +220,11 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
|
||||
lua_pushboolean(L, *(bool*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_STRING:
|
||||
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
|
||||
if (!arg->value) {
|
||||
lua_pushlstring(L, "", 0);
|
||||
} else {
|
||||
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
|
||||
}
|
||||
break;
|
||||
case FF_ARG_TYPE_STRBUF: {
|
||||
const FFstrbuf* sb = (const FFstrbuf*) arg->value;
|
||||
@@ -261,208 +280,67 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FF_HAVE_QUICKJS
|
||||
#include <quickjs.h>
|
||||
|
||||
struct FFQuickJSData {
|
||||
FF_LIBRARY_SYMBOL(JS_NewRuntime)
|
||||
FF_LIBRARY_SYMBOL(JS_NewContext)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeRuntime)
|
||||
FF_LIBRARY_SYMBOL(JS_EvalThis)
|
||||
FF_LIBRARY_SYMBOL(JS_GetException)
|
||||
FF_LIBRARY_SYMBOL(JS_ToCStringLen2)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeCString)
|
||||
FF_LIBRARY_SYMBOL(JS_NewStringLen)
|
||||
FF_LIBRARY_SYMBOL(JS_NewArray)
|
||||
FF_LIBRARY_SYMBOL(JS_NewBigUint64)
|
||||
FF_LIBRARY_SYMBOL(JS_SetPropertyUint32)
|
||||
FF_LIBRARY_SYMBOL(JS_NewObject)
|
||||
FF_LIBRARY_SYMBOL(JS_SetPropertyStr)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeValue)
|
||||
|
||||
JSRuntime* rt;
|
||||
JSContext* ctx;
|
||||
bool inited;
|
||||
} qjsData;
|
||||
|
||||
static const char* loadQuickJSState(void) {
|
||||
if (qjsData.inited) {
|
||||
if (qjsData.ctx == nullptr) {
|
||||
return "QuickJS is not available";
|
||||
}
|
||||
return nullptr;
|
||||
// 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) {
|
||||
if (str[0] != '\e' || str[1] != '[') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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";
|
||||
const char* p = str + 2;
|
||||
while (*p >= '0' && *p <= '?') {
|
||||
++p;
|
||||
}
|
||||
|
||||
qjsData.ctx = qjsData.ffJS_NewContext(qjsData.rt);
|
||||
if (qjsData.ctx == nullptr) {
|
||||
qjsData.ffJS_FreeRuntime(qjsData.rt);
|
||||
qjsData.rt = nullptr;
|
||||
return "JS_NewContext() failed";
|
||||
while (*p >= ' ' && *p <= '/') {
|
||||
++p;
|
||||
}
|
||||
|
||||
libqjs = nullptr; // don't close quickjs
|
||||
|
||||
return nullptr;
|
||||
if (*p < '@' || *p > '~') {
|
||||
return 0; // not a complete CSI sequence
|
||||
}
|
||||
return (uint32_t) (p - str) + 1;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
|
||||
if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) {
|
||||
// skip ANSI escape codes at the start of the string for truncation
|
||||
const char* p = in->chars + 2;
|
||||
while (*p && (ffCharIsDigit(*p) || *p == ';')) {
|
||||
++p;
|
||||
// Skip all ANSI escape codes at the start of the string, so that they don't count
|
||||
// towards the truncation / padding length. A value may carry more than one of them,
|
||||
// e.g. `\e[31m\e[1m`.
|
||||
uint32_t prefixLen = 0;
|
||||
while (in->length - prefixLen >= 2) {
|
||||
uint32_t len = ansiEscapeLength(in->chars + prefixLen);
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
if (*p && isascii(*p)) {
|
||||
++p;
|
||||
}
|
||||
uint32_t prefixLen = (uint32_t) (p - in->chars);
|
||||
ffStrbufAppendNS(out, prefixLen, in->chars);
|
||||
ffStrbufSubstrAfter(in, prefixLen - 1);
|
||||
|
||||
if (trailingEscape) {
|
||||
// likely have a `CSI m` reset at the end of the string
|
||||
uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e');
|
||||
if (iLastEscape != in->length) {
|
||||
ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape);
|
||||
ffStrbufSubstrBefore(in, iLastEscape);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
prefixLen += len;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (prefixLen == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ffStrbufAppendNS(out, prefixLen, in->chars);
|
||||
ffStrbufSubstrAfter(in, prefixLen - 1);
|
||||
|
||||
if (trailingEscape) {
|
||||
// likely have one or more `CSI m` resets at the end of the string
|
||||
uint32_t end = in->length;
|
||||
while (end >= 3) {
|
||||
uint32_t start = end - 1;
|
||||
while (start > 0 && in->chars[start] != '\e') {
|
||||
--start;
|
||||
}
|
||||
uint32_t len = ansiEscapeLength(in->chars + start);
|
||||
if (len == 0 || start + len != end) {
|
||||
break;
|
||||
}
|
||||
end = start;
|
||||
}
|
||||
|
||||
if (end < in->length) {
|
||||
ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end);
|
||||
ffStrbufSubstrBefore(in, end);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
@@ -758,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
|
||||
|
||||
+97
-18
@@ -26,15 +26,16 @@
|
||||
// 0: title
|
||||
// 1: blank
|
||||
// 2: logo type row
|
||||
// 3: output mode row
|
||||
// 4: blank
|
||||
// 5: modules title row
|
||||
// 6: blank
|
||||
// 7..7+listRows-1: module grid
|
||||
// 7+listRows: description row
|
||||
// 8+listRows: help line 1
|
||||
// 9+listRows: help line 2
|
||||
#define FF_GEN_CONFIG_LIST_TOP 7
|
||||
// 3: logo position row
|
||||
// 4: output mode row
|
||||
// 5: blank
|
||||
// 6: modules title row
|
||||
// 7: blank
|
||||
// 8..8+listRows-1: module grid
|
||||
// 8+listRows: description row
|
||||
// 9+listRows: help line 1
|
||||
// 10+listRows: help line 2
|
||||
#define FF_GEN_CONFIG_LIST_TOP 8
|
||||
#define FF_GEN_CONFIG_BOTTOM_CHROME 3
|
||||
|
||||
// Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen
|
||||
@@ -66,6 +67,7 @@ typedef struct FFGenConfigUI {
|
||||
uint32_t cursor;
|
||||
uint32_t viewOffset;
|
||||
FFLogoType logoType;
|
||||
FFLogoPosition logoPosition;
|
||||
bool fullConfig;
|
||||
bool confirmingOverwrite;
|
||||
uint16_t rows;
|
||||
@@ -165,7 +167,7 @@ static void enterRawMode(void) {
|
||||
|
||||
static void getTerminalSize(uint16_t* rows, uint16_t* cols) {
|
||||
FFTerminalSizeResult size;
|
||||
if (ffDetectTerminalSize(&size) && size.rows > 0 && size.columns > 0) {
|
||||
if (ffDetectTerminalSize(&size, true) && size.rows > 0 && size.columns > 0) {
|
||||
*rows = size.rows;
|
||||
*cols = size.columns;
|
||||
} else {
|
||||
@@ -248,8 +250,8 @@ static void removeAllBreaksSeparators(FFGenConfigUI* ui) {
|
||||
|
||||
static void computeLayout(FFGenConfigUI* ui) {
|
||||
FFGenConfigLayout* layout = &ui->layout;
|
||||
if (ui->rows < 10) {
|
||||
ui->rows = 10;
|
||||
if (ui->rows < 11) {
|
||||
ui->rows = 11;
|
||||
}
|
||||
int32_t listRows = (int32_t) ui->rows - FF_GEN_CONFIG_LIST_TOP - FF_GEN_CONFIG_BOTTOM_CHROME;
|
||||
layout->listRows = listRows < 0 ? 0 : (uint16_t) listRows;
|
||||
@@ -367,6 +369,26 @@ static void cycleLogoType(FFGenConfigUI* ui, int delta) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cycleLogoPosition(FFGenConfigUI* ui, int delta) {
|
||||
static const FFLogoPosition positions[] = {
|
||||
FF_LOGO_POSITION_AUTO,
|
||||
FF_LOGO_POSITION_LEFT,
|
||||
FF_LOGO_POSITION_RIGHT,
|
||||
FF_LOGO_POSITION_TOP,
|
||||
};
|
||||
|
||||
uint32_t index = 0;
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(positions); ++i) {
|
||||
if (positions[i] == ui->logoPosition) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
index = (index + (delta > 0 ? 1 : ARRAY_SIZE(positions) - 1)) % ARRAY_SIZE(positions);
|
||||
ui->logoPosition = positions[index];
|
||||
}
|
||||
|
||||
static uint32_t countSelectedModules(const FFlist* items) {
|
||||
uint32_t count = 0;
|
||||
FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
|
||||
@@ -558,7 +580,46 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 3: output mode
|
||||
// Row 3: logo position
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD "m");
|
||||
rowAppendVisual(&row, "Logo ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_UNDERLINE_ "m");
|
||||
rowAppendVisual(&row, "p");
|
||||
rowAppendRaw(&row, "\e[24m"); // Reset underline
|
||||
rowAppendVisual(&row, "osition:");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "auto", ui->logoPosition == FF_LOGO_POSITION_AUTO);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "left", ui->logoPosition == FF_LOGO_POSITION_LEFT);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "right", ui->logoPosition == FF_LOGO_POSITION_RIGHT);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "top", ui->logoPosition == FF_LOGO_POSITION_TOP);
|
||||
if (cols >= 120) {
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
|
||||
switch (ui->logoPosition) {
|
||||
case FF_LOGO_POSITION_AUTO:
|
||||
rowAppendVisual(&row, " - Choose left or top from terminal width");
|
||||
break;
|
||||
case FF_LOGO_POSITION_RIGHT:
|
||||
rowAppendVisual(&row, " - Place the logo to the right");
|
||||
break;
|
||||
case FF_LOGO_POSITION_TOP:
|
||||
rowAppendVisual(&row, " - Place the logo above module output");
|
||||
break;
|
||||
case FF_LOGO_POSITION_LEFT:
|
||||
default:
|
||||
rowAppendVisual(&row, " - Place the logo to the left");
|
||||
break;
|
||||
}
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 4: output mode
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
|
||||
@@ -581,11 +642,11 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 4: blank
|
||||
// Row 5: blank
|
||||
rowInit(&row, cols);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 5: modules title
|
||||
// Row 6: modules title
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
|
||||
@@ -596,7 +657,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 6: blank
|
||||
// Row 7: blank
|
||||
rowInit(&row, cols);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
@@ -618,7 +679,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
}
|
||||
|
||||
// Row after grid: description
|
||||
// Row 9+listRows: description
|
||||
rowInit(&row, cols);
|
||||
if (ui->cursor < ui->items.length) {
|
||||
const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
|
||||
@@ -650,7 +711,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
|
||||
rowInit(&row, cols);
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
|
||||
rowAppendVisual(&row, " l/L logo o minimal/full s/Enter save q/Esc quit g/G top/bottom");
|
||||
rowAppendVisual(&row, " l/L logo p/P position o minimal/full s/Enter save q/Esc quit g/G top/bottom");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
@@ -904,6 +965,10 @@ static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileEx
|
||||
cycleLogoType(ui, 1);
|
||||
} else if (ch == 'L') {
|
||||
cycleLogoType(ui, -1);
|
||||
} else if (ch == 'p') {
|
||||
cycleLogoPosition(ui, 1);
|
||||
} else if (ch == 'P') {
|
||||
cycleLogoPosition(ui, -1);
|
||||
} else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') {
|
||||
return 0;
|
||||
}
|
||||
@@ -940,6 +1005,18 @@ static FFLogoType initialLogoType(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static FFLogoPosition initialLogoPosition(void) {
|
||||
switch (instance.config.logo.position) {
|
||||
case FF_LOGO_POSITION_TOP:
|
||||
case FF_LOGO_POSITION_RIGHT:
|
||||
case FF_LOGO_POSITION_AUTO:
|
||||
return instance.config.logo.position;
|
||||
case FF_LOGO_POSITION_LEFT:
|
||||
default:
|
||||
return FF_LOGO_POSITION_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
static bool applyConfig(FFdata* data, const FFlist* items, bool fullConfig) {
|
||||
if (data->resultDoc) {
|
||||
fputs("Error: duplicated `--gen-config` or `--format json` flags found\n", stderr);
|
||||
@@ -972,6 +1049,7 @@ bool ffGenConfigInteractive(FFdata* data) {
|
||||
.cursor = 0,
|
||||
.viewOffset = 0,
|
||||
.logoType = initialLogoType(),
|
||||
.logoPosition = initialLogoPosition(),
|
||||
.fullConfig = false,
|
||||
.confirmingOverwrite = false,
|
||||
.rows = 24,
|
||||
@@ -993,6 +1071,7 @@ bool ffGenConfigInteractive(FFdata* data) {
|
||||
if (result == 1) {
|
||||
if (applyConfig(data, &ui.items, ui.fullConfig)) {
|
||||
instance.config.logo.type = ui.logoType;
|
||||
instance.config.logo.position = ui.logoPosition;
|
||||
success = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
+3
-12
@@ -178,9 +178,6 @@ void ffDestroyInstance(void) {
|
||||
#if FF_HAVE_LUA
|
||||
#include <lua.h>
|
||||
#endif
|
||||
#if FF_HAVE_QUICKJS
|
||||
#include <quickjs.h>
|
||||
#endif
|
||||
|
||||
// Must be in a file compiled with the libfastfetch target, because the FF_HAVE* macros are not defined for the executable targets
|
||||
void ffListFeatures(void) {
|
||||
@@ -248,12 +245,6 @@ void ffListFeatures(void) {
|
||||
#if FF_HAVE_FREETYPE
|
||||
"freetype\n"
|
||||
#endif
|
||||
#if FF_HAVE_PULSE
|
||||
"libpulse\n"
|
||||
#endif
|
||||
#if FF_HAVE_DDCUTIL
|
||||
"libddcutil\n"
|
||||
#endif
|
||||
#if FF_HAVE_ELF || __sun || (__FreeBSD__ && !__DragonFly__) || __OpenBSD__ || __NetBSD__
|
||||
"libelf\n"
|
||||
#endif
|
||||
@@ -281,6 +272,9 @@ void ffListFeatures(void) {
|
||||
#if FF_ENABLE_WCWIDTH
|
||||
"Embedded wcwidth\n"
|
||||
#endif
|
||||
#if FF_HAVE_SIXEL
|
||||
"Embedded sixel\n"
|
||||
#endif
|
||||
#if FF_HAVE_WINRT
|
||||
"WinRT headers\n"
|
||||
#endif
|
||||
@@ -292,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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "common/io.h"
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/time.h"
|
||||
|
||||
@@ -11,6 +12,7 @@
|
||||
#include <poll.h>
|
||||
#else
|
||||
#include <sys/select.h>
|
||||
#define st_mtim st_mtimespec // `struct stat` spells it `st_mtimespec` on Apple
|
||||
#endif
|
||||
|
||||
#if FF_HAVE_WORDEXP
|
||||
@@ -40,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;
|
||||
}
|
||||
}
|
||||
@@ -145,16 +149,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);
|
||||
@@ -164,7 +171,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
|
||||
@@ -174,7 +184,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";
|
||||
}
|
||||
}
|
||||
@@ -190,6 +203,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";
|
||||
}
|
||||
@@ -203,6 +218,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";
|
||||
}
|
||||
@@ -328,6 +344,25 @@ FFNativeFD ffGetNullFD(void) {
|
||||
return hNullFile;
|
||||
}
|
||||
|
||||
bool ffIsTerminal(int fd) {
|
||||
return isatty(fd) != 0;
|
||||
}
|
||||
|
||||
bool ffRemoveFile(const char* fileName) {
|
||||
return unlink(fileName) == 0;
|
||||
}
|
||||
|
||||
uint64_t ffPathGetMtime(const char* path) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// An mtime at or before the Unix epoch means the filesystem did not fill it in. Reporting it
|
||||
// as-is would hand the caller a value indistinguishable from "unknown", so treat it as such.
|
||||
if (st.st_mtim.tv_sec <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (uint64_t) st.st_mtim.tv_sec * 1000ull + (uint64_t) st.st_mtim.tv_nsec / 1000000ull;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/time.h"
|
||||
#include "common/windows/nt.h"
|
||||
#include "common/windows/unicode.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// TODO: long paths on Windows.
|
||||
//
|
||||
// Every path that crosses into the wide API from this file is converted into a fixed
|
||||
// `wchar_t[MAX_PATH]` buffer, and `RtlUTF8ToUnicodeN` refuses to fill it for a path of MAX_PATH
|
||||
// characters or more (STATUS_BUFFER_TOO_SMALL) -- which makes each of those call sites a silent
|
||||
// failure above that length rather than an error anyone sees. Raising a buffer on its own does not
|
||||
// help: the wide APIs accept a longer path only either with a `\\?\` prefix on every absolute path
|
||||
// (which also switches off the normalization the rest of the code relies on) or with a
|
||||
// `longPathAware` manifest plus the LongPathsEnabled registry value.
|
||||
//
|
||||
// The plan is the second route: define MAX_PATH as 32768 program-wide, add the manifest flag, and
|
||||
// size these buffers from the UTF-8 input rather than from the constant. It has to be done in one
|
||||
// pass, which is why it is recorded here instead of being applied at the call sites the review
|
||||
// happened to look at.
|
||||
static bool createSubfolders(wchar_t* fileName) {
|
||||
HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle;
|
||||
bool closeRoot = false;
|
||||
@@ -159,7 +175,9 @@ bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE) {
|
||||
if (GetLastError() == ERROR_PATH_NOT_FOUND) {
|
||||
DWORD errorCode = GetLastError();
|
||||
FF_DEBUG("Failed to open file: %s - %s", fileName, ffDebugWin32Error(errorCode));
|
||||
if (errorCode == ERROR_PATH_NOT_FOUND) {
|
||||
if (!createSubfolders(fileNameW)) {
|
||||
return false;
|
||||
}
|
||||
@@ -344,8 +362,18 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
}
|
||||
|
||||
ffStrbufAppendC(folder, '*');
|
||||
WIN32_FIND_DATAA entry;
|
||||
HANDLE hFind = FindFirstFileA(folder->chars, &entry);
|
||||
// The wide API on purpose: every path fastfetch holds is UTF-8, and FindFirstFileA would read
|
||||
// it in the active code page, so a directory whose name the code page cannot represent -- a
|
||||
// Chinese or Japanese name under an English or German locale, for one -- answers
|
||||
// ERROR_PATH_NOT_FOUND and the whole listing disappears. The names it returns are converted
|
||||
// back to UTF-8 below.
|
||||
wchar_t wideFolder[PATH_MAX];
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wideFolder, sizeof(wideFolder), nullptr, folder->chars, folder->length + 1))) {
|
||||
ffStrbufTrimRight(folder, '*');
|
||||
return;
|
||||
}
|
||||
WIN32_FIND_DATAW entry;
|
||||
HANDLE hFind = FindFirstFileW(wideFolder, &entry);
|
||||
ffStrbufTrimRight(folder, '*');
|
||||
if (hFind == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
@@ -353,14 +381,15 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
|
||||
do {
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
if (ffStrEquals(entry.cFileName, ".") || ffStrEquals(entry.cFileName, "..")) {
|
||||
if (entry.cFileName[0] == L'.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
ffStrbufSubstrBefore(folder, folderLength);
|
||||
ffStrbufAppendS(folder, entry.cFileName);
|
||||
ffStrbufAppendWS(folder, entry.cFileName);
|
||||
ffStrbufAppendC(folder, '/');
|
||||
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry.cFileName, pretty);
|
||||
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
|
||||
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), name.chars, pretty);
|
||||
ffStrbufSubstrBefore(folder, folderLength);
|
||||
continue;
|
||||
}
|
||||
@@ -373,8 +402,9 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
fputs(folder->chars + baseLength, stdout);
|
||||
}
|
||||
|
||||
puts(entry.cFileName);
|
||||
} while (FindNextFileA(hFind, &entry));
|
||||
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
|
||||
ffStrbufPutTo(&name, stdout);
|
||||
} while (FindNextFileW(hFind, &entry));
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
@@ -411,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";
|
||||
}
|
||||
@@ -441,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";
|
||||
}
|
||||
@@ -460,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) {
|
||||
@@ -496,6 +531,47 @@ FFNativeFD ffGetNullFD(void) {
|
||||
return hNullFile;
|
||||
}
|
||||
|
||||
bool ffRemoveFile(const char* fileName) {
|
||||
return DeleteFileA(fileName) != FALSE;
|
||||
bool ffIsTerminal(int fd) {
|
||||
intptr_t handle = _get_osfhandle(fd);
|
||||
if (handle == -1 || handle == -2) {
|
||||
// -1: an invalid fd; -2: a valid fd with no file behind it (`_get_osfhandle`'s way of
|
||||
// saying FF_INVALID_FD). Neither is a terminal.
|
||||
return false;
|
||||
}
|
||||
DWORD mode;
|
||||
return GetConsoleMode((HANDLE) handle, &mode) != 0;
|
||||
}
|
||||
|
||||
bool ffRemoveFile(const char* fileName) {
|
||||
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
|
||||
ULONG len;
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = DeleteFileW(fileNameW) != FALSE;
|
||||
FF_DEBUG("Deleting file: %s - %s", fileName, ret ? "Success" : ffDebugWin32Error(GetLastError()));
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t ffPathGetMtime(const char* path) {
|
||||
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
|
||||
ULONG len;
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, path, (ULONG) strlen(path) + 1))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE handle = CreateFileW(fileNameW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
|
||||
if (handle == INVALID_HANDLE_VALUE) { // file doesn't exist or isn't accessible
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE_BASIC_INFORMATION fileInfo;
|
||||
IO_STATUS_BLOCK iosb;
|
||||
if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileBasicInformation))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ffFileTimeToUnixMs((uint64_t) fileInfo.LastWriteTime.QuadPart);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -60,6 +61,22 @@ void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* m
|
||||
}
|
||||
}
|
||||
|
||||
bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max) {
|
||||
if (!yyjson_is_int(val)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `yyjson_get_sint()` is exact for every value that fits into `int64_t`, and is negative for the
|
||||
// `uint64_t` values above `INT64_MAX`, which no caller accepts anyway.
|
||||
int64_t num = unsafe_yyjson_get_sint(val);
|
||||
if (num < 0 || (uint64_t) num > max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = (uint32_t) num;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
|
||||
if (yyjson_is_int(val)) {
|
||||
int intVal = yyjson_get_int(val);
|
||||
@@ -71,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);
|
||||
@@ -81,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";
|
||||
}
|
||||
}
|
||||
@@ -133,7 +154,10 @@ static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_
|
||||
|
||||
static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
|
||||
switch (type[0]) {
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
// Both `cpuusage` and `command` start with a c, and they are disabled by separate options,
|
||||
// so the block has to survive either one being off -- otherwise a build with only the other
|
||||
// one enabled would never prepare this module (see the same guard in `commandoption.c`).
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
case 'c':
|
||||
case 'C': {
|
||||
if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
|
||||
@@ -254,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";
|
||||
}
|
||||
|
||||
@@ -262,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";
|
||||
}
|
||||
|
||||
@@ -276,6 +302,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
}
|
||||
|
||||
yyjson_val* module = item;
|
||||
bool gatedOnSucceeded = false;
|
||||
const char* type = yyjson_get_str(module);
|
||||
if (type) {
|
||||
module = nullptr;
|
||||
@@ -283,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";
|
||||
}
|
||||
|
||||
@@ -309,8 +337,43 @@ 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;
|
||||
|
||||
if (prepare) {
|
||||
// Whether this module is printed at all depends on the result of the previous
|
||||
// one, which is only known in the print pass. Preparing it here would leave a
|
||||
// result behind that no print pass ever consumes, and since the modules are
|
||||
// paired by position (see `ffPrepareCommand` / `ffDetectCommand`), that
|
||||
// would silently shift every later module onto the wrong result.
|
||||
// Skip it, and let the module report the missing preparation if it turns out
|
||||
// to be printed after all.
|
||||
continue;
|
||||
}
|
||||
|
||||
#if !FF_MODULE_DISABLE_COMMAND
|
||||
// A `command` module that runs in parallel is exactly the pairing above, so this
|
||||
// combination cannot be left to the print pass: the module would be handed the
|
||||
// result prepared for the next `command` module and print that instead of its
|
||||
// own output. Refused here rather than in the prepare pass, because returning
|
||||
// from there would leave every later module unprepared -- which the print pass
|
||||
// would then report as a failure of theirs.
|
||||
// `parallel` is on by default, so leaving the key out is the parallel case too.
|
||||
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 "
|
||||
"\"parallel\": false on it, or drop the condition.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
|
||||
continue;
|
||||
}
|
||||
@@ -319,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";
|
||||
}
|
||||
|
||||
@@ -365,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);
|
||||
|
||||
@@ -161,8 +161,8 @@ void* dlsym(void* handle, const char* symbol) {
|
||||
}
|
||||
|
||||
void* ffLibraryGetModule(const wchar_t* libraryFileName) {
|
||||
assert(libraryFileName != nullptr && "Use \"ffGetPeb()->ImageBaseAddress\" instead");
|
||||
|
||||
// `libraryFileName` is non-null per the `nonnull(1)` contract.
|
||||
// Prefer `ffGetPeb()->ImageBaseAddress` over looking the main module up by name.
|
||||
void* module = nullptr;
|
||||
USHORT libraryFileNameBytes = (USHORT) (wcslen(libraryFileName) * sizeof(wchar_t) + sizeof(wchar_t));
|
||||
NTSTATUS status = LdrGetDllHandle(nullptr, nullptr, &(UNICODE_STRING) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+39
-14
@@ -1,22 +1,47 @@
|
||||
#include "common/netif.h"
|
||||
#include "common/FFcache.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
|
||||
// netio, localip and dns resolve the default route on every round, and it can change while a
|
||||
// `--dynamic-interval` run is going on: a laptop switching from Wi-Fi to Ethernet, a VPN coming up.
|
||||
static FFNetifDefaultRouteResult defaultRouteV4;
|
||||
static FFNetifDefaultRouteResult defaultRouteV6;
|
||||
|
||||
static void resetDefaultRoute(void* storage) {
|
||||
// The impls only write the fields they found, so without this a round that finds no default
|
||||
// route would keep reporting the interface of the previous one.
|
||||
memset(storage, 0, sizeof(FFNetifDefaultRouteResult));
|
||||
}
|
||||
|
||||
static void initDefaultRouteV4(void* storage) {
|
||||
FFNetifDefaultRouteResult* result = storage;
|
||||
result->status = ffNetifGetDefaultRouteImplV4(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
|
||||
static void initDefaultRouteV6(void* storage) {
|
||||
FFNetifDefaultRouteResult* result = storage;
|
||||
result->status = ffNetifGetDefaultRouteImplV6(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
|
||||
static FFcacheEntry ffCacheEntryDefaultRouteV4 = {
|
||||
.name = "default route V4",
|
||||
.storage = &defaultRouteV4,
|
||||
.init = initDefaultRouteV4,
|
||||
.destroy = resetDefaultRoute,
|
||||
};
|
||||
|
||||
static FFcacheEntry ffCacheEntryDefaultRouteV6 = {
|
||||
.name = "default route V6",
|
||||
.storage = &defaultRouteV6,
|
||||
.init = initDefaultRouteV6,
|
||||
.destroy = resetDefaultRoute,
|
||||
};
|
||||
|
||||
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) {
|
||||
static FFNetifDefaultRouteResult result;
|
||||
if (result.status == FF_NETIF_UNINITIALIZED) {
|
||||
result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
return &result;
|
||||
return ffCacheGet(&ffCacheEntryDefaultRouteV4);
|
||||
}
|
||||
|
||||
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) {
|
||||
static FFNetifDefaultRouteResult result;
|
||||
if (result.status == FF_NETIF_UNINITIALIZED) {
|
||||
result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
return &result;
|
||||
return ffCacheGet(&ffCacheEntryDefaultRouteV6);
|
||||
}
|
||||
|
||||
+200
-175
@@ -1,192 +1,217 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/library.h"
|
||||
#include "common/networking.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include <zlib.h>
|
||||
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;
|
||||
|
||||
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;
|
||||
while (pos < headerEnd) {
|
||||
uint32_t eol = pos;
|
||||
while (eol < headerEnd && headers[eol] != '\n') {
|
||||
++eol;
|
||||
}
|
||||
}
|
||||
uint32_t lineEnd = (eol > pos && headers[eol - 1] == '\r') ? (eol - 1) : eol;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Only match at the beginning of a line, so that a value can never be mistaken
|
||||
// for a field name (obs-fold continuation lines included)
|
||||
if (lineEnd - pos >= nameLen && strncasecmp(headers + pos, name, nameLen) == 0) {
|
||||
uint32_t valueStart = pos + nameLen;
|
||||
while (valueStart < lineEnd && (headers[valueStart] == ' ' || headers[valueStart] == '\t')) {
|
||||
++valueStart;
|
||||
}
|
||||
*valueLen = lineEnd - valueStart;
|
||||
return headers + valueStart;
|
||||
}
|
||||
|
||||
// Decompress gzip content
|
||||
bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) {
|
||||
assert(headerEnd != nullptr && *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);
|
||||
if (eol >= headerEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
ffStrbufAppendS(&newBuffer, line); // Including the trailing \r
|
||||
ffStrbufAppendC(&newBuffer, '\n');
|
||||
pos = eol + 1;
|
||||
}
|
||||
|
||||
ffStrbufAppend(&newBuffer, &decompressedBuffer);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen) {
|
||||
// The value is a comma-separated list of transfer codings (RFC 9112 6.1)
|
||||
uint32_t codingCount = 0;
|
||||
const char* coding = nullptr;
|
||||
uint32_t codingLen = 0;
|
||||
|
||||
for (uint32_t i = 0, start = 0; i <= valueLen; ++i) {
|
||||
if (i < valueLen && value[i] != ',') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// trim the optional whitespace around the coding
|
||||
uint32_t from = start;
|
||||
uint32_t to = i;
|
||||
while (from < to && (value[from] == ' ' || value[from] == '\t')) {
|
||||
++from;
|
||||
}
|
||||
while (to > from && (value[to - 1] == ' ' || value[to - 1] == '\t')) {
|
||||
--to;
|
||||
}
|
||||
|
||||
if (to > from) {
|
||||
++codingCount;
|
||||
coding = value + from;
|
||||
codingLen = to - from;
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
|
||||
if (codingCount == 0) {
|
||||
return FF_NETWORKING_TE_NONE;
|
||||
}
|
||||
|
||||
if (codingCount == 1 && codingLen == 7 && strncasecmp(coding, "chunked", 7) == 0) {
|
||||
return FF_NETWORKING_TE_CHUNKED;
|
||||
}
|
||||
|
||||
return FF_NETWORKING_TE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* consumed) {
|
||||
uint32_t pos = 0;
|
||||
|
||||
for (;;) {
|
||||
// chunk-size [ chunk-ext ] CRLF
|
||||
uint32_t eol = pos;
|
||||
while (eol < bodyLen && body[eol] != '\n') {
|
||||
++eol;
|
||||
}
|
||||
if (eol >= bodyLen) {
|
||||
return 0; // the chunk-size line is not complete yet
|
||||
}
|
||||
|
||||
if (eol == pos || !isxdigit((unsigned char) body[pos])) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* stop = nullptr;
|
||||
unsigned long size = strtoul(body + pos, &stop, 16);
|
||||
if (stop == body + pos) {
|
||||
return -1;
|
||||
}
|
||||
pos = eol + 1; // skips the chunk extension, which ends at the CRLF
|
||||
|
||||
if (size == 0) {
|
||||
// last-chunk, followed by an optional trailer section and an empty line
|
||||
uint32_t p = pos;
|
||||
while (p < bodyLen) {
|
||||
uint32_t e = p;
|
||||
while (e < bodyLen && body[e] != '\n') {
|
||||
++e;
|
||||
}
|
||||
if (e >= bodyLen) {
|
||||
return 0;
|
||||
}
|
||||
if (e == p || (e == p + 1 && body[p] == '\r')) {
|
||||
*consumed = e + 1;
|
||||
return 1;
|
||||
}
|
||||
p = e + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (size > (unsigned long) (bodyLen - pos)) {
|
||||
return 0; // the chunk data is not complete yet
|
||||
}
|
||||
pos += (uint32_t) size;
|
||||
|
||||
if (bodyLen - pos < 2) {
|
||||
return 0;
|
||||
}
|
||||
if (body[pos] != '\r' || body[pos + 1] != '\n') {
|
||||
return -1;
|
||||
}
|
||||
pos += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Keeps the status line and every header except `dropHeader` and `Content-Length`,
|
||||
// then emits a `Content-Length` matching the (already decoded) body.
|
||||
// `body` may point into `buffer->chars`; it is copied before `buffer` is released.
|
||||
//
|
||||
// Returns the offset of the `\r` that opens the terminating CRLF CRLF of the new response. The
|
||||
// rebuild drops a header line and rewrites another, so the header it produces is shorter than the
|
||||
// one that went in: the offset the caller held before is no longer the one that describes this
|
||||
// buffer, and handing it on would point into the middle of the body.
|
||||
static uint32_t rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* body, uint32_t bodyLen, const char* dropHeader) {
|
||||
FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerEnd + bodyLen + 64);
|
||||
|
||||
uint32_t pos = 0;
|
||||
while (pos < headerEnd) {
|
||||
uint32_t eol = pos;
|
||||
while (eol < headerEnd && buffer->chars[eol] != '\n') {
|
||||
++eol;
|
||||
}
|
||||
uint32_t lineLen = (eol < headerEnd) ? (eol - pos + 1) : (headerEnd - pos);
|
||||
|
||||
if (!ffStrStartsWithIgnCase(buffer->chars + pos, "Content-Length:") &&
|
||||
!ffStrStartsWithIgnCase(buffer->chars + pos, dropHeader)) {
|
||||
ffStrbufAppendNS(&newBuffer, lineLen, buffer->chars + pos);
|
||||
}
|
||||
|
||||
pos += lineLen;
|
||||
}
|
||||
|
||||
ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n\r\n", bodyLen);
|
||||
ffStrbufAppendNS(&newBuffer, bodyLen, body);
|
||||
|
||||
ffStrbufDestroy(buffer);
|
||||
ffStrbufInitMove(buffer, &newBuffer);
|
||||
|
||||
// The terminating CRLF CRLF is the four bytes in front of the body
|
||||
return buffer->length - bodyLen - 4;
|
||||
}
|
||||
|
||||
bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd) {
|
||||
assert(buffer->allocated > 0);
|
||||
|
||||
// Held in a local because the rebuild below hands a different value back
|
||||
const uint32_t headerLength = *headerEnd;
|
||||
|
||||
// The header block is terminated by CR LF CR LF
|
||||
if (headerLength + 4 > buffer->length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* body = buffer->chars + headerLength + 4;
|
||||
uint32_t bodyLen = buffer->length - headerLength - 4;
|
||||
|
||||
uint32_t consumed = 0;
|
||||
if (ffNetworkingChunkedComplete(body, bodyLen, &consumed) != 1) {
|
||||
FF_DEBUG("Incomplete or malformed chunked body");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decoding in place is safe: the encoded form is never shorter than the payload
|
||||
char* out = body;
|
||||
uint32_t outLen = 0;
|
||||
uint32_t pos = 0;
|
||||
|
||||
while (pos < bodyLen) {
|
||||
uint32_t eol = pos;
|
||||
while (eol < bodyLen && body[eol] != '\n') {
|
||||
++eol;
|
||||
}
|
||||
|
||||
unsigned long size = strtoul(body + pos, nullptr, 16);
|
||||
pos = eol + 1;
|
||||
if (size == 0) {
|
||||
break; // last-chunk; trailers are dropped
|
||||
}
|
||||
|
||||
memmove(out + outLen, body + pos, size);
|
||||
outLen += (uint32_t) size;
|
||||
pos += (uint32_t) size + 2; // trailing CRLF
|
||||
}
|
||||
|
||||
FF_DEBUG("Decoded chunked body: %u bytes encoded, %u bytes decoded", bodyLen, outLen);
|
||||
*headerEnd = rebuildResponse(buffer, headerLength, out, outLen, "Transfer-Encoding:");
|
||||
return true;
|
||||
}
|
||||
#endif // FF_HAVE_ZLIB
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
// Upper bound of a single HTTP response, guarding against excessive memory allocation
|
||||
#define FF_NETWORKING_MAX_RESPONSE_SIZE (1024u * 1024u)
|
||||
|
||||
static const char* tryNonThreadingFastPath(FFNetworkingState* state) {
|
||||
#if defined(TCP_FASTOPEN) || __APPLE__
|
||||
|
||||
@@ -143,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
|
||||
}
|
||||
@@ -187,7 +191,7 @@ exit:
|
||||
FF_THREAD_ENTRY_DECL_WRAPPER(connectAndSend, FFNetworkingState*);
|
||||
|
||||
// Parallel DNS resolution and socket creation
|
||||
static const char* initNetworkingState(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
|
||||
static const char* initNetworkingState(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
|
||||
FF_DEBUG("Initializing network connection state: host=%s, path=%s", host, path);
|
||||
|
||||
// Initialize command and host information
|
||||
@@ -195,14 +199,20 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos
|
||||
ffStrbufAppendS(&state->command, "GET ");
|
||||
ffStrbufAppendS(&state->command, path);
|
||||
ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
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");
|
||||
if (strchr(host, ':') != nullptr) {
|
||||
// An IPv6 literal has to be bracketed in the Host header (RFC 9110 7.2), while
|
||||
// getaddrinfo() wants it bare
|
||||
ffStrbufAppendC(&state->command, '[');
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
ffStrbufAppendC(&state->command, ']');
|
||||
} else {
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
}
|
||||
// The Host header carries the port whenever it is not the default one (RFC 9110 7.2)
|
||||
if (port != 80) {
|
||||
ffStrbufAppendF(&state->command, ":%u", port);
|
||||
}
|
||||
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection
|
||||
|
||||
ffStrbufAppendS(&state->command, headers);
|
||||
ffStrbufAppendS(&state->command, "\r\n");
|
||||
@@ -220,10 +230,13 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos
|
||||
.ai_flags = AI_NUMERICSERV
|
||||
};
|
||||
|
||||
FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4");
|
||||
char portA[6];
|
||||
snprintf(portA, sizeof(portA), "%u", port);
|
||||
|
||||
FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
|
||||
// Use AI_NUMERICSERV flag to indicate the service is a numeric port, reducing parsing time
|
||||
|
||||
int gaiRes = getaddrinfo(host, "80", &hints, &state->addr);
|
||||
int gaiRes = getaddrinfo(host, portA, &hints, &state->addr);
|
||||
if (gaiRes != 0) {
|
||||
FF_DEBUG("getaddrinfo() failed: %s (res=%d)", gai_strerror(gaiRes), gaiRes);
|
||||
ret = "getaddrinfo() failed";
|
||||
@@ -259,6 +272,15 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SO_NOSIGPIPE
|
||||
// Prevent SIGPIPE when the server closes the connection during write
|
||||
if (setsockopt(state->sockfd, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(flag)) != 0) {
|
||||
FF_DEBUG("Failed to set SO_NOSIGPIPE: %s", strerror(errno));
|
||||
} else {
|
||||
FF_DEBUG("Successfully set SO_NOSIGPIPE");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (state->timeout > 0) {
|
||||
FF_DEBUG("Setting connection timeout: %u ms", state->timeout);
|
||||
[[maybe_unused]] uint32_t sec = state->timeout / 1000;
|
||||
@@ -295,33 +317,15 @@ error:
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
}
|
||||
|
||||
ffStrbufClear(&state->command);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
|
||||
FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path);
|
||||
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, path, headers);
|
||||
const char* initResult = initNetworkingState(state, host, port, path, headers);
|
||||
if (initResult != nullptr) {
|
||||
FF_DEBUG("Initialization failed: %s", initResult);
|
||||
return initResult;
|
||||
@@ -375,9 +379,20 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
}
|
||||
|
||||
// Set larger initial receive buffer instead of small repeated receives
|
||||
int rcvbuf = 65536; // 64KB
|
||||
int rcvbuf = 64 * 1024;
|
||||
setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
|
||||
|
||||
// The timeout has to be enforced by the socket itself on every platform: the poll() below
|
||||
// only reports readability once, so a server that sends a partial response and then keeps
|
||||
// the connection open would otherwise block this loop forever.
|
||||
if (timeout > 0) {
|
||||
FF_DEBUG("Setting receive timeout: %u ms", timeout);
|
||||
struct timeval timev;
|
||||
timev.tv_sec = timeout / 1000;
|
||||
timev.tv_usec = (typeof(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds
|
||||
setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev));
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
// poll for the socket to be readable.
|
||||
// Because of the non-blocking connectx() call, the connection might not be established yet
|
||||
@@ -401,14 +416,6 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
}
|
||||
}
|
||||
FF_DEBUG("Socket is readable, proceeding to receive data");
|
||||
#else
|
||||
if (timeout > 0) {
|
||||
FF_DEBUG("Setting receive timeout: %u ms", timeout);
|
||||
struct timeval timev;
|
||||
timev.tv_sec = timeout / 1000;
|
||||
timev.tv_usec = (typeof(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds
|
||||
setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev));
|
||||
}
|
||||
#endif
|
||||
|
||||
if (shutdown(state->sockfd, SHUT_WR) == -1) {
|
||||
@@ -420,16 +427,49 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
[[maybe_unused]] int recvCount = 0;
|
||||
uint32_t contentLength = 0;
|
||||
uint32_t headerEnd = 0;
|
||||
bool chunked = false;
|
||||
|
||||
do {
|
||||
FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
|
||||
// Runs until the response is framed; the buffer is grown on demand at the top
|
||||
for (;;) {
|
||||
if (ffStrbufGetFree(buffer) == 0) {
|
||||
// `Content-Length` may be absent (e.g. chunked responses). Grow the buffer
|
||||
// on demand instead of silently truncating the response.
|
||||
if (buffer->allocated >= FF_NETWORKING_MAX_RESPONSE_SIZE) {
|
||||
FF_DEBUG("Response is too large: %u bytes, aborting", buffer->allocated);
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
return "Response too large";
|
||||
}
|
||||
FF_DEBUG("Receive buffer is full, extending it");
|
||||
// Asking for exactly the room that is left under the cap, rather than for as much as
|
||||
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
|
||||
// from a size that is not one of those lands past the cap the check above just cleared.
|
||||
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
|
||||
}
|
||||
|
||||
// When the remaining length is known, ask for exactly that much. MSG_WAITALL then
|
||||
// returns as soon as the response is complete, instead of waiting for the server
|
||||
// to close the connection.
|
||||
// Without a Content-Length the requested length is just "whatever fits", so
|
||||
// waiting for all of it would block until the server closes -- and the framing
|
||||
// checks below would never get a chance to run. Read whatever has arrived instead
|
||||
// and let those checks decide when the response is complete.
|
||||
uint32_t want = ffStrbufGetFree(buffer);
|
||||
int recvFlags = 0;
|
||||
if (contentLength > 0 && headerEnd > 0) {
|
||||
uint32_t remaining = headerEnd + 4 + contentLength - buffer->length;
|
||||
if (remaining < want) {
|
||||
want = remaining;
|
||||
}
|
||||
recvFlags = MSG_WAITALL;
|
||||
}
|
||||
|
||||
FF_DEBUG("Data reception loop #%d, current buffer size: %u, requesting %u bytes",
|
||||
++recvCount,
|
||||
buffer->length,
|
||||
ffStrbufGetFree(buffer));
|
||||
want);
|
||||
|
||||
// We set `Connection: close`, so the server will close the connection when done.
|
||||
// Thus we can use MSG_WAITALL to wait until the buffer is full or the connection is closed.
|
||||
ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, ffStrbufGetFree(buffer), MSG_WAITALL);
|
||||
ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, want, recvFlags);
|
||||
|
||||
if (received <= 0) {
|
||||
if (received == 0) {
|
||||
@@ -453,11 +493,12 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
|
||||
|
||||
// Check for Content-Length header to pre-allocate enough memory
|
||||
const char* clHeader = strcasestr(buffer->chars, "Content-Length:");
|
||||
uint32_t valueLen = 0;
|
||||
const char* clHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Content-Length:", &valueLen);
|
||||
if (clHeader) {
|
||||
contentLength = (uint32_t) strtoul(clHeader + 15, nullptr, 10);
|
||||
contentLength = (uint32_t) strtoul(clHeader, nullptr, 10);
|
||||
if (contentLength > 0) {
|
||||
if (contentLength > 1024 * 1024) { // 1MB limit to prevent excessive memory allocation and potential attacks
|
||||
if (contentLength > FF_NETWORKING_MAX_RESPONSE_SIZE) { // 1MB limit to prevent excessive memory allocation and potential attacks
|
||||
FF_DEBUG("Content-Length is too large: %u bytes, aborting", contentLength);
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
@@ -470,9 +511,47 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
|
||||
}
|
||||
}
|
||||
|
||||
// A chunked response has no Content-Length; it is framed by a last-chunk
|
||||
const char* teHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Transfer-Encoding:", &valueLen);
|
||||
if (teHeader != nullptr) {
|
||||
switch (ffNetworkingParseTransferEncoding(teHeader, valueLen)) {
|
||||
case FF_NETWORKING_TE_CHUNKED:
|
||||
FF_DEBUG("Detected chunked transfer encoding");
|
||||
chunked = true;
|
||||
break;
|
||||
case FF_NETWORKING_TE_UNSUPPORTED:
|
||||
// The framing of e.g. `gzip, chunked` is unreadable and the payload
|
||||
// would stay encoded, so fail instead of returning garbage
|
||||
FF_DEBUG("Unsupported Transfer-Encoding: %.*s", (int) valueLen, teHeader);
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
return "Unsupported Transfer-Encoding";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (ffStrbufGetFree(buffer) > 0);
|
||||
|
||||
// Stop as soon as the response is framed, rather than waiting for the FIN
|
||||
if (chunked) {
|
||||
uint32_t consumed = 0;
|
||||
int complete = ffNetworkingChunkedComplete(buffer->chars + headerEnd + 4, buffer->length - headerEnd - 4, &consumed);
|
||||
if (complete < 0) {
|
||||
FF_DEBUG("Malformed chunked body");
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
return "Malformed chunked body";
|
||||
}
|
||||
if (complete > 0) {
|
||||
FF_DEBUG("Chunked body complete, %u bytes of encoded body", consumed);
|
||||
break;
|
||||
}
|
||||
} else if (contentLength > 0 && buffer->length >= headerEnd + 4 + contentLength) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("Closing socket: fd=%d", state->sockfd);
|
||||
close(state->sockfd);
|
||||
@@ -488,29 +567,24 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "No HTTP header end found";
|
||||
}
|
||||
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
|
||||
FF_DEBUG("Failed to decode chunked response");
|
||||
return "Failed to decode chunked response";
|
||||
}
|
||||
|
||||
if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n") && !ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n")) {
|
||||
FF_DEBUG("Invalid response: %.40s...", buffer->chars);
|
||||
return "Invalid response";
|
||||
}
|
||||
FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length);
|
||||
|
||||
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
|
||||
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
|
||||
// says not to send both; when one does, the chunked framing is the one that was acted on.
|
||||
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
|
||||
static LPFN_CONNECTEX ConnectEx;
|
||||
|
||||
// Upper bound of a single HTTP response, guarding against excessive memory allocation
|
||||
#define FF_NETWORKING_MAX_RESPONSE_SIZE (1024u * 1024u)
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -25,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";
|
||||
}
|
||||
@@ -33,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";
|
||||
@@ -45,26 +49,8 @@ static const char* initWsaData(WSADATA* wsaData) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
|
||||
FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, 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");
|
||||
}
|
||||
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);
|
||||
|
||||
static WSADATA wsaData;
|
||||
if (wsaData.wVersion == 0) {
|
||||
@@ -87,20 +73,25 @@ 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";
|
||||
}
|
||||
|
||||
FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4");
|
||||
if (GetAddrInfoW(hostW, L"80", &hints, &addr) != 0) {
|
||||
FF_DEBUG("GetAddrInfoW() failed");
|
||||
wchar_t portW[6];
|
||||
_itow(port, portW, 10);
|
||||
|
||||
FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
|
||||
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";
|
||||
}
|
||||
@@ -143,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;
|
||||
@@ -156,15 +148,20 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
|
||||
ffStrbufAppendS(&state->command, "GET ");
|
||||
ffStrbufAppendS(&state->command, path);
|
||||
ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
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");
|
||||
if (strchr(host, ':') != nullptr) {
|
||||
// An IPv6 literal has to be bracketed in the Host header (RFC 9110 7.2), while
|
||||
// GetAddrInfoW() wants it bare
|
||||
ffStrbufAppendC(&state->command, '[');
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
ffStrbufAppendC(&state->command, ']');
|
||||
} else {
|
||||
ffStrbufAppendS(&state->command, host);
|
||||
}
|
||||
|
||||
// The Host header carries the port whenever it is not the default one (RFC 9110 7.2)
|
||||
if (port != 80) {
|
||||
ffStrbufAppendF(&state->command, ":%u", port);
|
||||
}
|
||||
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure
|
||||
ffStrbufAppendS(&state->command, headers);
|
||||
ffStrbufAppendS(&state->command, "\r\n");
|
||||
|
||||
@@ -266,8 +263,26 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
[[maybe_unused]] int recvCount = 0;
|
||||
uint32_t contentLength = 0;
|
||||
uint32_t headerEnd = 0;
|
||||
bool chunked = false;
|
||||
|
||||
// Runs until the response is framed; the buffer is grown on demand at the top
|
||||
for (;;) {
|
||||
if (ffStrbufGetFree(buffer) == 0) {
|
||||
// `Content-Length` may be absent (e.g. chunked responses). Grow the buffer
|
||||
// on demand instead of silently truncating the response.
|
||||
if (buffer->allocated >= FF_NETWORKING_MAX_RESPONSE_SIZE) {
|
||||
FF_DEBUG("Response is too large: %u bytes, aborting", buffer->allocated);
|
||||
closesocket(state->sockfd);
|
||||
state->sockfd = INVALID_SOCKET;
|
||||
return "Response too large";
|
||||
}
|
||||
FF_DEBUG("Receive buffer is full, extending it");
|
||||
// Asking for exactly the room that is left under the cap, rather than for as much as
|
||||
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
|
||||
// from a size that is not one of those lands past the cap the check above just cleared.
|
||||
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
|
||||
}
|
||||
|
||||
do {
|
||||
FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
|
||||
++recvCount,
|
||||
buffer->length,
|
||||
@@ -306,11 +321,12 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
|
||||
|
||||
// Check for Content-Length header to pre-allocate enough memory
|
||||
const char* clHeader = strcasestr(buffer->chars, "Content-Length:");
|
||||
uint32_t valueLen = 0;
|
||||
const char* clHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Content-Length:", &valueLen);
|
||||
if (clHeader) {
|
||||
contentLength = (uint32_t) strtoul(clHeader + 15, nullptr, 10);
|
||||
contentLength = (uint32_t) strtoul(clHeader, nullptr, 10);
|
||||
if (contentLength > 0) {
|
||||
if (contentLength > 1024 * 1024) { // 1MB limit to prevent excessive memory allocation and potential attacks
|
||||
if (contentLength > FF_NETWORKING_MAX_RESPONSE_SIZE) { // 1MB limit to prevent excessive memory allocation and potential attacks
|
||||
FF_DEBUG("Content-Length is too large: %u bytes, aborting", contentLength);
|
||||
closesocket(state->sockfd);
|
||||
state->sockfd = INVALID_SOCKET;
|
||||
@@ -323,9 +339,47 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
|
||||
}
|
||||
}
|
||||
|
||||
// A chunked response has no Content-Length; it is framed by a last-chunk
|
||||
const char* teHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Transfer-Encoding:", &valueLen);
|
||||
if (teHeader != nullptr) {
|
||||
switch (ffNetworkingParseTransferEncoding(teHeader, valueLen)) {
|
||||
case FF_NETWORKING_TE_CHUNKED:
|
||||
FF_DEBUG("Detected chunked transfer encoding");
|
||||
chunked = true;
|
||||
break;
|
||||
case FF_NETWORKING_TE_UNSUPPORTED:
|
||||
// The framing of e.g. `gzip, chunked` is unreadable and the payload
|
||||
// would stay encoded, so fail instead of returning garbage
|
||||
FF_DEBUG("Unsupported Transfer-Encoding: %.*s", (int) valueLen, teHeader);
|
||||
closesocket(state->sockfd);
|
||||
state->sockfd = INVALID_SOCKET;
|
||||
return "Unsupported Transfer-Encoding";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (ffStrbufGetFree(buffer) > 0);
|
||||
|
||||
// Stop as soon as the response is framed, rather than waiting for the FIN
|
||||
if (chunked) {
|
||||
uint32_t consumed = 0;
|
||||
int complete = ffNetworkingChunkedComplete(buffer->chars + headerEnd + 4, buffer->length - headerEnd - 4, &consumed);
|
||||
if (complete < 0) {
|
||||
FF_DEBUG("Malformed chunked body");
|
||||
closesocket(state->sockfd);
|
||||
state->sockfd = INVALID_SOCKET;
|
||||
return "Malformed chunked body";
|
||||
}
|
||||
if (complete > 0) {
|
||||
FF_DEBUG("Chunked body complete, %u bytes of encoded body", consumed);
|
||||
break;
|
||||
}
|
||||
} else if (contentLength > 0 && buffer->length >= headerEnd + 4 + contentLength) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("Closing socket: fd=%u", (unsigned) state->sockfd);
|
||||
closesocket(state->sockfd);
|
||||
@@ -341,6 +395,11 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "No HTTP header end found";
|
||||
}
|
||||
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
|
||||
FF_DEBUG("Failed to decode chunked response");
|
||||
return "Failed to decode chunked response";
|
||||
}
|
||||
|
||||
if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n") && !ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n")) {
|
||||
FF_DEBUG("Invalid response: %.40s...", buffer->chars);
|
||||
return "Invalid response";
|
||||
@@ -349,23 +408,13 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
contentLength,
|
||||
buffer->length);
|
||||
|
||||
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
|
||||
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
|
||||
// says not to send both; when one does, the chunked framing is the one that was acted on.
|
||||
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
|
||||
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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user