From 8c98bf1953d82ea4f6fd9e07f46d10d2c6c272ab Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 2 Sep 2026 10:16:17 +0800 Subject: [PATCH 01/76] CMake: discovers module file automatically --- CMakeLists.txt | 111 +++++++++++-------------------------------------- 1 file changed, 25 insertions(+), 86 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 191eed4904..113278273e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,13 +128,24 @@ 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//` that contains `.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 "") +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() +unset(FF_MODULE_SRCS) + +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) set(BINARY_LINK_TYPE dlopen CACHE STRING "How to link fastfetch") @@ -496,88 +507,18 @@ set(LIBFASTFETCH_SRC 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 ) +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 @@ -1485,11 +1426,9 @@ add_library(libfastfetch OBJECT ) 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() From e9f719f37a7d71d7de2e55f2932a91fc0416c635 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 2 Sep 2026 10:44:36 +0800 Subject: [PATCH 02/76] Doc: update CONTRIBUTING.md [ci skip] --- CONTRIBUTING.md | 215 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 144 insertions(+), 71 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0da08c892c..c89492b32b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,15 +2,15 @@
- Note + Disclaimer - 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.
-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//`, `src/detection//`, `src/modules/modules.c` | `cmake -B build && cmake --build build -j` | -| Add a platform implementation | `src/detection//_.c` | Build on the target platform or CI | +| Add a module | `src/modules//`, `src/detection//`, `src/modules/modules.c`, `CMakeLists.txt` | `cmake -B build && cmake --build build -j` | +| Add a platform implementation | `src/detection//_.c`, the matching platform block in `CMakeLists.txt` | Build on the target platform or CI | | Add an ASCII logo | `src/logo/ascii//.txt`, matching `.inc` | `./build/fastfetch --logo ` | | Change formatting or JSON output | `src/modules//.c` | `./build/fastfetch -s --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 @@ -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//` always has a matching `detection//`. +Therefore, do not assume that `modules//` always has a matching `detection//`. ### Layer boundaries @@ -139,13 +139,13 @@ So do not assume `modules//` always has a matching `detection//`. | `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_` 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_` 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//.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//.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` + +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: -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. +| 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. Verify +### 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//.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_` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/.inc`: +CMake turns each `.txt` file into a `FASTFETCH_DATATEXT_LOGO_` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/.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 `. +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 `. ### 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 @@ -522,7 +589,7 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h ### 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. +CI runs codespell (`.codespellrc`). Known false positives are listed in `ignore-words-list` (`iterm`, `compiletime`, and various non-English distro words). Add new words there rather than changing the code. ### Compiler warnings @@ -566,7 +633,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 +684,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 @@ -649,13 +716,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 +735,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 +755,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 +768,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. --- From 90a6e90b1946dfacd4b97ba5baa9a7c39067e81a Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 2 Sep 2026 10:45:52 +0800 Subject: [PATCH 03/76] Processes (macOS): eliminates a compiler warning --- src/detection/processes/processes_apple.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detection/processes/processes_apple.c b/src/detection/processes/processes_apple.c index 8ed1472b20..719b1b1da8 100644 --- a/src/detection/processes/processes_apple.c +++ b/src/detection/processes/processes_apple.c @@ -34,7 +34,7 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) != sizeof(taskInfo)) { continue; } - result->threads += taskInfo.pti_threadnum; + result->threads += (uint32_t) taskInfo.pti_threadnum; } return nullptr; From caf64f5e4d7e17cd2a6f20b28280a4ebc32e6078 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 2 Sep 2026 10:48:22 +0800 Subject: [PATCH 04/76] Packaging: updates debian stuff [ci skip] --- debian/changelog.tpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog.tpl b/debian/changelog.tpl index a60ed786a3..7cb87d4838 100644 --- a/debian/changelog.tpl +++ b/debian/changelog.tpl @@ -1,3 +1,9 @@ +fastfetch (2.68.1~#UBUNTU_CODENAME#) #UBUNTU_CODENAME#; urgency=medium + + * Update to 2.68.1 + + -- Carter Li Wed, 02 Sep 2026 10:47:51 +0800 + fastfetch (2.68.0~#UBUNTU_CODENAME#) #UBUNTU_CODENAME#; urgency=medium * Update to 2.68.0 From 20597bff00453285b9a4f0d2c40fff41c3f25eb9 Mon Sep 17 00:00:00 2001 From: Piotr Kubaj Date: Wed, 2 Sep 2026 08:39:09 +0200 Subject: [PATCH 05/76] Logo (image): also try unsuffixed ImageMagick library names FreeBSD (and other systems that build ImageMagick without encoding the quantum depth into the library name) install libMagickCore-7.so and libMagickCore-6.so without a .Q16HDRI / .Q16 suffix. dlopen currently only tries the suffixed names, so image logos always fall back to ASCII art with "Image Magick library not found" even when ImageMagick is installed. Append the plain library names as a last resort. --- src/logo/image/im6.c | 3 ++- src/logo/image/im7.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c index 9fb4338c52..b8743b7ec1 100644 --- a/src/logo/image/im6.c +++ b/src/logo/image/im6.c @@ -15,7 +15,8 @@ FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { // clang-format off FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-6.Q16HDRI" FF_LIBRARY_EXTENSION, 8, - "libMagickCore-6.Q16" FF_LIBRARY_EXTENSION, 8 + "libMagickCore-6.Q16" FF_LIBRARY_EXTENSION, 8, + "libMagickCore-6" FF_LIBRARY_EXTENSION, 8 ) // clang-format on FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c index 967e79ea09..b44241ce5c 100644 --- a/src/logo/image/im7.c +++ b/src/logo/image/im7.c @@ -20,7 +20,8 @@ FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { #else FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11, - "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11 + "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11, + "libMagickCore-7" FF_LIBRARY_EXTENSION, 11 ) #endif // clang-format on From ce61783c4baeaecbb6989fae7cbeb47240849dc2 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 3 Sep 2026 09:43:07 +0800 Subject: [PATCH 06/76] Wallpaper (macOS): extracts image path from NSData `Configuration` field Also moves NSWorkspace method to the last method to try Fixes #2559 --- src/detection/wallpaper/wallpaper_apple.m | 158 ++++++++++++++-------- 1 file changed, 102 insertions(+), 56 deletions(-) diff --git a/src/detection/wallpaper/wallpaper_apple.m b/src/detection/wallpaper/wallpaper_apple.m index 63acae325c..1b4f6e3d59 100644 --- a/src/detection/wallpaper/wallpaper_apple.m +++ b/src/detection/wallpaper/wallpaper_apple.m @@ -5,79 +5,125 @@ #import #import -const char* ffDetectWallpaper(FFstrbuf* result) -{ - { - // Reliable for user-picked static images. - NSURL* url = [NSWorkspace.sharedWorkspace desktopImageURLForScreen:NSScreen.mainScreen]; - if (url.fileURL && ![url.path isEqualToString:@"/System/Library/CoreServices/DefaultDesktop.heic"] /* dynamic wallpapers */) - { - ffStrbufSetS(result, url.path.UTF8String); - return nullptr; - } +const char* detectFromPlist(FFstrbuf* result) { + // For Sonoma and later (macOS 14.0+) + // https://github.com/JohnCoates/Aerial/issues/1332 + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Application Support/com.apple.wallpaper/Store/Index.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + + if (error) { + return "Failed to read wallpaper plist file"; } - { - // For Sonoma - // https://github.com/JohnCoates/Aerial/issues/1332 - NSError* error; - NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Application Support/com.apple.wallpaper/Store/Index.plist", instance.state.platform.homeDir.chars]; - NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] - error:&error]; - if (!error) - { - NSArray* choices = [dict valueForKeyPath:@"SystemDefault.Desktop.Content.Choices"]; - if (choices.count > 0) - { - NSDictionary* choice = choices[0]; - NSArray* files = choice[@"Files"]; - if (files.count > 0) - { - NSString* file = files[0][@"relative"]; - ffStrbufAppendS(result, [NSURL URLWithString:file].path.UTF8String); + NSArray* choices = [dict valueForKeyPath:@"SystemDefault.Desktop.Content.Choices"]; + if (choices.count > 0) { + NSDictionary* choice = choices[0]; + + NSArray* files = choice[@"Files"]; + if (files.count > 0) { + NSString* file = files[0][@"relative"]; + ffStrbufSetS(result, [NSURL URLWithString:file].path.UTF8String); + } + + if (result->length == 0) { + NSData* configData = choice[@"Configuration"]; + if (configData && configData.length > 0) { + NSError* plistError = nil; + NSDictionary* configPlist = [NSPropertyListSerialization propertyListWithData:configData + options:NSPropertyListImmutable + format:NULL + error:&plistError]; + + if (!plistError && [configPlist isKindOfClass:NSDictionary.class]) { + NSDictionary* urlDict = configPlist[@"url"]; + if ([urlDict isKindOfClass:NSDictionary.class]) { + NSString* relativeUrlString = urlDict[@"relative"]; + if ([relativeUrlString isKindOfClass:[NSString class]]) { + NSURL* fileUrl = [NSURL URLWithString:relativeUrlString]; + if (fileUrl.fileURL) { + ffStrbufSetS(result, fileUrl.path.UTF8String); + } + } + } } - else - { - NSString* provider = choice[@"Provider"]; - NSString* builtinPrefix = @"com.apple.wallpaper.choice."; - if ([provider hasPrefix:builtinPrefix]) - provider = [provider substringFromIndex:builtinPrefix.length]; - if ([provider isEqualToString:@"sonoma"]) - ffStrbufSetStatic(result, "macOS Sonoma"); - else if ([provider isEqualToString:@"aerials"]) // Most builtin aerial wallpapers are private - ffStrbufSetStatic(result, "Built-in aerial photography"); - else - ffStrbufAppendF(result, "Built-in %s wallpaper", provider.UTF8String); + } + } + + if (result->length == 0) { + NSString* provider = choice[@"Provider"]; + if ([provider isKindOfClass:NSString.class]) { + NSString* builtinPrefix = @"com.apple.wallpaper.choice."; + if ([provider hasPrefix:builtinPrefix]) { + provider = [provider substringFromIndex:builtinPrefix.length]; + } + + // macOS internal wallpapers + if ([provider isEqualToString:@"aerials"]) { // Most builtin aerial wallpapers are private + ffStrbufSetStatic(result, "Built-in aerial photography"); + } else if ([provider isEqualToString:@"default"]) { + ffStrbufSetStatic(result, "macOS Default Wallpaper"); + } else { + ffStrbufSetF(result, "Built-in %s wallpaper", provider.UTF8String); } } - if (result->length > 0) - return nullptr; } } + if (result->length == 0) { + return "Failed to detect wallpaper from plist"; + } + return nullptr; +} - #ifdef FF_HAVE_SQLITE3 - - { - // For Ventura - // https://stackoverflow.com/questions/301215/getting-desktop-background-on-mac - FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&instance.state.platform.homeDir); - ffStrbufAppendS(&path, "Library/Application Support/Dock/desktoppicture.db"); - if (ffSettingsGetSQLite3String(path.chars, +#ifdef FF_HAVE_SQLITE3 +const char* detectFromSQLite(FFstrbuf* result) { + // For Ventura + // https://stackoverflow.com/questions/301215/getting-desktop-background-on-mac + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&instance.state.platform.homeDir); + ffStrbufAppendS(&path, "Library/Application Support/Dock/desktoppicture.db"); + if (ffSettingsGetSQLite3String(path.chars, "SELECT value\n" "FROM preferences\n" "JOIN data ON preferences.data_id=data.ROWID\n" "JOIN pictures ON preferences.picture_id=pictures.ROWID\n" "JOIN displays ON pictures.display_id=displays.ROWID\n" "JOIN spaces ON pictures.space_id=spaces.ROWID\n" - "WHERE display_id=1 AND space_id=1 AND key=1", result) - ) - return nullptr; + "WHERE display_id=1 AND space_id=1 AND key=1", + result)) { + return nullptr; } + return "Failed to detect wallpaper from SQLite database"; +} +#endif - #endif +const char* detectFromNSWorkspace(FFstrbuf* result) { + // Reliable for user-picked static images. + NSScreen* mainScreen = NSScreen.mainScreen; + if (!mainScreen) { + return "Failed to detect wallpaper from NSWorkspace: No main screen found"; + } - if (ffOsascript("tell application \"Finder\" to get POSIX path of (get desktop picture as alias)", result)) + NSURL* url = [NSWorkspace.sharedWorkspace desktopImageURLForScreen:mainScreen]; + if (url.fileURL) { + ffStrbufSetS(result, url.path.UTF8String); return nullptr; + } + + return "Failed to detect wallpaper from NSWorkspace"; +} - return "All detection methods failed"; +const char* ffDetectWallpaper(FFstrbuf* result) { + const char* error; + + if (@available(macOS 14.0, *)) { + error = detectFromPlist(result); + } else { +#ifdef FF_HAVE_SQLITE3 + error = detectFromSQLite(result); +#else + error = detectFromNSWorkspace(result); +#endif + } + return error; } From 1128a75d8cff1df7651a134a53004b655fb91bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Wed, 2 Sep 2026 15:53:16 +0800 Subject: [PATCH 07/76] InitSystem: uses SystemBasicProcessInformation if available --- src/common/windows/nt.h | 26 ++++++- src/detection/initsystem/initsystem_windows.c | 77 +++++++++++++------ 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/common/windows/nt.h b/src/common/windows/nt.h index ceecbf9c86..dd2caa06d9 100644 --- a/src/common/windows/nt.h +++ b/src/common/windows/nt.h @@ -13,6 +13,8 @@ enum { SystemLogicalProcessorAndGroupInformation = 107, SystemBasicPerformanceInformation = 123, SystemSecureBootInformation = 146, + SystemBasicProcessInformation = 252, + SystemHandleCountInformation = 253, }; typedef struct _PROCESSOR_POWER_INFORMATION { @@ -206,6 +208,20 @@ typedef struct _SYSTEM_BASIC_PERFORMANCE_INFORMATION { SIZE_T PeakCommitment; } SYSTEM_BASIC_PERFORMANCE_INFORMATION, *PSYSTEM_BASIC_PERFORMANCE_INFORMATION; +typedef struct _SYSTEM_BASICPROCESS_INFORMATION { + ULONG NextEntryOffset; + HANDLE UniqueProcessId; + HANDLE InheritedFromUniqueProcessId; + ULONG64 SequenceNumber; + UNICODE_STRING ImageName; +} SYSTEM_BASICPROCESS_INFORMATION, *PSYSTEM_BASICPROCESS_INFORMATION; + +typedef struct _SYSTEM_HANDLECOUNT_INFORMATION { + ULONG ProcessCount; + ULONG ThreadCount; + ULONG HandleCount; +} SYSTEM_HANDLECOUNT_INFORMATION, *PSYSTEM_HANDLECOUNT_INFORMATION; + NTSYSAPI NTSTATUS NTAPI NtDelayExecution(_In_ BOOLEAN Alertable, _In_ PLARGE_INTEGER DelayInterval); /** @@ -691,15 +707,19 @@ static inline uint64_t ffKSystemTimeToUInt64(const volatile KSYSTEM_TIME* pTime) static inline bool ffIsWindows10OrGreater() { #if FF_WIN81_COMPAT - return SharedUserData->NtMajorVersion >= 10; + return SharedUserData->NtBuildNumber >= 10240; #else return true; #endif } static inline bool ffIsWindows11OrGreater() { - return SharedUserData->NtMajorVersion > 10 || - (SharedUserData->NtMajorVersion == 10 && SharedUserData->NtBuildNumber >= 22000); + return SharedUserData->NtBuildNumber >= 22000; +} + +static inline bool ffIsSystemBasicProcessInfoAvailable() { // Includes HandleCountInformation, which was added together + // https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation#systembasicprocessinformation + return SharedUserData->NtBuildNumber >= 26200; // MSDN says it was added in 26100.4770; ntdoc says 25H2 } NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken( diff --git a/src/detection/initsystem/initsystem_windows.c b/src/detection/initsystem/initsystem_windows.c index f41a66cfa2..a6a90f3ce1 100644 --- a/src/detection/initsystem/initsystem_windows.c +++ b/src/detection/initsystem/initsystem_windows.c @@ -7,32 +7,65 @@ #include #include +static bool fillResult(FFInitSystemResult* result, uint32_t ppid, uint32_t pid, uint16_t len, PCWSTR name) { + if (ppid != 4 /* System */ || len <= 4 || _wcsnicmp(name + len - 4, L".exe", 4) != 0) { // smss.exe + return false; + } + result->pid = pid; + // We have no permission to open the process for querying the full information + wchar_t exePath[MAX_PATH]; + _snwprintf(exePath, ARRAY_SIZE(exePath), L"%ls\\system32\\%.*ls", (const wchar_t*) SharedUserData->NtSystemRoot, len, name); + ffGetFileVersion(exePath, NULL, &result->version); + ffStrbufSetWS(&result->exe, exePath); + ffStrbufSetNWS(&result->name, len - 4, name); + return true; +} + const char* ffDetectInitSystem(FFInitSystemResult* result) { - // We only need to find the first user process, so 1024 entries should be enough - SYSTEM_PROCESS_INFORMATION buffer[1024] = {}; - ULONG size = sizeof(buffer); - NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, buffer, size, &size); - if (status != STATUS_INFO_LENGTH_MISMATCH && !NT_SUCCESS(status)) { - return "NtQuerySystemInformation(SystemProcessInformation) failed"; + if (ffIsSystemBasicProcessInfoAvailable()) { + // SYSTEM_BASICPROCESS_INFORMATION entries are much smaller than SYSTEM_PROCESS_INFORMATION ones, + // so a modest buffer should be enough to contain all processes + SYSTEM_BASICPROCESS_INFORMATION buffer[1024]; + NTSTATUS status = NtQuerySystemInformation(SystemBasicProcessInformation, buffer, sizeof(buffer), NULL); + if (status != STATUS_INFO_LENGTH_MISMATCH && !NT_SUCCESS(status)) { + goto fallback; + } + for (auto ptr = buffer; ;ptr = (PSYSTEM_BASICPROCESS_INFORMATION) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + assert(ptr >= buffer && (uint8_t*) ptr < (uint8_t*) buffer + sizeof(buffer)); + if (fillResult(result, (uint32_t)(uintptr_t) ptr->InheritedFromUniqueProcessId, + (uint32_t)(uintptr_t) ptr->UniqueProcessId, + ptr->ImageName.Length / sizeof(*ptr->ImageName.Buffer), + ptr->ImageName.Buffer)) { + return nullptr; + } + // The last process in the list always has a NextEntryOffset of 0, even if the buffer was truncated. + if (!ptr->NextEntryOffset) { + return "Could not find init system process"; + } + } } - for (SYSTEM_PROCESS_INFORMATION* ptr = buffer; ; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { - assert(ptr >= buffer && (uint8_t*) ptr < (uint8_t*) buffer + sizeof(buffer)); - uint16_t len = ptr->ImageName.Length / sizeof(*ptr->ImageName.Buffer); - if (ptr->InheritedFromUniqueProcessId == (HANDLE)(uintptr_t) 4 /* System */ && - len > 4 && _wcsnicmp(ptr->ImageName.Buffer + len - 4, L".exe", 4) == 0) { // smss.exe - result->pid = (uint32_t)(uintptr_t) ptr->UniqueProcessId; - // We have no permission to open the process for querying the full information - wchar_t exePath[MAX_PATH]; - _snwprintf(exePath, ARRAY_SIZE(exePath), L"%ls\\system32\\%.*ls", (const wchar_t*) SharedUserData->NtSystemRoot, len, ptr->ImageName.Buffer); - ffGetFileVersion(exePath, NULL, &result->version); - ffStrbufSetWS(&result->exe, exePath); - ffStrbufSetNWS(&result->name, len - 4, ptr->ImageName.Buffer); - return nullptr; +fallback: + { + // We only need to find the first user process, so 1024 entries should be enough + SYSTEM_PROCESS_INFORMATION buffer[1024] = {}; + ULONG size = sizeof(buffer); + NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, buffer, size, &size); + if (status != STATUS_INFO_LENGTH_MISMATCH && !NT_SUCCESS(status)) { + return "NtQuerySystemInformation(SystemProcessInformation) failed"; } - // The last process in the list always has a NextEntryOffset of 0, even if the buffer was truncated. - if (!ptr->NextEntryOffset) { - return "Could not find init system process"; + + for (auto ptr = buffer; ; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + assert(ptr >= buffer && (uint8_t*) ptr < (uint8_t*) buffer + sizeof(buffer)); + uint16_t len = ptr->ImageName.Length / sizeof(*ptr->ImageName.Buffer); + if (fillResult(result, (uint32_t)(uintptr_t) ptr->InheritedFromUniqueProcessId, + (uint32_t)(uintptr_t) ptr->UniqueProcessId, len, ptr->ImageName.Buffer)) { + return nullptr; + } + // The last process in the list always has a NextEntryOffset of 0, even if the buffer was truncated. + if (!ptr->NextEntryOffset) { + return "Could not find init system process"; + } } } } From 00af2d1df259a5d19e2e381c3d5c9fea47f85acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Wed, 2 Sep 2026 16:12:10 +0800 Subject: [PATCH 08/76] WM: uses SystemBasicProcessInformation if available --- src/detection/wm/wm_windows.c | 144 ++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 52 deletions(-) diff --git a/src/detection/wm/wm_windows.c b/src/detection/wm/wm_windows.c index 01a336b287..21e073dd26 100644 --- a/src/detection/wm/wm_windows.c +++ b/src/detection/wm/wm_windows.c @@ -114,70 +114,110 @@ static bool isProcessTrusted(DWORD processId, FFProcessType processType, UNICODE #define ffStrEqualNWS(str, compareTo) (_wcsnicmp(str, L##compareTo, sizeof(compareTo) - 1) == 0) -const char* ffDetectWMPlugin(FFstrbuf* pluginName) { +// Returns true if the process was recognized as a WM plugin and pluginName was set. +static bool handleProcess(FFstrbuf* pluginName, uint32_t pid, const UNICODE_STRING* imageName) { alignas(UNICODE_STRING) uint8_t buffer[4096]; UNICODE_STRING* filePath = (UNICODE_STRING*) buffer; - FF_AUTO_FREE SYSTEM_PROCESS_INFORMATION* pstart = nullptr; - - // Multiple attempts in case processes change while - // we are in the middle of querying them. - ULONG size = 0; - for (int attempts = 0;; ++attempts) { - if (size) { - pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size); - assert(pstart); - } - NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size); - if (NT_SUCCESS(status)) { - break; - } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { - size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; + + if (imageName->Length == strlen("FancyWM-GUI.exe") * sizeof(wchar_t) && + ffStrEqualNWS(imageName->Buffer, "FancyWM-GUI.exe") && + isProcessTrusted(pid, FF_PROCESS_TYPE_WINDOWS_STORE | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) { + ffStrbufPrependS(pluginName, "FancyWM "); + } else { + ffStrbufSetStatic(pluginName, "FancyWM"); + } + return true; + } else if (imageName->Length == strlen("glazewm-watcher.exe") * sizeof(wchar_t) && + ffStrEqualNWS(imageName->Buffer, "glazewm-watcher.exe") && + isProcessTrusted(pid, FF_PROCESS_TYPE_SIGNED | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) { + ffStrbufPrependS(pluginName, "GlazeWM "); } else { - return "NtQuerySystemInformation(SystemProcessInformation) failed"; + ffStrbufSetStatic(pluginName, "GlazeWM"); + } + return true; + } else if (imageName->Length == strlen("komorebi.exe") * sizeof(wchar_t) && + ffStrEqualNWS(imageName->Buffer, "komorebi.exe") && + isProcessTrusted(pid, FF_PROCESS_TYPE_CUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateNWS(filePath->Length / sizeof(wchar_t), filePath->Buffer); + if (ffProcessAppendStdOut(pluginName, (char* const[]) { + path.chars, + "--version", + nullptr, + }) == nullptr) { + ffStrbufSubstrBeforeFirstC(pluginName, '\n'); + } } + if (pluginName->length == 0) { + ffStrbufSetStatic(pluginName, "Komorebi"); + } + return true; } - for (SYSTEM_PROCESS_INFORMATION* ptr = pstart;; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { - assert(ptr->ImageName.Length == 0 || ptr->ImageName.MaximumLength >= ptr->ImageName.Length + 2); // nullptr terminated - if (ptr->ImageName.Length == strlen("FancyWM-GUI.exe") * sizeof(wchar_t) && - ffStrEqualNWS(ptr->ImageName.Buffer, "FancyWM-GUI.exe") && - isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_WINDOWS_STORE | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { - if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) { - ffStrbufPrependS(pluginName, "FancyWM "); - } else { - ffStrbufSetStatic(pluginName, "FancyWM"); + return false; +} + +const char* ffDetectWMPlugin(FFstrbuf* pluginName) { + if (ffIsSystemBasicProcessInfoAvailable()) { + // Unlike SystemProcessInformation, SystemBasicProcessInformation only returns processes + // (no threads), so a single query with a modest buffer is enough. + ULONG size = 0; + if (NtQuerySystemInformation(SystemBasicProcessInformation, nullptr, 0, &size) != STATUS_INFO_LENGTH_MISMATCH) { + goto fallback; + } + // The process table may change between the two calls; retry with a larger buffer. + size += size / 8 + sizeof(SYSTEM_BASICPROCESS_INFORMATION); + FF_AUTO_FREE SYSTEM_BASICPROCESS_INFORMATION* pstart = malloc(size); + + if (!NT_SUCCESS(NtQuerySystemInformation(SystemBasicProcessInformation, pstart, size, &size))) { + goto fallback; + } + + for (auto ptr = pstart;; ptr = (SYSTEM_BASICPROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + assert(ptr >= pstart && (uint8_t*) ptr < (uint8_t*) pstart + size); + if (handleProcess(pluginName, (uint32_t)(uintptr_t) ptr->UniqueProcessId, &ptr->ImageName)) { + return nullptr; } - break; - } else if (ptr->ImageName.Length == strlen("glazewm-watcher.exe") * sizeof(wchar_t) && - ffStrEqualNWS(ptr->ImageName.Buffer, "glazewm-watcher.exe") && - isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_SIGNED | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { - if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, nullptr, pluginName)) { - ffStrbufPrependS(pluginName, "GlazeWM "); - } else { - ffStrbufSetStatic(pluginName, "GlazeWM"); + // The last process in the list always has a NextEntryOffset of 0, even if the buffer was truncated. + if (!ptr->NextEntryOffset) { + break; } - break; - } else if (ptr->ImageName.Length == strlen("komorebi.exe") * sizeof(wchar_t) && - ffStrEqualNWS(ptr->ImageName.Buffer, "komorebi.exe") && - isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_CUI, filePath, sizeof(buffer))) { - if (instance.config.general.detectVersion) { - FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateNWS(filePath->Length / sizeof(wchar_t), filePath->Buffer); - if (ffProcessAppendStdOut(pluginName, (char* const[]) { - path.chars, - "--version", - nullptr, - }) == nullptr) { - ffStrbufSubstrBeforeFirstC(pluginName, '\n'); - } + } + return nullptr; + } + +fallback: + { + FF_AUTO_FREE SYSTEM_PROCESS_INFORMATION* pstart = nullptr; + + // Multiple attempts in case processes change while + // we are in the middle of querying them. + ULONG size = 0; + for (int attempts = 0;; ++attempts) { + if (size) { + pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size); + assert(pstart); } - if (pluginName->length == 0) { - ffStrbufSetStatic(pluginName, "Komorebi"); + NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size); + if (NT_SUCCESS(status)) { + break; + } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { + size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; + } else { + return "NtQuerySystemInformation(SystemProcessInformation) failed"; } - break; } - if (ptr->NextEntryOffset == 0) { - break; + for (auto ptr = pstart;; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + assert(ptr->ImageName.Length == 0 || ptr->ImageName.MaximumLength >= ptr->ImageName.Length + 2); // nullptr terminated + if (handleProcess(pluginName, (uint32_t)(uintptr_t) ptr->UniqueProcessId, &ptr->ImageName)) { + return nullptr; + } + if (ptr->NextEntryOffset == 0) { + break; + } } } From 62f78ca60cedfdd75f8d9dbab91696bcb89e9eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Wed, 2 Sep 2026 16:23:06 +0800 Subject: [PATCH 09/76] Processes (Windows): uses SystemHandleCountInformation if available --- src/detection/processes/processes_windows.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/detection/processes/processes_windows.c b/src/detection/processes/processes_windows.c index 0a2ba2f41a..b5d65b3017 100644 --- a/src/detection/processes/processes_windows.c +++ b/src/detection/processes/processes_windows.c @@ -1,10 +1,23 @@ #include "processes.h" #include "common/mallocHelper.h" +#include "common/windows/nt.h" #include #include const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { + if (options->countKprocs && ffIsSystemBasicProcessInfoAvailable()) { + // SystemHandleCountInformation reports the total process/thread counts directly, + // so we don't need to walk the whole process table. + SYSTEM_HANDLECOUNT_INFORMATION info = {}; // Seems that kernel only fills the lower 32 bits of the counts, leave the upper 32 bits untouched. + if (NT_SUCCESS(NtQuerySystemInformation(SystemHandleCountInformation, &info, sizeof(info), NULL))) { + result->processes = info.ProcessCount; + result->threads = info.ThreadCount; + return nullptr; + } + // Otherwise fall back to walking the process table + } + FF_AUTO_FREE SYSTEM_PROCESS_INFORMATION* pstart = nullptr; // Multiple attempts in case processes change while From 4e78f1b7039fa9a97a541efb78fb49156edf0f01 Mon Sep 17 00:00:00 2001 From: Matt Housh Date: Fri, 4 Sep 2026 05:14:21 -0500 Subject: [PATCH 10/76] Packages (Linux): adds support for package detection for CRUX linux --- doc/json_schema.json | 3 ++- src/detection/packages/packages.h | 1 + src/detection/packages/packages_linux.c | 3 +++ src/modules/packages/option.h | 1 + src/modules/packages/packages.c | 6 ++++++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/json_schema.json b/doc/json_schema.json index 0aeba6de29..cfa85ea4c0 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -446,7 +446,7 @@ "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. {scoop-global}: Number of scoop-global packages\n 43. {scoop-user}: Number of scoop-user packages\n 44. {snap}: Number of snap packages\n 45. {soar}: Number of soar packages\n 46. {sorcery}: Number of sorcery packages\n 47. {winget}: Number of winget packages\n 48. {xbps}: Number of xbps packages\n 49. {brew-all}: Total number of all brew packages\n 50. {flatpak-all}: Total number of all flatpak app packages\n 51. {guix-all}: Total number of all guix packages\n 52. {hpkg-all}: Total number of all hpkg packages\n 53. {nix-all}: Total number of all nix packages\n 54. {all}: Number of all packages", "type": "string" }, "physicaldiskFormat": { @@ -3763,6 +3763,7 @@ "apk", "brew", "choco", + "crux", "dpkg", "emerge", "eopkg", diff --git a/src/detection/packages/packages.h b/src/detection/packages/packages.h index 2119349323..b7d9c90eb7 100644 --- a/src/detection/packages/packages.h +++ b/src/detection/packages/packages.h @@ -12,6 +12,7 @@ typedef struct FFPackagesResult { uint32_t brewCask; uint32_t cards; uint32_t choco; + uint32_t crux; uint32_t dpkg; uint32_t emerge; uint32_t eopkg; diff --git a/src/detection/packages/packages_linux.c b/src/detection/packages/packages_linux.c index 6d0a716164..1ad1124cb7 100644 --- a/src/detection/packages/packages_linux.c +++ b/src/detection/packages/packages_linux.c @@ -487,6 +487,9 @@ static void getPackageCounts(FFstrbuf* baseDir, FFPackagesResult* packageCounts, if (FF_PACKAGES_IS_ENABLED(options, APK)) { packageCounts->apk += getNumStrings(baseDir, "/lib/apk/db/installed", "C:Q", "apk"); } + if (FF_PACKAGES_IS_ENABLED(options, CRUX)) { + packageCounts->crux += getNumStrings(baseDir, "/var/lib/pkg/db", "\n\n", "crux"); + } if (FF_PACKAGES_IS_ENABLED(options, DPKG)) { packageCounts->dpkg += getNumStrings(baseDir, "/var/lib/dpkg/status", "Status: install ok installed", "dpkg"); } diff --git a/src/modules/packages/option.h b/src/modules/packages/option.h index a9bb444a86..ce96a3c8ea 100644 --- a/src/modules/packages/option.h +++ b/src/modules/packages/option.h @@ -41,6 +41,7 @@ typedef enum FFPackagesFlags: uint64_t { FF_PACKAGES_FLAG_CARDS_BIT = UINT64_C(1) << 34U, FF_PACKAGES_FLAG_PORG_BIT = UINT64_C(1) << 35U, FF_PACKAGES_FLAG_INSTALLRELEASE_BIT = UINT64_C(1) << 36U, + FF_PACKAGES_FLAG_CRUX_BIT = UINT64_C(1) << 37U, } FFPackagesFlags; static_assert(sizeof(FFPackagesFlags) == sizeof(uint64_t), ""); diff --git a/src/modules/packages/packages.c b/src/modules/packages/packages.c index f31275a86a..6edf2a5a7e 100644 --- a/src/modules/packages/packages.c +++ b/src/modules/packages/packages.c @@ -63,6 +63,7 @@ bool ffPrintPackages(FFPackagesOptions* options) { } FF_PRINT_PACKAGE(cards) FF_PRINT_PACKAGE(choco) + FF_PRINT_PACKAGE(crux) FF_PRINT_PACKAGE(dpkg) FF_PRINT_PACKAGE(emerge) FF_PRINT_PACKAGE(eopkg) @@ -154,6 +155,7 @@ bool ffPrintPackages(FFPackagesOptions* options) { FF_ARG(counts.brewCask, "brew-cask"), FF_ARG(counts.cards, "cards"), FF_ARG(counts.choco, "choco"), + FF_ARG(counts.choco, "crux"), FF_ARG(counts.dpkg, "dpkg"), FF_ARG(counts.emerge, "emerge"), FF_ARG(counts.eopkg, "eopkg"), @@ -257,6 +259,7 @@ void ffParsePackagesJsonObject(FFPackagesOptions* options, yyjson_val* module) { ; FF_TEST_PACKAGE_NAME(CARDS) FF_TEST_PACKAGE_NAME(CHOCO) + FF_TEST_PACKAGE_NAME(CRUX) break; case 'D': if (false) @@ -390,6 +393,7 @@ void ffGeneratePackagesJsonConfig(FFPackagesOptions* options, yyjson_mut_doc* do FF_TEST_PACKAGE_NAME(BREW) FF_TEST_PACKAGE_NAME(CARDS) FF_TEST_PACKAGE_NAME(CHOCO) + FF_TEST_PACKAGE_NAME(CRUX) FF_TEST_PACKAGE_NAME(DPKG) FF_TEST_PACKAGE_NAME(EMERGE) FF_TEST_PACKAGE_NAME(EOPKG) @@ -451,6 +455,7 @@ bool ffGeneratePackagesJsonResult(FFPackagesOptions* options, yyjson_mut_doc* do FF_APPEND_PACKAGE_COUNT(brewCask) FF_APPEND_PACKAGE_COUNT(cards) FF_APPEND_PACKAGE_COUNT(choco) + FF_APPEND_PACKAGE_COUNT(crux) FF_APPEND_PACKAGE_COUNT(dpkg) FF_APPEND_PACKAGE_COUNT(emerge) FF_APPEND_PACKAGE_COUNT(eopkg) @@ -549,6 +554,7 @@ FFModuleBaseInfo ffPackagesModuleInfo = { { "Number of brew-cask packages", "brew-cask" }, { "Number of cards packages", "cards" }, { "Number of choco packages", "choco" }, + { "Number of crux packages", "crux" }, { "Number of dpkg packages", "dpkg" }, { "Number of emerge packages", "emerge" }, { "Number of eopkg packages", "eopkg" }, From ecee5694fb02782bd9d4b8827602aa57e6374614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 4 Sep 2026 19:35:17 +0800 Subject: [PATCH 11/76] Global: uses `strtoull` for uint64_t Fixes #2563 #2564 --- src/detection/cpu/cpu_linux.c | 2 +- src/detection/gpu/gpu_linux.c | 2 +- src/detection/gpu/gpu_sunos.c | 2 +- src/detection/physicaldisk/physicaldisk_linux.c | 2 +- src/detection/physicalmemory/physicalmemory_apple.m | 2 +- src/detection/swap/swap_linux.c | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/detection/cpu/cpu_linux.c b/src/detection/cpu/cpu_linux.c index 589d0ac01b..4a338ea4e8 100644 --- a/src/detection/cpu/cpu_linux.c +++ b/src/detection/cpu/cpu_linux.c @@ -712,7 +712,7 @@ static bool detectFrequency(FFCPUResult* cpu, const FFCPUOptions* options) { while ((p = memmem(p, cpuinfo->length - (uint32_t) (p - cpuinfo->chars), "\nphysical id\t:", strlen("\nphysical id\t:")))) { p += strlen("\nphysical id\t:"); char* pend; - unsigned long long id = strtoul(p, &pend, 10); + unsigned long id = strtoul(p, &pend, 10); if (__builtin_expect(id > 64, false)) { // Do 129-socket boards exist? high |= 1ULL << (id - 64); } else { diff --git a/src/detection/gpu/gpu_linux.c b/src/detection/gpu/gpu_linux.c index 9575d9ff46..d812e08b75 100644 --- a/src/detection/gpu/gpu_linux.c +++ b/src/detection/gpu/gpu_linux.c @@ -426,7 +426,7 @@ static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf ffStrbufAppendS(deviceDir, "/revision"); if (ffReadFileBuffer(deviceDir->chars, buffer)) { char* pend; - uint64_t revision = strtoul(buffer->chars, &pend, 16); + uint32_t revision = (uint32_t) strtoul(buffer->chars, &pend, 16); if (pend != buffer->chars) { ffGPUQueryAmdGpuName((uint16_t) deviceId, (uint8_t) revision, gpu); } diff --git a/src/detection/gpu/gpu_sunos.c b/src/detection/gpu/gpu_sunos.c index 8e896625ff..fb9db7a29b 100644 --- a/src/detection/gpu/gpu_sunos.c +++ b/src/detection/gpu/gpu_sunos.c @@ -19,7 +19,7 @@ static int walkDevTree(di_node_t node, [[maybe_unused]] di_minor_t minor, FFlist gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; gpu->type = FF_GPU_TYPE_UNKNOWN; gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; - gpu->deviceId = strtoul(di_bus_addr(node), nullptr, 16); + gpu->deviceId = (uint64_t) strtoull(di_bus_addr(node), nullptr, 16); gpu->frequency = FF_GPU_FREQUENCY_UNSET; gpu->pcieSpeed = FF_GPU_PCIE_SPEED_UNSET; diff --git a/src/detection/physicaldisk/physicaldisk_linux.c b/src/detection/physicaldisk/physicaldisk_linux.c index b6d4c755f7..525e391bf5 100644 --- a/src/detection/physicaldisk/physicaldisk_linux.c +++ b/src/detection/physicaldisk/physicaldisk_linux.c @@ -34,7 +34,7 @@ static void parsePhysicalDisk(int dfd, const char* devName, FFPhysicalDiskOption ssize_t fileSize = ffReadFileDataRelative(dfd, "size", ARRAY_SIZE(blkSize) - 1, blkSize); if (fileSize > 0) { blkSize[fileSize] = 0; - size = (uint64_t) strtoul(blkSize, nullptr, 10) * 512; + size = (uint64_t) strtoull(blkSize, nullptr, 10) * 512; } } diff --git a/src/detection/physicalmemory/physicalmemory_apple.m b/src/detection/physicalmemory/physicalmemory_apple.m index c58c2f14bd..4ce5797022 100644 --- a/src/detection/physicalmemory/physicalmemory_apple.m +++ b/src/detection/physicalmemory/physicalmemory_apple.m @@ -38,7 +38,7 @@ static void appendDevice( if (size) { char* unit = nullptr; - device->size = strtoul(size.UTF8String, &unit, 10); + device->size = (uint64_t) strtoull(size.UTF8String, &unit, 10); if (*unit == ' ') ++unit; switch (*unit) diff --git a/src/detection/swap/swap_linux.c b/src/detection/swap/swap_linux.c index d72125d782..5d456626ca 100644 --- a/src/detection/swap/swap_linux.c +++ b/src/detection/swap/swap_linux.c @@ -19,11 +19,11 @@ static const char* detectByProcMeminfo(FFlist* result) { char* token = nullptr; if ((token = strstr(buf, "SwapTotal:")) != nullptr) { - swapTotal = strtoul(token + strlen("SwapTotal:"), nullptr, 10); + swapTotal = (uint64_t) strtoull(token + strlen("SwapTotal:"), nullptr, 10); } if ((token = strstr(buf, "SwapFree:")) != nullptr) { - swapFree = strtoul(token + strlen("SwapFree:"), nullptr, 10); + swapFree = (uint64_t) strtoull(token + strlen("SwapFree:"), nullptr, 10); } FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); From 6a275da8b126ebef200c5fe324f31c508a9fd53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 5 Sep 2026 00:32:31 +0800 Subject: [PATCH 12/76] Sound: removes an unused file --- src/detection/sound/sound_nosupport.c | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/detection/sound/sound_nosupport.c diff --git a/src/detection/sound/sound_nosupport.c b/src/detection/sound/sound_nosupport.c deleted file mode 100644 index b04530f9eb..0000000000 --- a/src/detection/sound/sound_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "sound.h" - -const char* ffDetectSound([[maybe_unused]] FFlist* devices /* List of FFSoundDevice */) { - return "Not supported on this platform"; -} From c708b6f5cdb514db711f29f41b3e69166af1839e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 5 Sep 2026 00:32:58 +0800 Subject: [PATCH 13/76] CPU (Linux): adds apple code of M5x --- src/detection/cpu/cpu.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/detection/cpu/cpu.c b/src/detection/cpu/cpu.c index a796175e0d..75db6f92cb 100644 --- a/src/detection/cpu/cpu.c +++ b/src/detection/cpu/cpu.c @@ -62,6 +62,10 @@ const char* ffCPUAppleCodeToName(uint32_t code) { return "Apple M4 Pro"; case 6041: return "Apple M4 Max"; + case 8142: + return "Apple M5"; + case 6050: + return "Apple M5 Pro / Max"; default: return nullptr; } From 2e542bea643c9f80aacd3fd3668ad8398b479abd Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sat, 5 Sep 2026 09:43:05 +0400 Subject: [PATCH 14/76] CMake: bumps min version to 3.21 (#2565) C_STANDARD C17 and C23 were only introduced in CMake 3.21 --- .github/workflows/build-linux-i686.yml | 2 +- CMakeLists.txt | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-linux-i686.yml b/.github/workflows/build-linux-i686.yml index 985b3b089b..85f1efb455 100644 --- a/.github/workflows/build-linux-i686.yml +++ b/.github/workflows/build-linux-i686.yml @@ -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" -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 . - name: build project run: cmake --build . --target package --verbose -j4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 113278273e..5d52ecd8e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.12.0) # Don't change before we investigate why #2553 happens +cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23 project(fastfetch VERSION 2.68.1 @@ -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") From 5d290cc3d5c61ee23f7f1ffa04e54c18254984af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 5 Sep 2026 20:14:58 +0800 Subject: [PATCH 15/76] Common (CommandOption): find module configuration from JSON config file --- src/common/impl/commandoption.c | 60 ++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c index fbbcc18db8..0b1bac8dc6 100644 --- a/src/common/impl/commandoption.c +++ b/src/common/impl/commandoption.c @@ -70,15 +70,15 @@ 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 case 'C': case 'c': FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name) ffPrepareCPUUsage(); break; - #endif +#endif - #if !FF_MODULE_DISABLE_DISKIO +#if !FF_MODULE_DISABLE_DISKIO case 'D': case 'd': FF_IF_MODULE_MATCH(ffDiskIOModuleInfo.name) { @@ -87,9 +87,9 @@ void ffPrepareCommandOption(FFdata* data) { 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) { @@ -98,9 +98,9 @@ void ffPrepareCommandOption(FFdata* data) { 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) { @@ -109,9 +109,9 @@ void ffPrepareCommandOption(FFdata* data) { 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) { @@ -120,9 +120,9 @@ void ffPrepareCommandOption(FFdata* data) { 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) { @@ -131,7 +131,7 @@ void ffPrepareCommandOption(FFdata* data) { ffPrepareWeather(&options); } break; - #endif +#endif } #undef FF_IF_MODULE_MATCH @@ -178,6 +178,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, @@ -191,7 +219,13 @@ static bool parseStructureCommand( if (data->resultDoc != nullptr) { fn(data, baseInfo, optionBuf); } else { - baseInfo->printModule(optionBuf); + yyjson_val* configModule = findStructureModuleConfig(data, baseInfo->name); + if (configModule != nullptr) { + baseInfo->parseJsonObject(optionBuf, configModule); + baseInfo->printModule(optionBuf); + } else { + baseInfo->printModule(optionBuf); + } } baseInfo->destroyOptions(optionBuf); return true; From 66e9e38f2811b3ae68b31b862561393c714dcf46 Mon Sep 17 00:00:00 2001 From: Sultaniiazov David Date: Sun, 6 Sep 2026 18:29:42 +0300 Subject: [PATCH 16/76] Logo (Builtin): adds ALT Atomic (#2549) Co-authored-by: Vladimir Romanov --- src/logo/ascii/a.inc | 20 ++++++++++++++++++++ src/logo/ascii/a/alt_atomic.txt | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/logo/ascii/a/alt_atomic.txt diff --git a/src/logo/ascii/a.inc b/src/logo/ascii/a.inc index 38ebae9b33..f9c26d55ed 100644 --- a/src/logo/ascii/a.inc +++ b/src/logo/ascii/a.inc @@ -181,6 +181,26 @@ static const FFlogo A[] = { .colorTitle = FF_COLOR_FG_CYAN, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_ALT_ATOMIC + // ALT Atomic + { + .names = { "alt-atomic" }, + .lines = FASTFETCH_DATATEXT_LOGO_ALT_ATOMIC, + .colors = { + FF_COLOR_FG_RGB "79;157;207", + FF_COLOR_FG_RGB "113;174;213", + FF_COLOR_FG_RGB "61;120;158", + FF_COLOR_FG_RGB "244;250;252", + FF_COLOR_FG_RGB "207;230;243", + FF_COLOR_FG_RGB "29;58;76", + FF_COLOR_FG_RGB "213;201;181", + FF_COLOR_FG_RGB "150;180;193", + FF_COLOR_FG_RGB "161;204;230", + }, + .colorKeys = FF_COLOR_FG_RGB "80;157;207", + .colorTitle = FF_COLOR_FG_RGB "255;209;164", + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_ALTLINUX // ALTLinux { diff --git a/src/logo/ascii/a/alt_atomic.txt b/src/logo/ascii/a/alt_atomic.txt new file mode 100644 index 0000000000..a89f94b969 --- /dev/null +++ b/src/logo/ascii/a/alt_atomic.txt @@ -0,0 +1,20 @@ + $6.:$3-==$1++++++$3==-$6:. + $6.:$3=$1++++++++++++++++++$3=$6:. + $6:$3=$1+++++++++++$2*$1++++++++++++$3=$6: + $6.$3=$1+++++++++++$9%$4@@@$5@$9#$1+++++++++++$3=$6. + $3-$1+++++++++++$2+$4@@$9%$1+$2*$5@$4@$9#$1++++++++++++$3- + $3=$1++++++++++++$5%$4@$9%$1++++$5@$4@$2*$1++++++++++++$3= + $3-$1+++++$8*******$9#$4@@$2*$1++++$9#$4@$5%$2*****$1++++++++$3- +$6:$1+++++$7#$8*$1++++++$5%$4@$5%$9#$5%$4@@@@@@@$5@%%%%$9%$2*$1++++++$6: +$3=$1+++++$8*$7%$8*$1+++$2*$9%$4@@@@@$5%$9###$5%$9%$8#*$2*$1+++$2**$1++++++$3= +$1++++++++$7#%$5%$4@@$5@@%$9#$1+$2*$7#$8#$1++$5%@$2*+$8*#*$2***$1+++++++ +$1+++++++$9#$4@@$5%$7%%#$5%@$2*$1++$8#*$1+$2*$5@%$2*$1+$2**$8#$7##$1++++++++ +$3=$1+++++$5%$4@$2*$1+=+$2+$8*$4@@$7%%#$8**$2*$8*$5@%$2**$1++++$7#%$1++++++$3= +$6:$1+++++$9##$2****$1++$5%$4@$5%$2*$8*$7###%%%#######$8*$1++++++$6: + $3-$1++++++$2****$1++$9#$4@@$2*$1++++$5%$4@$5%$2*$8***$2**$1+++++++$3- + $3=$1++++++++++++$5@$4@$5%$1++++$2*$9#$2*$1++++++++++++$3= + $3-$1+++++++++++$9#$4@@$9#$1+++++++++++++++++$3- + $6.$3=$1++++++++++$9#$5@$4@$5@$9#$1+++++++++++++$3=$6. + $6:$3=$1+++++++++$2*$9###$1+++++++++++$3=$6: + $6.:$3=$1++++++++++++++++++$3=$6:. + $6.:$3-==$1++++++$3==-$6:. \ No newline at end of file From a70c37fdafe09ec2e6f5ee261778a8e757fe5b41 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Mon, 7 Sep 2026 10:58:46 +0800 Subject: [PATCH 17/76] Logo (Builtin): removes Zerene Distro discontinued #2567 --- src/logo/ascii/z.inc | 12 ------------ src/logo/ascii/z/zerene.txt | 18 ------------------ 2 files changed, 30 deletions(-) delete mode 100644 src/logo/ascii/z/zerene.txt diff --git a/src/logo/ascii/z.inc b/src/logo/ascii/z.inc index d2371f9a73..393c906313 100644 --- a/src/logo/ascii/z.inc +++ b/src/logo/ascii/z.inc @@ -3,18 +3,6 @@ #include "common/color.h" static const FFlogo Z[] = { - #ifdef FASTFETCH_DATATEXT_LOGO_ZERENE - // Zerene - { - .names = { "Zerene" }, - .lines = FASTFETCH_DATATEXT_LOGO_ZERENE, - .colors = { - FF_COLOR_FG_BLUE, - }, - .colorKeys = FF_COLOR_FG_BLUE, - .colorTitle = FF_COLOR_FG_BLUE, - }, - #endif #ifdef FASTFETCH_DATATEXT_LOGO_ZORIN // Zorin { diff --git a/src/logo/ascii/z/zerene.txt b/src/logo/ascii/z/zerene.txt deleted file mode 100644 index 3d0e0ce9bd..0000000000 --- a/src/logo/ascii/z/zerene.txt +++ /dev/null @@ -1,18 +0,0 @@ - MMM - MMMM - MMMMM - MMMMMM - MMMMMMa - MMMMMMMa - MMMMMMM - MMMMMM - MMMM - M - oxdo aMa - aMd aMMMMMMMMM - aMMMMM MMMMMMMMMMMMMMa - MMMMMMMa MMMMMMMMMMMMMMMa - MMMMMMMMa - MMMMMMMMa - MMMMMMM -MMMM From 7d0256fb857886bce14604bd0dd9841298684ad2 Mon Sep 17 00:00:00 2001 From: Stian Halseth <33956011+shalseth@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:31:55 +0200 Subject: [PATCH 18/76] CPU (Linux): adds SPARC support (#2572) /proc/cpuinfo on sparc has none of the fields the generic fallback looks for, so the module printed "Unknown (64)". The name is in "cpu", and the frequency in "CpuClkTck" -- in Hz, hex on sparc64, decimal on sparc32. --- CHANGELOG.md | 5 +++++ src/detection/cpu/cpu_linux.c | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1c3fbcfe..786fbef96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +Features: +* Added CPU name and frequency detection support on SPARC. (CPU, Linux) + # 2.68.1 Changes: diff --git a/src/detection/cpu/cpu_linux.c b/src/detection/cpu/cpu_linux.c index 4a338ea4e8..152827294a 100644 --- a/src/detection/cpu/cpu_linux.c +++ b/src/detection/cpu/cpu_linux.c @@ -619,6 +619,9 @@ static const char* parseCpuInfo( (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) || #elif __sh__ (cpu->name.length == 0 && ffParsePropLine(line, "cpu type :", &cpu->name)) || +#elif __sparc__ || __sparc + (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) || + (cpuMHz->length == 0 && ffParsePropLine(line, "Cpu0ClkTck :", cpuMHz)) || #else (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) || (cpu->name.length == 0 && ffParsePropLine(line, "model :", &cpu->name)) || @@ -1101,6 +1104,10 @@ static const char* detectPhysicalCores(FFCPUResult* cpu) { if (cpu->name.length) { ffStrbufPrependS(&cpu->name, "Machine "); } + #elif __sparc__ || __sparc + // Cpu0ClkTck is in Hz, printed as "%016lx" by sparc64 and "%ld" by sparc32. A 32-bit userland can run + // on a 64-bit kernel, so take the base from the width of the value rather than from our own bitness. + cpu->frequencyBase = (uint32_t) (strtoull(cpuMHz.chars, nullptr, cpuMHz.length == 16 ? 16 : 10) / 1000000); #endif } From 222fe7e3f99415a7013201e803cd35772bc104a3 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 9 Sep 2026 13:49:56 +0800 Subject: [PATCH 19/76] 3rdparty (yyjson): upgrades to 0.13.0 --- src/3rdparty/yyjson/repo.json | 2 +- src/3rdparty/yyjson/yyjson.c | 985 ++++++++++++++------ src/3rdparty/yyjson/yyjson.h | 1585 +++++++++++++++++++-------------- 3 files changed, 1612 insertions(+), 960 deletions(-) diff --git a/src/3rdparty/yyjson/repo.json b/src/3rdparty/yyjson/repo.json index bfa523fb55..02c0cd7e8e 100644 --- a/src/3rdparty/yyjson/repo.json +++ b/src/3rdparty/yyjson/repo.json @@ -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" } diff --git a/src/3rdparty/yyjson/yyjson.c b/src/3rdparty/yyjson/yyjson.c index c16d925811..a9bc5f87a0 100644 --- a/src/3rdparty/yyjson/yyjson.c +++ b/src/3rdparty/yyjson/yyjson.c @@ -21,7 +21,6 @@ *============================================================================*/ #include "yyjson.h" -#include /* for `HUGE_VAL/INFINIY/NAN` macros, no libm required */ @@ -35,7 +34,7 @@ # pragma clang diagnostic ignored "-Wunused-label" # pragma clang diagnostic ignored "-Wunused-macros" # pragma clang diagnostic ignored "-Wunused-variable" -#elif defined(__GNUC__) +#elif YYJSON_IS_REAL_GCC && yyjson_gcc_available(4, 2, 0) # pragma GCC diagnostic ignored "-Wunused-function" # pragma GCC diagnostic ignored "-Wunused-parameter" # pragma GCC diagnostic ignored "-Wunused-label" @@ -46,6 +45,7 @@ # pragma warning(disable:4101) /* unreferenced variable */ # pragma warning(disable:4102) /* unreferenced label */ # pragma warning(disable:4127) /* conditional expression is constant */ +# pragma warning(disable:4702) /* unreachable code */ # pragma warning(disable:4706) /* assignment within conditional expression */ #endif @@ -109,37 +109,58 @@ uint32_t yyjson_version(void) { #endif /* int128 type */ -#if defined(__SIZEOF_INT128__) && (__SIZEOF_INT128__ == 16) && \ - (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)) -# define YYJSON_HAS_INT128 1 -#else -# define YYJSON_HAS_INT128 0 +#ifndef YYJSON_HAS_INT128 +# if defined(__SIZEOF_INT128__) && (__SIZEOF_INT128__ == 16) && \ + (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)) && \ + (!defined(__EMSCRIPTEN__) && !defined(__wasm__)) +# define YYJSON_HAS_INT128 1 +# else +# define YYJSON_HAS_INT128 0 +# endif #endif /* IEEE 754 floating-point binary representation */ -#if defined(__STDC_IEC_559__) || defined(__STDC_IEC_60559_BFP__) -# define YYJSON_HAS_IEEE_754 1 -#elif FLT_RADIX == 2 && \ +#ifndef YYJSON_HAS_IEEE_754 +# if defined(__STDC_IEC_559__) || defined(__STDC_IEC_60559_BFP__) +# define YYJSON_HAS_IEEE_754 1 +# elif FLT_RADIX == 2 && \ FLT_MANT_DIG == 24 && FLT_DIG == 6 && \ FLT_MIN_EXP == -125 && FLT_MAX_EXP == 128 && \ FLT_MIN_10_EXP == -37 && FLT_MAX_10_EXP == 38 && \ DBL_MANT_DIG == 53 && DBL_DIG == 15 && \ DBL_MIN_EXP == -1021 && DBL_MAX_EXP == 1024 && \ DBL_MIN_10_EXP == -307 && DBL_MAX_10_EXP == 308 -# define YYJSON_HAS_IEEE_754 1 -#else -# define YYJSON_HAS_IEEE_754 0 -# undef YYJSON_DISABLE_FAST_FP_CONV -# define YYJSON_DISABLE_FAST_FP_CONV 1 +# define YYJSON_HAS_IEEE_754 1 +# else +# define YYJSON_HAS_IEEE_754 0 +# undef YYJSON_DISABLE_FAST_FP_CONV +# define YYJSON_DISABLE_FAST_FP_CONV 1 +# endif +#endif + +#if YYJSON_DISABLE_FAST_FP_CONV && YYJSON_FREESTANDING +# error DISABLE_FAST_FP_CONV and FREESTANDING cannot be used together +#endif + +/* Inf and NaN */ +#ifndef INFINITY +# ifndef HUGE_VAL +# define INFINITY ((double)(1.0 / 0.0)) +# else +# define INFINITY HUGE_VAL +# endif +#endif +#ifndef NAN +# define NAN ((double)(0.0 / 0.0)) #endif /* Correct rounding in double number computations. On the x86 architecture, some compilers may use x87 FPU instructions for - floating-point arithmetic. The x87 FPU loads all floating point number as - 80-bit double-extended precision internally, then rounds the result to original - precision, which may produce inaccurate results. For a more detailed + floating-point arithmetic. The x87 FPU loads all floating-point numbers as + 80-bit double-extended precision internally, then rounds the result to the + original precision, which may produce inaccurate results. For a more detailed explanation, see the paper: https://arxiv.org/abs/cs/0701192 Here are some examples of double precision calculation error: @@ -155,7 +176,7 @@ uint32_t yyjson_version(void) { If we are sure that there's no similar error described above, we can define the YYJSON_DOUBLE_MATH_CORRECT as 1 to enable the fast path calculation. This is - not an accurate detection, it's just try to avoid the error at compile-time. + not an accurate detection; it just tries to avoid the error at compile-time. An accurate detection can be done at run-time: bool is_double_math_correct(void) { @@ -317,35 +338,20 @@ uint32_t yyjson_version(void) { #define YYJSON_ALC_DYN_MIN_SIZE 0x1000 /* Default value for compile-time options. */ -#ifndef YYJSON_DISABLE_READER -#define YYJSON_DISABLE_READER 0 -#endif -#ifndef YYJSON_DISABLE_WRITER -#define YYJSON_DISABLE_WRITER 0 -#endif -#ifndef YYJSON_DISABLE_INCR_READER -#define YYJSON_DISABLE_INCR_READER 0 -#endif -#ifndef YYJSON_DISABLE_UTILS -#define YYJSON_DISABLE_UTILS 0 -#endif -#ifndef YYJSON_DISABLE_FAST_FP_CONV -#define YYJSON_DISABLE_FAST_FP_CONV 0 -#endif -#ifndef YYJSON_DISABLE_NON_STANDARD -#define YYJSON_DISABLE_NON_STANDARD 0 -#endif -#ifndef YYJSON_DISABLE_UTF8_VALIDATION -#define YYJSON_DISABLE_UTF8_VALIDATION 0 -#endif +#ifndef YYJSON_READER_DEPTH_LIMIT +#define YYJSON_READER_DEPTH_LIMIT 0 +#endif +#ifndef YYJSON_WRITER_DEPTH_LIMIT +#define YYJSON_WRITER_DEPTH_LIMIT 0 +#endif /*============================================================================== * MARK: - Macros (Private) *============================================================================*/ -/* Macros used for loop unrolling and other purpose. */ +/* Macros used for loop unrolling and other purposes. */ #define repeat2(x) { x x } #define repeat4(x) { x x x x } #define repeat8(x) { x x x x x x x x } @@ -385,7 +391,7 @@ uint32_t yyjson_version(void) { #define U32(hi) ((u32)(hi##UL)) /* Used to cast away (remove) const qualifier. */ -#define constcast(type) (type)(void *)(size_t)(const void *) +#define constcast yyjson_constcast /* Compiler barriers for single variables. @@ -438,6 +444,7 @@ uint32_t yyjson_version(void) { #define MSG_ERR_UTF8 "invalid utf-8 encoding in string" #define MSG_ERR_UTF16 "UTF-16 encoding is not supported" #define MSG_ERR_UTF32 "UTF-32 encoding is not supported" +#define MSG_DEPTH "depth limit exceeded" /* U64 constant values */ #undef U64_MAX @@ -524,7 +531,7 @@ uint32_t yyjson_version(void) { * MARK: - Types (Private) *============================================================================*/ -/** Type define for primitive types. */ +/** Type aliases for primitive types. */ typedef float f32; typedef double f64; typedef int8_t i8; @@ -929,7 +936,7 @@ static_inline bool char_is_sign(u8 d) { return !!(char_table3[d] & CHAR_TYPE_SIGN); } -/** Match a none-zero digit: [1-9] */ +/** Match a non-zero digit: [1-9] */ static_inline bool char_is_nonzero(u8 d) { return !!(char_table3[d] & CHAR_TYPE_NONZERO); } @@ -939,7 +946,7 @@ static_inline bool char_is_digit(u8 d) { return !!(char_table3[d] & CHAR_TYPE_DIGIT); } -/** Match an exponent sign: [eE]. */ +/** Match an exponent character: [eE]. */ static_inline bool char_is_exp(u8 d) { return !!(char_table3[d] & CHAR_TYPE_EXP); } @@ -1016,10 +1023,10 @@ static_inline usize ext_space_len(const u8 *cur) { *============================================================================*/ /** - This table is used to convert 4 hex character sequence to a number. - A valid hex character [0-9A-Fa-f] will mapped to it's raw number [0x00, 0x0F], - an invalid hex character will mapped to [0xF0]. - (generate with misc/make_tables.c) + This table is used to convert a 4-hex-character sequence to a number. + A valid hex character [0-9A-Fa-f] is mapped to its raw value [0x00, 0x0F]; + an invalid hex character is mapped to [0xF0]. + (generated with misc/make_tables.c) */ static const u8 hex_conv_table[256] = { 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, @@ -1187,16 +1194,18 @@ utf8_seq_def(b4_req2, 03, 30, 00, 00) /** Maximum pow10 exponent that can be represented exactly as a float64. */ #define F64_POW10_MAX_EXACT_EXP 22 +#if YYJSON_DOUBLE_MATH_CORRECT /** Cached pow10 table. */ static const f64 f64_pow10_table[F64_POW10_MAX_EXACT_EXP + 1] = { 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 }; +#endif /** Maximum pow10 exponent that can be represented exactly as a uint64. */ #define U64_POW10_MAX_EXACT_EXP 19 -/** Table: [ 10^0, ..., 10^19 ] (generate with misc/make_tables.c) */ +/** Table: [ 10^0, ..., 10^19 ] (generated with misc/make_tables.c) */ static const u64 u64_pow10_table[U64_POW10_MAX_EXACT_EXP + 1] = { U64(0x00000000, 0x00000001), U64(0x00000000, 0x0000000A), U64(0x00000000, 0x00000064), U64(0x00000000, 0x000003E8), @@ -1222,9 +1231,9 @@ static const u64 u64_pow10_table[U64_POW10_MAX_EXACT_EXP + 1] = { /** Maximum exact decimal exponent in pow10_sig_table */ #define POW10_SIG_TABLE_MAX_EXACT_EXP 55 -/** Normalized significant 128 bits of pow10, no rounded up (size: 10.4KB). +/** Normalized significant 128 bits of pow10, not rounded up (size: 10.4KB). This lookup table is used by both the double number reader and writer. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const u64 pow10_sig_table[] = { U64(0xBF29DCAB, 0xA82FDEAE), U64(0x7432EE87, 0x3880FC33), /* ~= 10^-343 */ U64(0xEEF453D6, 0x923BD65A), U64(0x113FAA29, 0x06A13B3F), /* ~= 10^-342 */ @@ -1898,7 +1907,7 @@ static const u64 pow10_sig_table[] = { /** Get the cached pow10 value from `pow10_sig_table`. - @param exp10 The exponent of pow(10, e). This value must in range + @param exp10 The exponent of pow(10, e). This value must be in the range `POW10_SIG_TABLE_MIN_EXP` to `POW10_SIG_TABLE_MAX_EXP`. @param hi The highest 64 bits of pow(10, e). @param lo The lower 64 bits after `hi`. @@ -1910,7 +1919,8 @@ static_inline void pow10_table_get_sig(i32 exp10, u64 *hi, u64 *lo) { } /** - Get the exponent (base 2) for highest 64 bits significand in `pow10_sig_table`. + Get the exponent (base 2) for the highest 64-bit significand in + `pow10_sig_table`. */ static_inline void pow10_table_get_exp(i32 exp10, i32 *exp2) { /* e2 = floor(log2(pow(10, e))) - 64 + 1 */ @@ -1940,7 +1950,7 @@ static_inline u64 f64_to_bits(f64 f) { return u; } -/** Convert double to bits. */ +/** Convert float to bits. */ static_inline u32 f32_to_bits(f32 f) { u32 u; memcpy(&u, &f, sizeof(u)); @@ -1951,10 +1961,17 @@ static_inline u32 f32_to_bits(f32 f) { static_inline u64 f64_bits_inf(bool sign) { #if YYJSON_HAS_IEEE_754 return F64_BITS_INF | ((u64)sign << 63); -#elif defined(INFINITY) - return f64_to_bits(sign ? -INFINITY : INFINITY); #else - return f64_to_bits(sign ? -HUGE_VAL : HUGE_VAL); + return f64_to_bits(sign ? (f64)-INFINITY : (f64)INFINITY); +#endif +} + +/** Returns whether the double value is infinity (not NaN). */ +static_inline bool f64_is_inf(f64 val) { +#if YYJSON_HAS_IEEE_754 + return (f64_to_bits(val) & F64_EXP_MASK) == F64_BITS_INF; +#else + return val >= (f64)INFINITY || val <= (f64)-INFINITY; #endif } @@ -1962,10 +1979,8 @@ static_inline u64 f64_bits_inf(bool sign) { static_inline u64 f64_bits_nan(bool sign) { #if YYJSON_HAS_IEEE_754 return F64_BITS_NAN | ((u64)sign << 63); -#elif defined(NAN) - return f64_to_bits(sign ? (f64)-NAN : (f64)NAN); #else - return f64_to_bits((sign ? -0.0 : 0.0) / 0.0); + return f64_to_bits(sign ? (f64)-NAN : (f64)NAN); #endif } @@ -2086,6 +2101,8 @@ static_inline void u128_mul_add(u64 a, u64 b, u64 c, u64 *hi, u64 *lo) { * These functions are used to read and write JSON files. *============================================================================*/ +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + #define YYJSON_FOPEN_E #if !defined(_MSC_VER) && defined(__GLIBC__) && defined(__GLIBC_PREREQ) # if __GLIBC_PREREQ(2, 7) @@ -2120,6 +2137,8 @@ static_inline usize fread_safe(void *buf, usize size, FILE *file) { #endif } +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + /*============================================================================== @@ -2166,29 +2185,6 @@ static_inline void *mem_align_up(void *mem, usize align) { -/*============================================================================== - * MARK: - Default Memory Allocator (Private) - * This is a simple libc memory allocator wrapper. - *============================================================================*/ - -static void *default_malloc(void *ctx, usize size) { - return malloc(size); -} - -static void *default_realloc(void *ctx, void *ptr, usize old_size, usize size) { - return realloc(ptr, size); -} - -static void default_free(void *ctx, void *ptr) { - free(ptr); -} - -static const yyjson_alc YYJSON_DEFAULT_ALC = { - default_malloc, default_realloc, default_free, NULL -}; - - - /*============================================================================== * MARK: - Null Memory Allocator (Private) * This allocator is just a placeholder to ensure that the internal @@ -2213,6 +2209,44 @@ static const yyjson_alc YYJSON_NULL_ALC = { +/*============================================================================== + * MARK: - Default Memory Allocator (Private) + * This is a simple libc memory allocator wrapper. + *============================================================================*/ + +#if defined(YYJSON_CUSTOM_ALC) + +/* user-provided via macro */ +extern const yyjson_alc YYJSON_CUSTOM_ALC; +#define YYJSON_DEFAULT_ALC YYJSON_CUSTOM_ALC + +#elif YYJSON_FREESTANDING + +/* null allocator */ +static const yyjson_alc YYJSON_DEFAULT_ALC = { + null_malloc, null_realloc, null_free, NULL +}; + +#else /* YYJSON_FREESTANDING */ + +/* default libc allocator */ +static void *default_malloc(void *ctx, usize size) { + return malloc(size); +} +static void *default_realloc(void *ctx, void *ptr, usize old_size, usize size) { + return realloc(ptr, size); +} +static void default_free(void *ctx, void *ptr) { + free(ptr); +} +static const yyjson_alc YYJSON_DEFAULT_ALC = { + default_malloc, default_realloc, default_free, NULL +}; + +#endif /* YYJSON_FREESTANDING */ + + + /*============================================================================== * MARK: - Pool Memory Allocator (Public) * This allocator is initialized with a fixed-size buffer. @@ -2221,14 +2255,14 @@ static const yyjson_alc YYJSON_NULL_ALC = { /** memory chunk header */ typedef struct pool_chunk { - usize size; /* chunk memory size, include chunk header */ + usize size; /* chunk memory size, including chunk header */ struct pool_chunk *next; /* linked list, nullable */ /* char mem[]; flexible array member */ } pool_chunk; /** allocator ctx header */ typedef struct pool_ctx { - usize size; /* total memory size, include ctx header */ + usize size; /* total memory size, including ctx header */ pool_chunk *free_list; /* linked list, nullable */ /* pool_chunk chunks[]; flexible array member */ } pool_ctx; @@ -2380,7 +2414,7 @@ bool yyjson_alc_pool_init(yyjson_alc *alc, void *buf, usize size) { /** memory chunk header */ typedef struct dyn_chunk { - usize size; /* chunk size, include header */ + usize size; /* chunk size, including header */ struct dyn_chunk *next; /* char mem[]; flexible array member */ } dyn_chunk; @@ -2494,9 +2528,11 @@ static void dyn_free(void *ctx_ptr, void *ptr) { yyjson_alc *yyjson_alc_dyn_new(void) { const yyjson_alc def = YYJSON_DEFAULT_ALC; usize hdr_len = sizeof(yyjson_alc) + sizeof(dyn_ctx); - yyjson_alc *alc = (yyjson_alc *)def.malloc(def.ctx, hdr_len); - dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + yyjson_alc *alc; + dyn_ctx *ctx; + alc = (yyjson_alc *)def.malloc(def.ctx, hdr_len); if (unlikely(!alc)) return NULL; + ctx = (dyn_ctx *)(void *)(alc + 1); alc->malloc = dyn_malloc; alc->realloc = dyn_realloc; alc->free = dyn_free; @@ -2507,9 +2543,10 @@ yyjson_alc *yyjson_alc_dyn_new(void) { void yyjson_alc_dyn_free(yyjson_alc *alc) { const yyjson_alc def = YYJSON_DEFAULT_ALC; - dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + dyn_ctx *ctx; dyn_chunk *chunk, *next; if (unlikely(!alc)) return; + ctx = (dyn_ctx *)(void *)(alc + 1); for (chunk = ctx->free_list.next; chunk; chunk = next) { next = chunk->next; def.free(def.ctx, chunk); @@ -2642,7 +2679,8 @@ yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc) { return doc; } -yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, const yyjson_alc *alc) { +yyjson_mut_doc *yyjson_doc_mut_copy(const yyjson_doc *doc, + const yyjson_alc *alc) { yyjson_mut_doc *m_doc; yyjson_mut_val *m_val; @@ -2658,7 +2696,7 @@ yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, const yyjson_alc *alc) { return m_doc; } -yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, +yyjson_mut_doc *yyjson_mut_doc_mut_copy(const yyjson_mut_doc *doc, const yyjson_alc *alc) { yyjson_mut_doc *m_doc; yyjson_mut_val *m_val; @@ -2678,7 +2716,7 @@ yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, } yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *m_doc, - yyjson_val *i_vals) { + const yyjson_val *i_vals) { /* The immutable object or array stores all sub-values in a contiguous memory, We copy them to another contiguous memory as mutable values, @@ -2693,7 +2731,7 @@ yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *m_doc, i_vals_len = (usize)(unsafe_yyjson_get_next(i_vals) - i_vals); m_vals = unsafe_yyjson_mut_val(m_doc, i_vals_len); if (!m_vals) return NULL; - i_val = i_vals; + i_val = constcast(yyjson_val *)i_vals; m_val = m_vals; for (; i_val < i_end; i_val++, m_val++) { @@ -2743,8 +2781,8 @@ yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *m_doc, return m_vals; } -static yyjson_mut_val *unsafe_yyjson_mut_val_mut_copy(yyjson_mut_doc *m_doc, - yyjson_mut_val *m_vals) { +static yyjson_mut_val *unsafe_yyjson_mut_val_mut_copy( + yyjson_mut_doc *m_doc, const yyjson_mut_val *m_vals) { /* The mutable object or array stores all sub-values in a circular linked list, so we can traverse them in the same loop. The traversal starts from @@ -2790,13 +2828,13 @@ static yyjson_mut_val *unsafe_yyjson_mut_val_mut_copy(yyjson_mut_doc *m_doc, } yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, - yyjson_mut_val *val) { + const yyjson_mut_val *val) { if (doc && val) return unsafe_yyjson_mut_val_mut_copy(doc, val); return NULL; } /* Count the number of values and the total length of the strings. */ -static void yyjson_mut_stat(yyjson_mut_val *val, +static void yyjson_mut_stat(const yyjson_mut_val *val, usize *val_sum, usize *str_sum) { yyjson_type type = unsafe_yyjson_get_type(val); *val_sum += 1; @@ -2822,7 +2860,7 @@ static void yyjson_mut_stat(yyjson_mut_val *val, /* Copy mutable values to immutable value pool. */ static usize yyjson_imut_copy(yyjson_val **val_ptr, char **buf_ptr, - yyjson_mut_val *mval) { + const yyjson_mut_val *mval) { yyjson_val *val = *val_ptr; yyjson_type type = unsafe_yyjson_get_type(mval); if (type == YYJSON_TYPE_ARR || type == YYJSON_TYPE_OBJ) { @@ -2861,13 +2899,13 @@ static usize yyjson_imut_copy(yyjson_val **val_ptr, char **buf_ptr, } } -yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *mdoc, +yyjson_doc *yyjson_mut_doc_imut_copy(const yyjson_mut_doc *mdoc, const yyjson_alc *alc) { if (!mdoc) return NULL; return yyjson_mut_val_imut_copy(mdoc->root, alc); } -yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *mval, +yyjson_doc *yyjson_mut_val_imut_copy(const yyjson_mut_val *mval, const yyjson_alc *alc) { usize val_num = 0, str_sum = 0, hdr_size, buf_size; yyjson_doc *doc = NULL; @@ -2908,9 +2946,9 @@ yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *mval, return doc; } -static_inline bool unsafe_yyjson_num_equals(void *lhs, void *rhs) { - yyjson_val_uni *luni = &((yyjson_val *)lhs)->uni; - yyjson_val_uni *runi = &((yyjson_val *)rhs)->uni; +static_inline bool unsafe_yyjson_num_equals(const void *lhs, const void *rhs) { + const yyjson_val_uni *luni = &((const yyjson_val *)lhs)->uni; + const yyjson_val_uni *runi = &((const yyjson_val *)rhs)->uni; yyjson_subtype lt = unsafe_yyjson_get_subtype(lhs); yyjson_subtype rt = unsafe_yyjson_get_subtype(rhs); if (lt == rt) return luni->u64 == runi->u64; @@ -2923,14 +2961,14 @@ static_inline bool unsafe_yyjson_num_equals(void *lhs, void *rhs) { return false; } -static_inline bool unsafe_yyjson_str_equals(void *lhs, void *rhs) { +static_inline bool unsafe_yyjson_str_equals(const void *lhs, const void *rhs) { usize len = unsafe_yyjson_get_len(lhs); if (len != unsafe_yyjson_get_len(rhs)) return false; return !memcmp(unsafe_yyjson_get_str(lhs), unsafe_yyjson_get_str(rhs), len); } -bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { +bool unsafe_yyjson_equals(const yyjson_val *lhs, const yyjson_val *rhs) { yyjson_type type = unsafe_yyjson_get_type(lhs); if (type != unsafe_yyjson_get_type(rhs)) return false; @@ -2985,7 +3023,8 @@ bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { } } -bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, yyjson_mut_val *rhs) { +bool unsafe_yyjson_mut_equals(const yyjson_mut_val *lhs, + const yyjson_mut_val *rhs) { yyjson_type type = unsafe_yyjson_get_type(lhs); if (type != unsafe_yyjson_get_type(rhs)) return false; @@ -2995,7 +3034,7 @@ bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, yyjson_mut_val *rhs) { if (len != unsafe_yyjson_get_len(rhs)) return false; if (len > 0) { yyjson_mut_obj_iter iter; - yyjson_mut_obj_iter_init(rhs, &iter); + yyjson_mut_obj_iter_init(constcast(yyjson_mut_val *)rhs, &iter); lhs = (yyjson_mut_val *)lhs->uni.ptr; while (len-- > 0) { rhs = yyjson_mut_obj_iter_getn(&iter, lhs->uni.str, @@ -3700,7 +3739,7 @@ static_noinline void bigint_set_buf(bigint *big, u64 sig, i32 *exp, u64 val = 0; bool dig_big_cut = false; bool has_dot = (hdr < dot_pos) & (dot_pos < sig_end); - u32 dig_len_total = U64_SAFE_DIG + (u32)(sig_end - hdr) - has_dot; + usize dig_len_total = U64_SAFE_DIG + (usize)(sig_end - hdr) - has_dot; sig -= (*sig_cut >= '5'); /* sig was rounded before */ if (dig_len_total > F64_MAX_DEC_DIG) { @@ -3748,8 +3787,8 @@ typedef struct diy_fp { i32 pad; /* padding, useless */ } diy_fp; -/** Get cached rounded diy_fp with pow(10, e) The input value must in range - [POW10_SIG_TABLE_MIN_EXP, POW10_SIG_TABLE_MAX_EXP]. */ +/** Get cached rounded diy_fp for pow(10, e). The input value must be in the + range [POW10_SIG_TABLE_MIN_EXP, POW10_SIG_TABLE_MAX_EXP]. */ static_inline diy_fp diy_fp_get_cached_pow10(i32 exp10) { diy_fp fp; u64 sig_ext; @@ -4142,7 +4181,7 @@ static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, 1. The floating-point number calculation should be accurate, see the comments of macro `YYJSON_DOUBLE_MATH_CORRECT`. 2. Correct rounding should be performed (fegetround() == FE_TONEAREST). - 3. The input of floating point number calculation does not lose precision, + 3. The input to floating-point calculations does not lose precision, which means: 64 - leading_zero(input) - trailing_zero(input) < 53. We don't check all available inputs here, because that would make the code @@ -4161,12 +4200,13 @@ static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, return_f64(dbl); } #endif + if (unlikely(sig == 0)) return_f64_bin(0); /* Fast path 2: To keep it simple, we only accept normal number here, - let the slow path to handle subnormal and infinity number. + let the slow path handle subnormal and infinite numbers. */ if (likely(!sig_cut && exp > -F64_MAX_DEC_EXP + 1 && @@ -4632,7 +4672,7 @@ static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, return_err(hdr, "strtod() failed to parse the number"); } } - if (unlikely(val->uni.f64 >= HUGE_VAL || val->uni.f64 <= -HUGE_VAL)) { + if (unlikely(f64_is_inf(val->uni.f64))) { return_inf(); } val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; @@ -4742,7 +4782,6 @@ static_inline bool read_str_opt(u8 quo, u8 **ptr, u8 *eof, yyjson_read_flag flg, u8 *hdr = *ptr + 1; u8 **end = ptr; u8 *src = hdr, *dst = NULL, *pos; - u16 hi, lo; u32 uni, tmp; /* Resume incremental parsing. */ @@ -5074,7 +5113,6 @@ static_noinline bool read_str_id(u8 **ptr, u8 *eof, yyjson_read_flag flg, u8 *hdr = *ptr; u8 **end = ptr; u8 *src = hdr, *dst = NULL; - u16 hi, lo; u32 uni, tmp; /* add null-terminator for previous raw string */ @@ -5306,6 +5344,7 @@ fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); fail_character: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); +fail_depth: return_err(cur, DEPTH, MSG_DEPTH); #undef return_err } @@ -5366,6 +5405,10 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, u8 *raw_ptr = raw_end; u8 **pre = &raw_ptr; /* previous raw end pointer */ +#if YYJSON_READER_DEPTH_LIMIT + usize ctn_depth = 0; /* current array/object depth */ +#endif + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; @@ -5391,6 +5434,12 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, } arr_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif /* save current container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -5450,7 +5499,7 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, cur++; if (likely(ctn_len == 0)) goto arr_end; if (has_allow(TRAILING_COMMAS)) goto arr_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -5496,6 +5545,9 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, goto fail_character_arr_end; arr_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif /* get parent container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); @@ -5514,6 +5566,12 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, } obj_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif /* push container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -5535,7 +5593,7 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, cur++; if (likely(ctn_len == 0)) goto obj_end; if (has_allow(TRAILING_COMMAS)) goto obj_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -5660,6 +5718,9 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, goto fail_character_obj_end; obj_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif /* pop container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); /* point to the next value */ @@ -5709,6 +5770,7 @@ fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); +fail_depth: return_err(cur, DEPTH, MSG_DEPTH); #undef val_incr #undef return_err @@ -5769,6 +5831,9 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, u8 raw_end[1]; /* raw end for null-terminator */ u8 *raw_ptr = raw_end; u8 **pre = &raw_ptr; /* previous raw end pointer */ +#if YYJSON_READER_DEPTH_LIMIT + usize ctn_depth = 0; /* current array/object depth */ +#endif dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); @@ -5797,6 +5862,13 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, } arr_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif + /* save current container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -5869,7 +5941,7 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, cur++; if (likely(ctn_len == 0)) goto arr_end; if (has_allow(TRAILING_COMMAS)) goto arr_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -5919,6 +5991,9 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, goto fail_character_arr_end; arr_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif /* get parent container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); @@ -5938,6 +6013,13 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, } obj_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif + /* push container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -5971,7 +6053,7 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, cur++; if (likely(ctn_len == 0)) goto obj_end; if (has_allow(TRAILING_COMMAS)) goto obj_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -6104,6 +6186,10 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, goto fail_character_obj_end; obj_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif + /* pop container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); /* point to the next value */ @@ -6154,6 +6240,7 @@ fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); +fail_depth: return_err(cur, DEPTH, MSG_DEPTH); #undef val_incr #undef return_err @@ -6253,6 +6340,8 @@ yyjson_doc *yyjson_read_opts(char *dat, usize len, #undef return_err } +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + yyjson_doc *yyjson_read_file(const char *path, yyjson_read_flag flg, const yyjson_alc *alc_ptr, @@ -6300,6 +6389,7 @@ yyjson_doc *yyjson_read_fp(FILE *file, long file_size = 0, file_pos; void *buf = NULL; usize buf_size = 0; + usize dat_size = 0; /* validate input parameters */ if (!err) err = &tmp_err; @@ -6309,22 +6399,26 @@ yyjson_doc *yyjson_read_fp(FILE *file, file_pos = ftell(file); if (file_pos != -1) { /* get total file size, may fail */ - if (fseek(file, 0, SEEK_END) == 0) file_size = ftell(file); + if (fseek(file, 0, SEEK_END) == 0) { + file_size = ftell(file); + if (file_size == -1) file_size = 0; + } /* reset to original position, may fail */ if (fseek(file, file_pos, SEEK_SET) != 0) file_size = 0; - /* get file size from current postion to end */ + /* get file size from current position to end */ if (file_size > 0) file_size -= file_pos; } /* read file */ if (file_size > 0) { /* read the entire file in one call */ - buf_size = (usize)file_size + YYJSON_PADDING_SIZE; + dat_size = (usize)file_size; + buf_size = dat_size + YYJSON_PADDING_SIZE; buf = alc.malloc(alc.ctx, buf_size); if (buf == NULL) { return_err(MEMORY_ALLOCATION, MSG_MALLOC); } - if (fread_safe(buf, (usize)file_size, file) != (usize)file_size) { + if (fread_safe(buf, dat_size, file) != dat_size) { return_err(FILE_READ, MSG_FREAD); } } else { @@ -6351,8 +6445,11 @@ yyjson_doc *yyjson_read_fp(FILE *file, } tmp = ((u8 *)buf) + buf_size - YYJSON_PADDING_SIZE - chunk_now; read_size = fread_safe(tmp, chunk_now, file); - file_size += (long)read_size; - if (read_size != chunk_now) break; + dat_size += read_size; + if (read_size != chunk_now) { + if (ferror(file)) return_err(FILE_READ, MSG_FREAD); + break; + } chunk_now *= 2; if (chunk_now > chunk_max) chunk_now = chunk_max; @@ -6360,9 +6457,9 @@ yyjson_doc *yyjson_read_fp(FILE *file, } /* read JSON */ - memset((u8 *)buf + file_size, 0, YYJSON_PADDING_SIZE); + memset((u8 *)buf + dat_size, 0, YYJSON_PADDING_SIZE); flg |= YYJSON_READ_INSITU; - doc = yyjson_read_opts((char *)buf, (usize)file_size, flg, &alc, err); + doc = yyjson_read_opts((char *)buf, dat_size, flg, &alc, err); if (doc) { doc->str_pool = (char *)buf; return doc; @@ -6374,6 +6471,8 @@ yyjson_doc *yyjson_read_fp(FILE *file, #undef return_err } +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + const char *yyjson_read_number(const char *dat, yyjson_val *val, yyjson_read_flag flg, @@ -6471,11 +6570,16 @@ struct yyjson_incr_state { usize hdr_len; /* value count used by yyjson_doc */ usize alc_len; /* value count allocated */ usize ctn_len; /* the number of elements in current container */ +#if YYJSON_READER_DEPTH_LIMIT + usize ctn_depth; /* current array/object depth */ +#endif yyjson_val *val_hdr; /* the head of allocated values */ yyjson_val *val_end; /* the end of allocated values */ yyjson_val *val; /* current JSON value */ yyjson_val *ctn; /* current container */ u8 *str_con[2]; /* string parser incremental state */ + u8 *raw_ptr; /* pending position for a deferred raw null-terminator */ + u8 raw_end[1]; /* dummy target for the first deferred null-terminator */ }; yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, @@ -6511,6 +6615,7 @@ yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, } memset(state->hdr + buf_len, 0, YYJSON_PADDING_SIZE); state->cur = state->hdr; + state->raw_ptr = state->raw_end; state->label = LABEL_doc_begin; return state; } @@ -6569,12 +6674,19 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, } while (false) /* save position where it's possible to resume incremental parsing */ +#if YYJSON_READER_DEPTH_LIMIT +#define save_incr_depth() (state->ctn_depth = ctn_depth) +#else +#define save_incr_depth() ((void)0) +#endif #define save_incr_state(_label) do { \ state->label = LABEL_##_label; \ state->cur = cur; \ state->val = val; \ state->ctn_len = ctn_len; \ + save_incr_depth(); \ state->hdr_len = hdr_len; \ + state->raw_ptr = raw_ptr; \ if (unlikely(cur >= end)) goto unexpected_end; \ } while (false) @@ -6606,12 +6718,15 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, const char *msg; /* error message */ yyjson_read_err tmp_err; - u8 raw_end[1]; /* raw end for null-terminator */ - u8 *raw_ptr = raw_end; + u8 *raw_ptr; /* deferred raw null-terminator position, committed at save */ u8 **pre = &raw_ptr; /* previous raw end pointer */ u8 **con = NULL; /* for incremental string parsing */ u8 saved_end = '\0'; /* saved end char */ +#if YYJSON_READER_DEPTH_LIMIT + usize ctn_depth = 0; /* current array/object depth */ +#endif + /* validate input parameters */ if (!err) err = &tmp_err; if (unlikely(!state)) { @@ -6631,6 +6746,9 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, flg = state->flg; alc = state->alc; ctn_len = state->ctn_len; +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth = state->ctn_depth; +#endif hdr_len = state->hdr_len; alc_len = state->alc_len; val = state->val; @@ -6638,6 +6756,7 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, val_end = state->val_end; ctn = state->ctn; con = state->str_con; + raw_ptr = state->raw_ptr; alc_max = USIZE_MAX / sizeof(yyjson_val); /* insert null terminator to make us stop at the specified end, even if @@ -6703,7 +6822,11 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, goto arr_val_begin; } if (char_is_num(*cur)) { - if (likely(read_num(&cur, pre, flg, val, &msg))) goto doc_end; + if (likely(read_num(&cur, pre, flg, val, &msg))) { + /* a root number may continue with more digits in a later chunk */ + if (unlikely(len < state->buf_len)) check_maybe_truncated_number(); + goto doc_end; + } goto fail_number; } if (*cur == '"') { @@ -6733,6 +6856,13 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, return_err(cur, UNEXPECTED_CHARACTER, msg); arr_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif + /* save current container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -6791,7 +6921,7 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, if (*cur == ']') { cur++; if (likely(ctn_len == 0)) goto arr_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -6822,6 +6952,9 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, goto fail_character_arr_end; arr_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif /* get parent container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); @@ -6840,6 +6973,13 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, } obj_begin: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_READER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif + /* push container */ ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | (ctn->tag & YYJSON_TAG_MASK); @@ -6863,7 +7003,7 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, if (likely(*cur == '}')) { cur++; if (likely(ctn_len == 0)) goto obj_end; - while (*cur != ',') cur--; + do { cur--; } while (*cur != ','); goto fail_trailing_comma; } if (char_is_space(*cur)) { @@ -6954,6 +7094,10 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, goto fail_character_obj_end; obj_end: +#if YYJSON_READER_DEPTH_LIMIT + ctn_depth--; +#endif + /* pop container */ ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); /* point to the next value */ @@ -6970,10 +7114,15 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, doc_end: /* check invalid contents after json document */ - if (unlikely(cur < end) && !has_flg(STOP_WHEN_DONE)) { + if (unlikely(cur < end || len < state->buf_len) && + !has_flg(STOP_WHEN_DONE)) { save_incr_state(doc_end); while (char_is_space(*cur)) cur++; if (unlikely(cur < end)) goto fail_garbage; + /* the document is complete for the bytes seen so far, but more input + is still pending; it may hold trailing content that has to be + rejected, so request the remaining data before finalizing */ + if (unlikely(len < state->buf_len)) goto unexpected_end; } **pre = '\0'; @@ -6990,7 +7139,7 @@ yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, unexpected_end: err->pos = len; - /* if no nore data, stop the incr read */ + /* if no more data, stop the incr read */ if (unlikely(len >= state->buf_len)) { err->code = YYJSON_READ_ERROR_UNEXPECTED_END; err->msg = MSG_NOT_END; @@ -7020,11 +7169,13 @@ fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); +fail_depth: return_err(cur, DEPTH, MSG_DEPTH); #undef val_incr #undef return_err #undef return_err_inv_param #undef save_incr_state +#undef save_incr_depth #undef check_maybe_truncated_number } @@ -7230,7 +7381,7 @@ static_inline u8 *write_u64(u64 val, u8 *buf) { #if !YYJSON_DISABLE_FAST_FP_CONV /* FP_WRITER */ /** Trailing zero count table for number 0 to 99. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const u8 dec_trailing_zero_table[] = { 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -7742,7 +7893,7 @@ static_inline u8 *write_inf_or_nan(u8 *buf, yyjson_write_flag flg, We follow the ECMAScript specification for printing floating-point numbers, similar to `Number.prototype.toString()`, but with the following changes: 1. Keep the negative sign of `-0.0` to preserve input information. - 2. Keep decimal point to indicate the number is floating point. + 2. Keep the decimal point to indicate that the number is floating-point. 3. Remove positive sign in the exponent part. */ static_noinline u8 *write_f32_raw(u8 *buf, u64 raw_f64, @@ -7815,7 +7966,7 @@ static_noinline u8 *write_f32_raw(u8 *buf, u64 raw_f64, num_hdr = buf + pre_ofs; num_end = write_u32_len_7_to_9_trim(sig_dec, num_hdr); - /* seperate these digits to leave a space for dot */ + /* separate these digits to leave a space for dot */ num_sep_pos = no_pre_zero ? dot_ofs : 0; num_sep = num_hdr + num_sep_pos; byte_move_8(num_sep + no_pre_zero, num_sep); @@ -7869,7 +8020,7 @@ static_noinline u8 *write_f32_raw(u8 *buf, u64 raw_f64, We follow the ECMAScript specification for printing floating-point numbers, similar to `Number.prototype.toString()`, but with the following changes: 1. Keep the negative sign of `-0.0` to preserve input information. - 2. Keep decimal point to indicate the number is floating point. + 2. Keep the decimal point to indicate that the number is floating-point. 3. Remove positive sign in the exponent part. */ static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { @@ -7938,7 +8089,7 @@ static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { num_hdr = buf + pre_ofs; num_end = write_u64_len_16_to_17_trim(sig_dec, num_hdr); - /* seperate these digits to leave a space for dot */ + /* separate these digits to leave a space for dot */ num_sep_pos = no_pre_zero ? dot_ofs : 0; num_sep = num_hdr + num_sep_pos; byte_move_16(num_sep + no_pre_zero, num_sep); @@ -7993,7 +8144,7 @@ static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { We follow the ECMAScript specification for printing floating-point numbers, similar to `Number.prototype.toFixed(prec)`, but with the following changes: 1. Keep the negative sign of `-0.0` to preserve input information. - 2. Keep decimal point to indicate the number is floating point. + 2. Keep the decimal point to indicate that the number is floating-point. 3. Remove positive sign in the exponent part. 4. Remove trailing zeros and reduce unnecessary precision. */ @@ -8098,7 +8249,7 @@ static_noinline u8 *write_f64_raw_fixed(u8 *buf, u64 raw, yyjson_write_flag flg, num_hdr = buf + pre_ofs; num_end = write_u64_len_1_to_17(sig_dec, num_hdr); - /* seperate these digits to leave a space for dot */ + /* separate these digits to leave a space for dot */ num_sep_pos = no_pre_zero ? dot_ofs : -dot_ofs; num_sep = buf + num_sep_pos; byte_move_16(num_sep + 1, num_sep); @@ -8353,7 +8504,7 @@ typedef u8 char_enc_type; #define CHAR_ENC_ESC_4 9 /* 4-byte UTF-8, escaped as '\uXXXX\uXXXX'. */ /** Character encode type table: don't escape unicode, don't escape '/'. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const char_enc_type enc_table_cpy[256] = { 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, @@ -8374,7 +8525,7 @@ static const char_enc_type enc_table_cpy[256] = { }; /** Character encode type table: don't escape unicode, escape '/'. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const char_enc_type enc_table_cpy_slash[256] = { 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, @@ -8395,7 +8546,7 @@ static const char_enc_type enc_table_cpy_slash[256] = { }; /** Character encode type table: escape unicode, don't escape '/'. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const char_enc_type enc_table_esc[256] = { 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, @@ -8416,7 +8567,7 @@ static const char_enc_type enc_table_esc[256] = { }; /** Character encode type table: escape unicode, escape '/'. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ static const char_enc_type enc_table_esc_slash[256] = { 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, @@ -8437,7 +8588,7 @@ static const char_enc_type enc_table_esc_slash[256] = { }; /** Escaped hex character table: ["00" "01" "02" ... "FD" "FE" "FF"]. - (generate with misc/make_tables.c) */ + (generated with misc/make_tables.c) */ yyjson_align(2) static const u8 esc_hex_char_table[512] = { '0', '0', '0', '1', '0', '2', '0', '3', @@ -8506,7 +8657,76 @@ static const u8 esc_hex_char_table[512] = { 'F', 'C', 'F', 'D', 'F', 'E', 'F', 'F' }; -/** Escaped single character table. (generate with misc/make_tables.c) */ +/** Lowercase variant of esc_hex_char_table. */ +yyjson_align(2) +static const u8 esc_hex_char_table_lower[512] = { + '0', '0', '0', '1', '0', '2', '0', '3', + '0', '4', '0', '5', '0', '6', '0', '7', + '0', '8', '0', '9', '0', 'a', '0', 'b', + '0', 'c', '0', 'd', '0', 'e', '0', 'f', + '1', '0', '1', '1', '1', '2', '1', '3', + '1', '4', '1', '5', '1', '6', '1', '7', + '1', '8', '1', '9', '1', 'a', '1', 'b', + '1', 'c', '1', 'd', '1', 'e', '1', 'f', + '2', '0', '2', '1', '2', '2', '2', '3', + '2', '4', '2', '5', '2', '6', '2', '7', + '2', '8', '2', '9', '2', 'a', '2', 'b', + '2', 'c', '2', 'd', '2', 'e', '2', 'f', + '3', '0', '3', '1', '3', '2', '3', '3', + '3', '4', '3', '5', '3', '6', '3', '7', + '3', '8', '3', '9', '3', 'a', '3', 'b', + '3', 'c', '3', 'd', '3', 'e', '3', 'f', + '4', '0', '4', '1', '4', '2', '4', '3', + '4', '4', '4', '5', '4', '6', '4', '7', + '4', '8', '4', '9', '4', 'a', '4', 'b', + '4', 'c', '4', 'd', '4', 'e', '4', 'f', + '5', '0', '5', '1', '5', '2', '5', '3', + '5', '4', '5', '5', '5', '6', '5', '7', + '5', '8', '5', '9', '5', 'a', '5', 'b', + '5', 'c', '5', 'd', '5', 'e', '5', 'f', + '6', '0', '6', '1', '6', '2', '6', '3', + '6', '4', '6', '5', '6', '6', '6', '7', + '6', '8', '6', '9', '6', 'a', '6', 'b', + '6', 'c', '6', 'd', '6', 'e', '6', 'f', + '7', '0', '7', '1', '7', '2', '7', '3', + '7', '4', '7', '5', '7', '6', '7', '7', + '7', '8', '7', '9', '7', 'a', '7', 'b', + '7', 'c', '7', 'd', '7', 'e', '7', 'f', + '8', '0', '8', '1', '8', '2', '8', '3', + '8', '4', '8', '5', '8', '6', '8', '7', + '8', '8', '8', '9', '8', 'a', '8', 'b', + '8', 'c', '8', 'd', '8', 'e', '8', 'f', + '9', '0', '9', '1', '9', '2', '9', '3', + '9', '4', '9', '5', '9', '6', '9', '7', + '9', '8', '9', '9', '9', 'a', '9', 'b', + '9', 'c', '9', 'd', '9', 'e', '9', 'f', + 'a', '0', 'a', '1', 'a', '2', 'a', '3', + 'a', '4', 'a', '5', 'a', '6', 'a', '7', + 'a', '8', 'a', '9', 'a', 'a', 'a', 'b', + 'a', 'c', 'a', 'd', 'a', 'e', 'a', 'f', + 'b', '0', 'b', '1', 'b', '2', 'b', '3', + 'b', '4', 'b', '5', 'b', '6', 'b', '7', + 'b', '8', 'b', '9', 'b', 'a', 'b', 'b', + 'b', 'c', 'b', 'd', 'b', 'e', 'b', 'f', + 'c', '0', 'c', '1', 'c', '2', 'c', '3', + 'c', '4', 'c', '5', 'c', '6', 'c', '7', + 'c', '8', 'c', '9', 'c', 'a', 'c', 'b', + 'c', 'c', 'c', 'd', 'c', 'e', 'c', 'f', + 'd', '0', 'd', '1', 'd', '2', 'd', '3', + 'd', '4', 'd', '5', 'd', '6', 'd', '7', + 'd', '8', 'd', '9', 'd', 'a', 'd', 'b', + 'd', 'c', 'd', 'd', 'd', 'e', 'd', 'f', + 'e', '0', 'e', '1', 'e', '2', 'e', '3', + 'e', '4', 'e', '5', 'e', '6', 'e', '7', + 'e', '8', 'e', '9', 'e', 'a', 'e', 'b', + 'e', 'c', 'e', 'd', 'e', 'e', 'e', 'f', + 'f', '0', 'f', '1', 'f', '2', 'f', '3', + 'f', '4', 'f', '5', 'f', '6', 'f', '7', + 'f', '8', 'f', '9', 'f', 'a', 'f', 'b', + 'f', 'c', 'f', 'd', 'f', 'e', 'f', 'f' +}; + +/** Escaped single character table. (generated with misc/make_tables.c) */ yyjson_align(2) static const u8 esc_single_char_table[512] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', @@ -8575,6 +8795,13 @@ static const u8 esc_single_char_table[512] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }; +/** Returns the hex digit table to use for \uXXXX escapes. */ +static_inline const u8 *get_hex_table_with_flag(yyjson_write_flag flg) { + return has_flg(LOWERCASE_HEX) + ? esc_hex_char_table_lower + : esc_hex_char_table; +} + /** Returns the encode table with options. */ static_inline const char_enc_type *get_enc_table_with_flag( yyjson_write_flag flg) { @@ -8640,9 +8867,11 @@ static_inline u8 *write_str_noesc(u8 *cur, const u8 *str, usize str_len) { */ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, const u8 *str, usize str_len, - const char_enc_type *enc_table) { - /* The replacement character U+FFFD, used to indicate invalid character. */ - const v32 rep = {{ 'F', 'F', 'F', 'D' }}; + const char_enc_type *enc_table, + const u8 *hex_table) { + /* The replacement character U+FFFD, used to indicate invalid character. + Looked up via hex_table so that LOWERCASE_HEX produces "fffd" while + the default produces "FFFD". */ const v32 pre = {{ '\\', 'u', '0', '0' }}; const u8 *src = str; @@ -8759,7 +8988,7 @@ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, } case CHAR_ENC_ESC_1: { byte_copy_4(cur + 0, &pre); - byte_copy_2(cur + 4, &esc_hex_char_table[*src * 2]); + byte_copy_2(cur + 4, &hex_table[*src * 2]); cur += 6; src += 1; goto copy_utf8; @@ -8775,8 +9004,8 @@ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, u = (u16)(((u16)(src[0] & 0x1F) << 6) | ((u16)(src[1] & 0x3F) << 0)); byte_copy_2(cur + 0, &pre); - byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); - byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + byte_copy_2(cur + 2, &hex_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &hex_table[(u & 0xFF) * 2]); cur += 6; src += 2; goto copy_utf8; @@ -8792,8 +9021,8 @@ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, ((u16)(src[1] & 0x3F) << 6) | ((u16)(src[2] & 0x3F) << 0)); byte_copy_2(cur + 0, &pre); - byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); - byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + byte_copy_2(cur + 2, &hex_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &hex_table[(u & 0xFF) * 2]); cur += 6; src += 3; goto copy_utf8; @@ -8812,11 +9041,11 @@ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, hi = (u >> 10) + 0xD800; lo = (u & 0x3FF) + 0xDC00; byte_copy_2(cur + 0, &pre); - byte_copy_2(cur + 2, &esc_hex_char_table[(hi >> 8) * 2]); - byte_copy_2(cur + 4, &esc_hex_char_table[(hi & 0xFF) * 2]); + byte_copy_2(cur + 2, &hex_table[(hi >> 8) * 2]); + byte_copy_2(cur + 4, &hex_table[(hi & 0xFF) * 2]); byte_copy_2(cur + 6, &pre); - byte_copy_2(cur + 8, &esc_hex_char_table[(lo >> 8) * 2]); - byte_copy_2(cur + 10, &esc_hex_char_table[(lo & 0xFF) * 2]); + byte_copy_2(cur + 8, &hex_table[(lo >> 8) * 2]); + byte_copy_2(cur + 10, &hex_table[(lo & 0xFF) * 2]); cur += 12; src += 4; goto copy_utf8; @@ -8843,7 +9072,12 @@ static_inline u8 *write_str(u8 *cur, bool esc, bool inv, err_esc: if (!inv) return NULL; byte_copy_2(cur + 0, &pre); - byte_copy_4(cur + 2, &rep); + /* U+FFFD = 0xFFFD, written as two pairs from hex_table so that + LOWERCASE_HEX produces "fffd". Replaces a single byte_copy_4 + from a hardcoded uppercase "FFFD" v32; same total output, one + extra load on the (rare) invalid-UTF-8-with-ALLOW path. */ + byte_copy_2(cur + 2, &hex_table[0xFF * 2]); + byte_copy_2(cur + 4, &hex_table[0xFD * 2]); cur += 6; src += 1; goto copy_utf8; @@ -8884,10 +9118,12 @@ static_inline u8 *write_indent(u8 *cur, usize level, usize spaces) { return cur; } +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Write data to file pointer. */ static bool write_dat_to_fp(FILE *fp, u8 *dat, usize len, yyjson_write_err *err) { - if (fwrite(dat, len, 1, fp) != 1) { + if (fwrite(dat, 1, len, fp) != len) { err->msg = "file writing failed"; err->code = YYJSON_WRITE_ERROR_FILE_WRITE; return false; @@ -8909,7 +9145,7 @@ static bool write_dat_to_file(const char *path, u8 *dat, usize len, if (file == NULL) { return_err(FILE_OPEN, MSG_FOPEN); } - if (fwrite(dat, len, 1, file) != 1) { + if (fwrite(dat, 1, len, file) != len) { return_err(FILE_WRITE, MSG_FWRITE); } if (fclose(file) != 0) { @@ -8921,6 +9157,8 @@ static bool write_dat_to_file(const char *path, u8 *dat, usize len, #undef return_err } +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + /*============================================================================== @@ -8944,11 +9182,11 @@ static_inline void yyjson_write_ctx_get(yyjson_write_ctx *ctx, } /** Write single JSON value. */ -static_inline u8 *yyjson_write_single(yyjson_val *val, - yyjson_write_flag flg, - yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { +static_inline u8 *write_root_single(yyjson_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { #define return_err(_code, _msg) do { \ if (hdr) alc.free(alc.ctx, (void *)hdr); \ *dat_len = 0; \ @@ -8958,7 +9196,8 @@ static_inline u8 *yyjson_write_single(yyjson_val *val, } while (false) #define incr_len(_len) do { \ - hdr = (u8 *)alc.malloc(alc.ctx, _len); \ + if (buf) hdr = *dat_len >= _len ? (u8 *)buf : (u8 *)NULL; \ + else hdr = (u8 *)alc.malloc(alc.ctx, _len); \ if (!hdr) goto fail_alloc; \ cur = hdr; \ } while (false) @@ -8972,6 +9211,7 @@ static_inline u8 *yyjson_write_single(yyjson_val *val, usize str_len; const u8 *str_ptr; const char_enc_type *enc_table = get_enc_table_with_flag(flg); + const u8 *hex_table = get_hex_table_with_flag(flg); bool cpy = (enc_table == enc_table_cpy); bool esc = has_flg(ESCAPE_UNICODE) != 0; bool inv = has_allow(INVALID_UNICODE) != 0; @@ -8995,7 +9235,8 @@ static_inline u8 *yyjson_write_single(yyjson_val *val, if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { cur = write_str_noesc(cur, str_ptr, str_len); } else { - cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + cur = write_str(cur, esc, inv, str_ptr, str_len, + enc_table, hex_table); if (unlikely(!cur)) goto fail_str; } break; @@ -9050,11 +9291,11 @@ fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); /** Write JSON document minify. The root of this document should be a non-empty container. */ -static_inline u8 *yyjson_write_minify(const yyjson_val *root, - const yyjson_write_flag flg, - const yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { +static_inline u8 *write_root_minify(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { #define return_err(_code, _msg) do { \ *dat_len = 0; \ err->code = YYJSON_WRITE_ERROR_##_code; \ @@ -9068,7 +9309,8 @@ static_inline u8 *yyjson_write_minify(const yyjson_val *root, if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ usize ctx_pos = (usize)((u8 *)ctx - hdr); \ usize cur_pos = (usize)(cur - hdr); \ - ctx_len = (usize)(end - (u8 *)ctx); \ + yyjson_assume((u8 *)ctx <= (u8 *)end); \ + ctx_len = (usize)((u8 *)end - (u8 *)ctx); \ alc_inc = yyjson_max(alc_len / 2, ext_len); \ alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ @@ -9097,18 +9339,29 @@ static_inline u8 *yyjson_write_minify(const yyjson_val *root, u8 *hdr, *cur, *end, *tmp; yyjson_write_ctx *ctx, *ctx_tmp; usize alc_len, alc_inc, ctx_len, ext_len, str_len; +#if YYJSON_WRITER_DEPTH_LIMIT + usize ctn_depth = 0; +#endif const u8 *str_ptr; const char_enc_type *enc_table = get_enc_table_with_flag(flg); + const u8 *hex_table = get_hex_table_with_flag(flg); bool cpy = (enc_table == enc_table_cpy); bool esc = has_flg(ESCAPE_UNICODE) != 0; bool inv = has_allow(INVALID_UNICODE) != 0; bool newline = has_flg(NEWLINE_AT_END) != 0; - alc_len = root->uni.ofs / sizeof(yyjson_val); - alc_len = alc_len * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; - alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); - hdr = (u8 *)alc.malloc(alc.ctx, alc_len); - if (!hdr) goto fail_alloc; + if (buf) { + hdr = (u8 *)buf; + alc_len = *dat_len; + alc_len = size_align_down(alc_len, sizeof(yyjson_write_ctx)); + if (alc_len <= sizeof(yyjson_write_ctx)) goto fail_alloc; + } else { + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + } cur = hdr; end = hdr + alc_len; ctx = (yyjson_write_ctx *)(void *)end; @@ -9132,7 +9385,8 @@ static_inline u8 *yyjson_write_minify(const yyjson_val *root, if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { cur = write_str_noesc(cur, str_ptr, str_len); } else { - cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + cur = write_str(cur, esc, inv, str_ptr, str_len, + enc_table, hex_table); if (unlikely(!cur)) goto fail_str; } *cur++ = is_key ? ':' : ','; @@ -9149,8 +9403,17 @@ static_inline u8 *yyjson_write_minify(const yyjson_val *root, (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { ctn_len_tmp = unsafe_yyjson_get_len(val); ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); - incr_len(16); + incr_len(2 * sizeof(*ctx)); +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_WRITER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif if (unlikely(ctn_len_tmp == 0)) { +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif /* write empty container */ *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); @@ -9196,6 +9459,9 @@ static_inline u8 *yyjson_write_minify(const yyjson_val *root, goto val_begin; ctn_end: +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif cur--; *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); *cur++ = ','; @@ -9223,6 +9489,9 @@ fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); +#if YYJSON_WRITER_DEPTH_LIMIT +fail_depth: return_err(DEPTH, MSG_DEPTH); +#endif #undef return_err #undef incr_len @@ -9231,11 +9500,11 @@ fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); /** Write JSON document pretty. The root of this document should be a non-empty container. */ -static_inline u8 *yyjson_write_pretty(const yyjson_val *root, - const yyjson_write_flag flg, - const yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { +static_inline u8 *write_root_pretty(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { #define return_err(_code, _msg) do { \ *dat_len = 0; \ err->code = YYJSON_WRITE_ERROR_##_code; \ @@ -9249,7 +9518,8 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ usize ctx_pos = (usize)((u8 *)ctx - hdr); \ usize cur_pos = (usize)(cur - hdr); \ - ctx_len = (usize)(end - (u8 *)ctx); \ + yyjson_assume((u8 *)ctx <= (u8 *)end); \ + ctx_len = (usize)((u8 *)end - (u8 *)ctx); \ alc_inc = yyjson_max(alc_len / 2, ext_len); \ alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ @@ -9278,19 +9548,30 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, u8 *hdr, *cur, *end, *tmp; yyjson_write_ctx *ctx, *ctx_tmp; usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; +#if YYJSON_WRITER_DEPTH_LIMIT + usize ctn_depth = 0; +#endif const u8 *str_ptr; const char_enc_type *enc_table = get_enc_table_with_flag(flg); + const u8 *hex_table = get_hex_table_with_flag(flg); bool cpy = (enc_table == enc_table_cpy); bool esc = has_flg(ESCAPE_UNICODE) != 0; bool inv = has_allow(INVALID_UNICODE) != 0; usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; bool newline = has_flg(NEWLINE_AT_END) != 0; - alc_len = root->uni.ofs / sizeof(yyjson_val); - alc_len = alc_len * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; - alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); - hdr = (u8 *)alc.malloc(alc.ctx, alc_len); - if (!hdr) goto fail_alloc; + if (buf) { + hdr = (u8 *)buf; + alc_len = *dat_len; + alc_len = size_align_down(alc_len, sizeof(yyjson_write_ctx)); + if (alc_len <= sizeof(yyjson_write_ctx)) goto fail_alloc; + } else { + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + } cur = hdr; end = hdr + alc_len; ctx = (yyjson_write_ctx *)(void *)end; @@ -9313,12 +9594,15 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, str_len = unsafe_yyjson_get_len(val); str_ptr = (const u8 *)unsafe_yyjson_get_str(val); check_str_len(str_len); + if ((sizeof(usize) < 8) && !no_indent && + level > (USIZE_MAX - 16 - str_len * 6) / 4) goto fail_alloc; incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); cur = write_indent(cur, no_indent ? 0 : level, spaces); if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { cur = write_str_noesc(cur, str_ptr, str_len); } else { - cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + cur = write_str(cur, esc, inv, str_ptr, str_len, + enc_table, hex_table); if (unlikely(!cur)) goto fail_str; } *cur++ = is_key ? ':' : ','; @@ -9340,9 +9624,18 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); ctn_len_tmp = unsafe_yyjson_get_len(val); ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(2 * sizeof(*ctx) + (no_indent ? 0 : level * 4)); +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_WRITER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif if (unlikely(ctn_len_tmp == 0)) { +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif /* write empty container */ - incr_len(16 + (no_indent ? 0 : level * 4)); cur = write_indent(cur, no_indent ? 0 : level, spaces); *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); @@ -9351,7 +9644,6 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, goto val_end; } else { /* push context, setup new container */ - incr_len(32 + (no_indent ? 0 : level * 4)); yyjson_write_ctx_set(--ctx, ctn_len, ctn_obj); ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; ctn_obj = ctn_obj_tmp; @@ -9400,6 +9692,9 @@ static_inline u8 *yyjson_write_pretty(const yyjson_val *root, goto val_begin; ctn_end: +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif cur -= 2; *cur++ = '\n'; incr_len(level * 4); @@ -9430,23 +9725,20 @@ fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); +#if YYJSON_WRITER_DEPTH_LIMIT +fail_depth: return_err(DEPTH, MSG_DEPTH); +#endif #undef return_err #undef incr_len #undef check_str_len } - - -/*============================================================================== - * MARK: - JSON Writer (Public) - *============================================================================*/ - -char *yyjson_val_write_opts(const yyjson_val *val, - yyjson_write_flag flg, - const yyjson_alc *alc_ptr, - usize *dat_len, - yyjson_write_err *err) { +static char *write_root(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + char *buf, usize *dat_len, + yyjson_write_err *err) { yyjson_write_err tmp_err; usize tmp_dat_len; yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; @@ -9463,23 +9755,39 @@ char *yyjson_val_write_opts(const yyjson_val *val, } if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { - return (char *)yyjson_write_single(root, flg, alc, dat_len, err); + return (char *)write_root_single(root, flg, alc, buf, dat_len, err); } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { - return (char *)yyjson_write_pretty(root, flg, alc, dat_len, err); + return (char *)write_root_pretty(root, flg, alc, buf, dat_len, err); } else { - return (char *)yyjson_write_minify(root, flg, alc, dat_len, err); + return (char *)write_root_minify(root, flg, alc, buf, dat_len, err); } } + + +/*============================================================================== + * MARK: - JSON Writer (Public) + *============================================================================*/ + +char *yyjson_val_write_opts(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + return write_root(val, flg, alc_ptr, NULL, dat_len, err); +} + char *yyjson_write_opts(const yyjson_doc *doc, yyjson_write_flag flg, const yyjson_alc *alc_ptr, usize *dat_len, yyjson_write_err *err) { yyjson_val *root = doc ? doc->root : NULL; - return yyjson_val_write_opts(root, flg, alc_ptr, dat_len, err); + return write_root(root, flg, alc_ptr, NULL, dat_len, err); } +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + bool yyjson_val_write_file(const char *path, const yyjson_val *val, yyjson_write_flag flg, @@ -9499,7 +9807,7 @@ bool yyjson_val_write_file(const char *path, return false; } - dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + dat = (u8 *)write_root(root, flg, &alc, NULL, &dat_len, err); if (unlikely(!dat)) return false; suc = write_dat_to_file(path, dat, dat_len, err); alc.free(alc.ctx, dat); @@ -9525,7 +9833,7 @@ bool yyjson_val_write_fp(FILE *fp, return false; } - dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + dat = (u8 *)write_root(root, flg, &alc, NULL, &dat_len, err); if (unlikely(!dat)) return false; suc = write_dat_to_fp(fp, dat, dat_len, err); alc.free(alc.ctx, dat); @@ -9550,6 +9858,30 @@ bool yyjson_write_fp(FILE *fp, return yyjson_val_write_fp(fp, root, flg, alc_ptr, err); } +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + +size_t yyjson_val_write_buf(char *buf, size_t buf_len, + const yyjson_val *val, + yyjson_write_flag flg, + yyjson_write_err *err) { + if (unlikely(!buf || !buf_len)) { + if (err) err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + if (err) err->msg = "input buf or buf_len is invalid"; + return 0; + } else { + write_root(val, flg, &YYJSON_NULL_ALC, buf, &buf_len, err); + return buf_len; + } +} + +size_t yyjson_write_buf(char *buf, size_t buf_len, + const yyjson_doc *doc, + yyjson_write_flag flg, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_buf(buf, buf_len, root, flg, err); +} + /*============================================================================== @@ -9593,22 +9925,22 @@ static_inline usize yyjson_mut_doc_estimated_val_num( } /** Write single JSON value. */ -static_inline u8 *yyjson_mut_write_single(yyjson_mut_val *val, - yyjson_write_flag flg, - yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { - return yyjson_write_single((yyjson_val *)val, flg, alc, dat_len, err); +static_inline u8 *mut_write_root_single(yyjson_mut_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { + return write_root_single((yyjson_val *)val, flg, alc, buf, dat_len, err); } /** Write JSON document minify. The root of this document should be a non-empty container. */ -static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, - usize estimated_val_num, - yyjson_write_flag flg, - yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { +static_inline u8 *mut_write_root_minify(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { #define return_err(_code, _msg) do { \ *dat_len = 0; \ err->code = YYJSON_WRITE_ERROR_##_code; \ @@ -9622,7 +9954,8 @@ static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ usize ctx_pos = (usize)((u8 *)ctx - hdr); \ usize cur_pos = (usize)(cur - hdr); \ - ctx_len = (usize)(end - (u8 *)ctx); \ + yyjson_assume((u8 *)ctx <= (u8 *)end); \ + ctx_len = (usize)((u8 *)end - (u8 *)ctx); \ alc_inc = yyjson_max(alc_len / 2, ext_len); \ alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ @@ -9651,17 +9984,28 @@ static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, u8 *hdr, *cur, *end, *tmp; yyjson_mut_write_ctx *ctx, *ctx_tmp; usize alc_len, alc_inc, ctx_len, ext_len, str_len; +#if YYJSON_WRITER_DEPTH_LIMIT + usize ctn_depth = 0; +#endif const u8 *str_ptr; const char_enc_type *enc_table = get_enc_table_with_flag(flg); + const u8 *hex_table = get_hex_table_with_flag(flg); bool cpy = (enc_table == enc_table_cpy); bool esc = has_flg(ESCAPE_UNICODE) != 0; bool inv = has_allow(INVALID_UNICODE) != 0; bool newline = has_flg(NEWLINE_AT_END) != 0; - alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; - alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); - hdr = (u8 *)alc.malloc(alc.ctx, alc_len); - if (!hdr) goto fail_alloc; + if (buf) { + hdr = (u8 *)buf; + alc_len = *dat_len; + alc_len = size_align_down(alc_len, sizeof(yyjson_mut_write_ctx)); + if (alc_len <= sizeof(yyjson_mut_write_ctx)) goto fail_alloc; + } else { + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + } cur = hdr; end = hdr + alc_len; ctx = (yyjson_mut_write_ctx *)(void *)end; @@ -9687,7 +10031,8 @@ static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { cur = write_str_noesc(cur, str_ptr, str_len); } else { - cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + cur = write_str(cur, esc, inv, str_ptr, str_len, + enc_table, hex_table); if (unlikely(!cur)) goto fail_str; } *cur++ = is_key ? ':' : ','; @@ -9704,8 +10049,17 @@ static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { ctn_len_tmp = unsafe_yyjson_get_len(val); ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); - incr_len(16); + incr_len(2 * sizeof(*ctx)); +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_WRITER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif if (unlikely(ctn_len_tmp == 0)) { +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif /* write empty container */ *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); @@ -9753,6 +10107,9 @@ static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, goto val_begin; ctn_end: +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif cur--; *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); *cur++ = ','; @@ -9782,6 +10139,9 @@ fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); +#if YYJSON_WRITER_DEPTH_LIMIT +fail_depth: return_err(DEPTH, MSG_DEPTH); +#endif #undef return_err #undef incr_len @@ -9790,12 +10150,12 @@ fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); /** Write JSON document pretty. The root of this document should be a non-empty container. */ -static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, - usize estimated_val_num, - yyjson_write_flag flg, - yyjson_alc alc, - usize *dat_len, - yyjson_write_err *err) { +static_inline u8 *mut_write_root_pretty(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + char *buf, usize *dat_len, + yyjson_write_err *err) { #define return_err(_code, _msg) do { \ *dat_len = 0; \ err->code = YYJSON_WRITE_ERROR_##_code; \ @@ -9809,7 +10169,8 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ usize ctx_pos = (usize)((u8 *)ctx - hdr); \ usize cur_pos = (usize)(cur - hdr); \ - ctx_len = (usize)(end - (u8 *)ctx); \ + yyjson_assume((u8 *)ctx <= (u8 *)end); \ + ctx_len = (usize)((u8 *)end - (u8 *)ctx); \ alc_inc = yyjson_max(alc_len / 2, ext_len); \ alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ @@ -9838,18 +10199,29 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, u8 *hdr, *cur, *end, *tmp; yyjson_mut_write_ctx *ctx, *ctx_tmp; usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; +#if YYJSON_WRITER_DEPTH_LIMIT + usize ctn_depth = 0; +#endif const u8 *str_ptr; const char_enc_type *enc_table = get_enc_table_with_flag(flg); + const u8 *hex_table = get_hex_table_with_flag(flg); bool cpy = (enc_table == enc_table_cpy); bool esc = has_flg(ESCAPE_UNICODE) != 0; bool inv = has_allow(INVALID_UNICODE) != 0; usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; bool newline = has_flg(NEWLINE_AT_END) != 0; - alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; - alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); - hdr = (u8 *)alc.malloc(alc.ctx, alc_len); - if (!hdr) goto fail_alloc; + if (buf) { + hdr = (u8 *)buf; + alc_len = *dat_len; + alc_len = size_align_down(alc_len, sizeof(yyjson_mut_write_ctx)); + if (alc_len <= sizeof(yyjson_mut_write_ctx)) goto fail_alloc; + } else { + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + } cur = hdr; end = hdr + alc_len; ctx = (yyjson_mut_write_ctx *)(void *)end; @@ -9874,12 +10246,15 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, str_len = unsafe_yyjson_get_len(val); str_ptr = (const u8 *)unsafe_yyjson_get_str(val); check_str_len(str_len); + if ((sizeof(usize) < 8) && !no_indent && + level > (USIZE_MAX - 16 - str_len * 6) / 4) goto fail_alloc; incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); cur = write_indent(cur, no_indent ? 0 : level, spaces); if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { cur = write_str_noesc(cur, str_ptr, str_len); } else { - cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + cur = write_str(cur, esc, inv, str_ptr, str_len, + enc_table, hex_table); if (unlikely(!cur)) goto fail_str; } *cur++ = is_key ? ':' : ','; @@ -9901,9 +10276,18 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); ctn_len_tmp = unsafe_yyjson_get_len(val); ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(2 * sizeof(*ctx) + (no_indent ? 0 : level * 4)); +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth++; + if (unlikely(ctn_depth >= (usize)YYJSON_WRITER_DEPTH_LIMIT)) { + goto fail_depth; + } +#endif if (unlikely(ctn_len_tmp == 0)) { +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif /* write empty container */ - incr_len(16 + (no_indent ? 0 : level * 4)); cur = write_indent(cur, no_indent ? 0 : level, spaces); *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); @@ -9912,7 +10296,6 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, goto val_end; } else { /* push context, setup new container */ - incr_len(32 + (no_indent ? 0 : level * 4)); yyjson_mut_write_ctx_set(--ctx, ctn, ctn_len, ctn_obj); ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; ctn_obj = ctn_obj_tmp; @@ -9963,6 +10346,9 @@ static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, goto val_begin; ctn_end: +#if YYJSON_WRITER_DEPTH_LIMIT + ctn_depth--; +#endif cur -= 2; *cur++ = '\n'; incr_len(level * 4); @@ -9995,18 +10381,21 @@ fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); +#if YYJSON_WRITER_DEPTH_LIMIT +fail_depth: return_err(DEPTH, MSG_DEPTH); +#endif #undef return_err #undef incr_len #undef check_str_len } -static char *yyjson_mut_write_opts_impl(const yyjson_mut_val *val, - usize estimated_val_num, - yyjson_write_flag flg, - const yyjson_alc *alc_ptr, - usize *dat_len, - yyjson_write_err *err) { +static char *mut_write_root(const yyjson_mut_val *val, + usize estimated_val_num, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + char *buf, usize *dat_len, + yyjson_write_err *err) { yyjson_write_err tmp_err; usize tmp_dat_len; yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; @@ -10023,13 +10412,13 @@ static char *yyjson_mut_write_opts_impl(const yyjson_mut_val *val, } if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { - return (char *)yyjson_mut_write_single(root, flg, alc, dat_len, err); + return (char *)mut_write_root_single(root, flg, alc, buf, dat_len, err); } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { - return (char *)yyjson_mut_write_pretty(root, estimated_val_num, - flg, alc, dat_len, err); + return (char *)mut_write_root_pretty(root, estimated_val_num, + flg, alc, buf, dat_len, err); } else { - return (char *)yyjson_mut_write_minify(root, estimated_val_num, - flg, alc, dat_len, err); + return (char *)mut_write_root_minify(root, estimated_val_num, + flg, alc, buf, dat_len, err); } } @@ -10044,7 +10433,7 @@ char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, const yyjson_alc *alc_ptr, usize *dat_len, yyjson_write_err *err) { - return yyjson_mut_write_opts_impl(val, 0, flg, alc_ptr, dat_len, err); + return mut_write_root(val, 0, flg, alc_ptr, NULL, dat_len, err); } char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, @@ -10061,10 +10450,34 @@ char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, root = NULL; estimated_val_num = 0; } - return yyjson_mut_write_opts_impl(root, estimated_val_num, - flg, alc_ptr, dat_len, err); + return mut_write_root(root, estimated_val_num, + flg, alc_ptr, NULL, dat_len, err); +} + +size_t yyjson_mut_val_write_buf(char *buf, size_t buf_len, + const yyjson_mut_val *val, + yyjson_write_flag flg, + yyjson_write_err *err) { + if (unlikely(!buf || !buf_len)) { + if (err) err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + if (err) err->msg = "input buf or buf_len is invalid"; + return 0; + } else { + mut_write_root(val, 0, flg, &YYJSON_NULL_ALC, buf, &buf_len, err); + return buf_len; + } +} + +size_t yyjson_mut_write_buf(char *buf, size_t buf_len, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + yyjson_write_err *err) { + yyjson_mut_val *root = doc ? doc->root : NULL; + return yyjson_mut_val_write_buf(buf, buf_len, root, flg, err); } +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + bool yyjson_mut_val_write_file(const char *path, const yyjson_mut_val *val, yyjson_write_flag flg, @@ -10135,6 +10548,8 @@ bool yyjson_mut_write_fp(FILE *fp, return yyjson_mut_val_write_fp(fp, root, flg, alc_ptr, err); } +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + #undef has_flg #undef has_allow #endif /* YYJSON_DISABLE_WRITER */ @@ -10222,7 +10637,7 @@ static_inline bool ptr_token_to_idx(const char *cur, usize len, usize *idx) { @param token a JSON pointer token @param len unescaped token length @param esc number of escaped characters in this token - @return true if `str` is equals to `token` + @return true if `str` is equal to `token` */ static_inline bool ptr_token_eq(void *key, const char *token, usize len, usize esc) { @@ -10251,7 +10666,7 @@ static_inline bool ptr_token_eq(void *key, @param esc number of escaped characters in this token @return value at index, or NULL if token is not index or index is out of range */ -static_inline yyjson_val *ptr_arr_get(yyjson_val *arr, const char *token, +static_inline yyjson_val *ptr_arr_get(const yyjson_val *arr, const char *token, usize len, usize esc) { yyjson_val *val = unsafe_yyjson_get_first(arr); usize num = unsafe_yyjson_get_len(arr), idx = 0; @@ -10274,7 +10689,7 @@ static_inline yyjson_val *ptr_arr_get(yyjson_val *arr, const char *token, @param esc [in] number of escaped characters in this token @return value associated with the token, or NULL if no value */ -static_inline yyjson_val *ptr_obj_get(yyjson_val *obj, const char *token, +static_inline yyjson_val *ptr_obj_get(const yyjson_val *obj, const char *token, usize len, usize esc) { yyjson_val *key = unsafe_yyjson_get_first(obj); usize num = unsafe_yyjson_get_len(obj); @@ -10295,7 +10710,7 @@ static_inline yyjson_val *ptr_obj_get(yyjson_val *obj, const char *token, @param last [out] whether index is last @return value at index, or NULL if token is not index or index is out of range */ -static_inline yyjson_mut_val *ptr_mut_arr_get(yyjson_mut_val *arr, +static_inline yyjson_mut_val *ptr_mut_arr_get(const yyjson_mut_val *arr, const char *token, usize len, usize esc, yyjson_mut_val **pre, @@ -10325,7 +10740,7 @@ static_inline yyjson_mut_val *ptr_mut_arr_get(yyjson_mut_val *arr, @param pre [out] previous (sibling) key of the returned value's key @return value associated with the token, or NULL if no value */ -static_inline yyjson_mut_val *ptr_mut_obj_get(yyjson_mut_val *obj, +static_inline yyjson_mut_val *ptr_mut_obj_get(const yyjson_mut_val *obj, const char *token, usize len, usize esc, yyjson_mut_val **pre) { @@ -10388,7 +10803,7 @@ static_inline yyjson_mut_val *ptr_new_key(const char *token, #define return_err_alloc(_ret) \ return_err(_ret, MEMORY_ALLOCATION, 0, "failed to create value") -yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, +yyjson_val *unsafe_yyjson_ptr_getx(const yyjson_val *val, const char *ptr, size_t ptr_len, yyjson_ptr_err *err) { @@ -10408,12 +10823,12 @@ yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, val = NULL; } if (!val) return_err_resolve(NULL, token - hdr); - if (ptr == end) return val; + if (ptr == end) return constcast(yyjson_val *)val; } } yyjson_mut_val *unsafe_yyjson_mut_ptr_getx( - yyjson_mut_val *val, const char *ptr, size_t ptr_len, + const yyjson_mut_val *val, const char *ptr, size_t ptr_len, yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { const char *hdr = ptr, *end = ptr + ptr_len, *token; @@ -10425,7 +10840,7 @@ yyjson_mut_val *unsafe_yyjson_mut_ptr_getx( while (true) { token = ptr_next_token(&ptr, end, &len, &esc); if (unlikely(!token)) return_err_syntax(NULL, ptr - hdr); - ctn = val; + ctn = constcast(yyjson_mut_val *)val; type = unsafe_yyjson_get_type(val); if (type == YYJSON_TYPE_OBJ) { val = ptr_mut_obj_get(val, token, len, esc, &pre); @@ -10442,7 +10857,7 @@ yyjson_mut_val *unsafe_yyjson_mut_ptr_getx( } } if (!val) return_err_resolve(NULL, token - hdr); - if (ptr == end) return val; + if (ptr == end) return constcast(yyjson_mut_val *)val; } } @@ -10496,7 +10911,7 @@ bool unsafe_yyjson_mut_ptr_putx( val = NULL; ctn_type = YYJSON_TYPE_OBJ; token = ptr_next_token(&ptr, end, &token_len, &esc); - if (unlikely(!token)) return_err_resolve(false, token - hdr); + if (unlikely(!token)) return_err_syntax(false, ptr - hdr); } /* container is object, create parent nodes */ @@ -10701,8 +11116,8 @@ static patch_op patch_op_get(yyjson_val *op) { root, _ptr->uni.str, _ptr##_len, _val, NULL, &err->ptr) yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, - yyjson_val *orig, - yyjson_val *patch, + const yyjson_val *orig, + const yyjson_val *patch, yyjson_patch_err *err) { yyjson_mut_val *root; @@ -10822,8 +11237,8 @@ yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, } yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, - yyjson_mut_val *orig, - yyjson_mut_val *patch, + const yyjson_mut_val *orig, + const yyjson_mut_val *patch, yyjson_patch_err *err) { yyjson_mut_val *root, *obj; yyjson_mut_arr_iter iter; @@ -10842,7 +11257,7 @@ yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, if (unlikely(!root)) return_err_copy(); /* iterate through the patch array */ - yyjson_mut_arr_iter_init(patch, &iter); + yyjson_mut_arr_iter_init(constcast(yyjson_mut_val *)patch, &iter); while ((obj = yyjson_mut_arr_iter_next(&iter))) { patch_op op_enum; yyjson_mut_val *op, *path, *from = NULL, *value; @@ -10959,8 +11374,8 @@ yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, *============================================================================*/ yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, - yyjson_val *orig, - yyjson_val *patch) { + const yyjson_val *orig, + const yyjson_val *patch) { usize idx, max; yyjson_val *key, *orig_val, *patch_val, local_orig; yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; @@ -10974,9 +11389,9 @@ yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, memset(&local_orig, 0, sizeof(local_orig)); if (!yyjson_is_obj(orig)) { + local_orig.tag = builder->tag; + local_orig.uni = builder->uni; orig = &local_orig; - orig->tag = builder->tag; - orig->uni = builder->uni; } /* If orig is contributing, copy any items not modified by the patch */ @@ -11011,8 +11426,8 @@ yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, } yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, - yyjson_mut_val *orig, - yyjson_mut_val *patch) { + const yyjson_mut_val *orig, + const yyjson_mut_val *patch) { usize idx, max; yyjson_mut_val *key, *orig_val, *patch_val, local_orig; yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; @@ -11026,9 +11441,9 @@ yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, memset(&local_orig, 0, sizeof(local_orig)); if (!yyjson_mut_is_obj(orig)) { + local_orig.tag = builder->tag; + local_orig.uni = builder->uni; orig = &local_orig; - orig->tag = builder->tag; - orig->uni = builder->uni; } /* If orig is contributing, copy any items not modified by the patch */ diff --git a/src/3rdparty/yyjson/yyjson.h b/src/3rdparty/yyjson/yyjson.h index 5eb6d46801..60561d535b 100644 --- a/src/3rdparty/yyjson/yyjson.h +++ b/src/3rdparty/yyjson/yyjson.h @@ -31,99 +31,106 @@ -/*============================================================================== - * MARK: - Header Files - *============================================================================*/ - -#include -#include -#include -#include -#include -#include - - - /*============================================================================== * MARK: - Compile-time Options *============================================================================*/ -/* - Define as 1 to disable JSON reader at compile-time. - This disables functions with "read" in their name. - Reduces binary size by about 60%. - */ +/* Define as 1 to disable JSON reader at compile-time. + This disables functions with "read" in their name. + Reduces binary size by about 60%. */ #ifndef YYJSON_DISABLE_READER +#define YYJSON_DISABLE_READER 0 #endif -/* - Define as 1 to disable JSON writer at compile-time. - This disables functions with "write" in their name. - Reduces binary size by about 30%. - */ +/* Define as 1 to disable JSON writer at compile-time. + This disables functions with "write" in their name. + Reduces binary size by about 30%. */ #ifndef YYJSON_DISABLE_WRITER +#define YYJSON_DISABLE_WRITER 0 #endif -/* - Define as 1 to disable JSON incremental reader at compile-time. - This disables functions with "incr" in their name. - */ +/* Define as 1 to disable JSON incremental reader at compile-time. + This disables functions with "incr" in their name. */ #ifndef YYJSON_DISABLE_INCR_READER +#define YYJSON_DISABLE_INCR_READER 0 #endif -/* - Define as 1 to disable JSON Pointer, JSON Patch and JSON Merge Patch supports. - This disables functions with "ptr" or "patch" in their name. - */ +/* Define as 1 to disable file/fp read and write APIs. */ +#ifndef YYJSON_DISABLE_FILE +#define YYJSON_DISABLE_FILE 0 +#endif + +/* Define as 1 to disable JSON Pointer, JSON Patch and JSON Merge Patch. + This disables functions with "ptr" or "patch" in their name. */ #ifndef YYJSON_DISABLE_UTILS +#define YYJSON_DISABLE_UTILS 0 #endif -/* - Define as 1 to disable the fast floating-point number conversion in yyjson. - Libc's `strtod/snprintf` will be used instead. +/* Define as 1 to disable the fast floating-point number conversion in yyjson. + Libc's `strtod/snprintf` will be used instead. - This reduces binary size by about 30%, but significantly slows down the - floating-point read/write speed. - */ + This reduces binary size by about 30%, but significantly slows down the + floating-point read/write speed. */ #ifndef YYJSON_DISABLE_FAST_FP_CONV +#define YYJSON_DISABLE_FAST_FP_CONV 0 #endif -/* - Define as 1 to disable non-standard JSON features support at compile-time, - such as YYJSON_READ_ALLOW_XXX and YYJSON_WRITE_ALLOW_XXX. +/* Define as 1 to disable non-standard JSON features support at compile-time, + such as YYJSON_READ_ALLOW_XXX and YYJSON_WRITE_ALLOW_XXX. - This reduces binary size by about 10%, and slightly improves performance. - */ + This reduces binary size by about 10%, and slightly improves performance. */ #ifndef YYJSON_DISABLE_NON_STANDARD +#define YYJSON_DISABLE_NON_STANDARD 0 #endif -/* - Define as 1 to disable UTF-8 validation at compile-time. +/* Define as 1 to disable UTF-8 validation at compile-time. - Use this if all input strings are guaranteed to be valid UTF-8 - (e.g. language-level String types are already validated). + Use this if all input strings are guaranteed to be valid UTF-8 + (e.g. language-level String types are already validated). - Disabling UTF-8 validation improves performance for non-ASCII strings by about - 3% to 7%. + Disabling UTF-8 validation improves performance for non-ASCII strings by + about 3% to 7%. - Note: If this flag is enabled while passing illegal UTF-8 strings, - the following errors may occur: - - Escaped characters may be ignored when parsing JSON strings. - - Ending quotes may be ignored when parsing JSON strings, causing the - string to merge with the next value. - - When serializing with `yyjson_mut_val`, the string's end may be accessed - out of bounds, potentially causing a segmentation fault. - */ + Note: If this flag is enabled while passing illegal UTF-8 strings, + the following errors may occur: + - Escaped characters may be ignored when parsing JSON strings. + - Ending quotes may be ignored when parsing JSON strings, causing the + string to merge with the next value. + - When serializing with `yyjson_mut_val`, the string's end may be accessed + out of bounds, potentially causing a segmentation fault. */ #ifndef YYJSON_DISABLE_UTF8_VALIDATION +#define YYJSON_DISABLE_UTF8_VALIDATION 0 #endif -/* - Define as 1 to improve performance on architectures that do not support - unaligned memory access. +/* Define as 1 to improve performance on architectures that do not support + unaligned memory access. - Normally, this does not need to be set manually. See the C file for details. - */ + Normally, this does not need to be set manually. */ #ifndef YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS +/* auto detected in yyjson.c */ +#endif + +/* Define to an integer to set a depth limit for reading nested arrays/objects. + 0 disables the policy limit. */ +#ifndef YYJSON_READER_DEPTH_LIMIT +#define YYJSON_READER_DEPTH_LIMIT 0 +#endif + +/* Define to an integer to set a depth limit for writing nested arrays/objects. + 0 disables the policy limit. */ +#ifndef YYJSON_WRITER_DEPTH_LIMIT +#define YYJSON_WRITER_DEPTH_LIMIT 0 +#endif + +/* Define as 1 to build without libc (stdlib, string, math, stdio). + Inline fallbacks for memcpy/memmove/memset/memcmp/strlen are provided. + Optional `YYJSON_FREESTANDING_HEADER` for custom replacements. + + `malloc`/`free` are unavailable; pass `yyjson_alc` per call or define + `YYJSON_CUSTOM_ALC`. Also disables file/fp APIs. Cannot be used with + `YYJSON_DISABLE_FAST_FP_CONV`. */ +#ifndef YYJSON_FREESTANDING +#define YYJSON_FREESTANDING 0 #endif /* Define as 1 to export symbols when building this library as a Windows DLL. */ @@ -293,6 +300,16 @@ # endif #endif +/** assume for compiler */ +#undef yyjson_assume +#if yyjson_has_builtin(__builtin_unreachable) || yyjson_gcc_available(4, 5, 0) +# define yyjson_assume(expr) ((expr) ? (void)0 : __builtin_unreachable()) +#elif YYJSON_MSC_VER >= 1300 +# define yyjson_assume(expr) __assume(expr) +#else +# define yyjson_assume(expr) ((void)0) +#endif + /** compile-time constant check for compiler */ #ifndef yyjson_constant_p # if yyjson_has_builtin(__builtin_constant_p) || (YYJSON_GCC_VER >= 3) @@ -340,6 +357,82 @@ # define yyjson_api_inline static yyjson_inline #endif +/** Used to cast away (remove) const qualifier. */ +#ifndef yyjson_constcast +# define yyjson_constcast(type) (type)(void *)(size_t)(const void *) +#endif + +/** Microsoft Visual C++ 6.0 doesn't support converting number from u64 to f64: + error C2520: conversion from unsigned __int64 to double not implemented. */ +#ifndef YYJSON_U64_TO_F64_NO_IMPL +# if (0 < YYJSON_MSC_VER) && (YYJSON_MSC_VER <= 1200) +# define YYJSON_U64_TO_F64_NO_IMPL 1 +# else +# define YYJSON_U64_TO_F64_NO_IMPL 0 +# endif +#endif + + + +/*============================================================================== + * MARK: - Header Files + *============================================================================*/ + +#include /* for size_t, NULL */ +#include /* for CHAR_BIT, *_MAX */ +#include /* for floating-point limit macros */ + +/** freestanding */ +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE +#include /* for FILE, fopen, fread, fwrite, sprintf */ +#endif +#if !YYJSON_FREESTANDING +#include /* for malloc, realloc, free, strtod */ +#include /* for memcpy, memmove, memset, memcmp, strlen */ +#include /* for HUGE_VAL, INFINITY, NAN (no libm required) */ +#elif defined(YYJSON_FREESTANDING_HEADER) +# include YYJSON_FREESTANDING_HEADER /* custom replacement for string.h */ +#else +# ifndef memcpy +# define memcpy(d,s,n) yyjson_memcpy(d,s,n) +# endif +# ifndef memmove +# define memmove(d,s,n) yyjson_memmove(d,s,n) +# endif +# ifndef memset +# define memset(d,v,n) yyjson_memset(d,v,n) +# endif +# ifndef memcmp +# define memcmp(a,b,n) yyjson_memcmp(a,b,n) +# endif +# ifndef strlen +# define strlen(s) yyjson_strlen(s) +# endif +yyjson_api_inline void *yyjson_memcpy(void *d, const void *s, size_t n) { + char *p = (char *)d; const char *q = (const char *)s; + while (n--) *p++ = *q++; return d; +} +yyjson_api_inline void *yyjson_memmove(void *d, const void *s, size_t n) { + char *p = (char *)d; const char *q = (const char *)s; + if (p == q || !n) return d; + if (p < q) { while (n--) *p++ = *q++; } + else { p += n; q += n; while (n--) *--p = *--q; } + return d; +} +yyjson_api_inline void *yyjson_memset(void *d, int v, size_t n) { + char *p = (char *)d, x = (char)v; + while (n--) *p++ = x; return d; +} +yyjson_api_inline int yyjson_memcmp(const void *a, const void *b, size_t n) { + const unsigned char *p = (const unsigned char *)a; + const unsigned char *q = (const unsigned char *)b; + while (n--) { if (*p != *q) return (int)(*p - *q); p++; q++; } return 0; +} +yyjson_api_inline size_t yyjson_strlen(const char *s) { + const char *p = s; while (*p) p++; return (size_t)(p - s); +} +#endif + /** stdint (C89 compatible) */ #if (defined(YYJSON_HAS_STDINT_H) && YYJSON_HAS_STDINT_H) || \ YYJSON_MSC_VER >= 1600 || YYJSON_STDC_VER >= 199901L || \ @@ -418,8 +511,8 @@ /** stdbool (C89 compatible) */ #if (defined(YYJSON_HAS_STDBOOL_H) && YYJSON_HAS_STDBOOL_H) || \ - (yyjson_has_include() && !defined(__STRICT_ANSI__)) || \ - YYJSON_MSC_VER >= 1800 || YYJSON_STDC_VER >= 199901L + YYJSON_MSC_VER >= 1800 || YYJSON_STDC_VER >= 199901L || \ + (yyjson_has_include() && !defined(__STRICT_ANSI__)) # include #elif !defined(__bool_true_false_are_defined) # define __bool_true_false_are_defined 1 @@ -446,18 +539,6 @@ # endif #endif -/** - Microsoft Visual C++ 6.0 doesn't support converting number from u64 to f64: - error C2520: conversion from unsigned __int64 to double not implemented. - */ -#ifndef YYJSON_U64_TO_F64_NO_IMPL -# if (0 < YYJSON_MSC_VER) && (YYJSON_MSC_VER <= 1200) -# define YYJSON_U64_TO_F64_NO_IMPL 1 -# else -# define YYJSON_U64_TO_F64_NO_IMPL 0 -# endif -#endif - /*============================================================================== @@ -474,12 +555,14 @@ extern "C" { # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-function" # pragma clang diagnostic ignored "-Wunused-parameter" -#elif defined(__GNUC__) -# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +#elif YYJSON_IS_REAL_GCC +# if yyjson_gcc_available(4, 6, 0) # pragma GCC diagnostic push # endif -# pragma GCC diagnostic ignored "-Wunused-function" -# pragma GCC diagnostic ignored "-Wunused-parameter" +# if yyjson_gcc_available(4, 2, 0) +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunused-parameter" +# endif #elif defined(_MSC_VER) # pragma warning(push) # pragma warning(disable:4800) /* 'int': forcing value to 'true' or 'false' */ @@ -495,16 +578,16 @@ extern "C" { #define YYJSON_VERSION_MAJOR 0 /** The minor version of yyjson. */ -#define YYJSON_VERSION_MINOR 12 +#define YYJSON_VERSION_MINOR 13 /** The patch version of yyjson. */ #define YYJSON_VERSION_PATCH 0 /** The version of yyjson in hex: `(major << 16) | (minor << 8) | (patch)`. */ -#define YYJSON_VERSION_HEX 0x000C00 +#define YYJSON_VERSION_HEX 0x000D00 /** The version string of yyjson. */ -#define YYJSON_VERSION_STRING "0.12.0" +#define YYJSON_VERSION_STRING "0.13.0" /** The version of yyjson in hex, same as `YYJSON_VERSION_HEX`. */ yyjson_api uint32_t yyjson_version(void); @@ -548,7 +631,7 @@ typedef uint8_t yyjson_subtype; #define YYJSON_SUBTYPE_SINT ((uint8_t)(1 << 3)) /* ___01___ */ /** Real number subtype: `double`. */ #define YYJSON_SUBTYPE_REAL ((uint8_t)(2 << 3)) /* ___10___ */ -/** String that do not need to be escaped for writing (internal use). */ +/** String that does not need to be escaped for writing (internal use). */ #define YYJSON_SUBTYPE_NOESC ((uint8_t)(1 << 3)) /* ___01___ */ /** The mask used to extract the type of a JSON value. */ @@ -604,19 +687,19 @@ typedef struct yyjson_alc { calculated. This is not a general-purpose allocator. It is designed to handle a single JSON - data at a time. If it is used for overly complex memory tasks, such as parsing - multiple JSON documents using the same allocator but releasing only a few of - them, it may cause memory fragmentation, resulting in performance degradation - and memory waste. + document at a time. If it is used for overly complex memory tasks, such as + parsing multiple JSON documents using the same allocator but releasing only a + few of them, it may cause memory fragmentation, resulting in performance + degradation and memory waste. @param alc The allocator to be initialized. - If this parameter is NULL, the function will fail and return false. + If `alc` is NULL, returns false. If `buf` or `size` is invalid, this will be set to an empty allocator. @param buf The buffer memory for this allocator. - If this parameter is NULL, the function will fail and return false. + If `buf` is NULL, returns false. @param size The size of `buf`, in bytes. - If this parameter is less than 8 words (32/64 bytes on 32/64-bit OS), the - function will fail and return false. + If `size` is less than 8 words (32/64 bytes on 32/64-bit OS), + returns false. @return true if the `alc` has been successfully initialized. @b Example @@ -626,7 +709,7 @@ typedef struct yyjson_alc { yyjson_alc alc; yyjson_alc_pool_init(&alc, buf, 1024); - const char *json = "{\"name\":\"Helvetica\",\"size\":16}" + const char *json = "{\"name\":\"Helvetica\",\"size\":16}"; yyjson_doc *doc = yyjson_read_opts(json, strlen(json), 0, &alc, NULL); // the memory of `doc` is on the stack @endcode @@ -695,7 +778,7 @@ typedef struct yyjson_doc yyjson_doc; /** An immutable value for reading JSON. A JSON Value has the same lifetime as its document. The memory is held by its - document and and cannot be freed alone. + document and cannot be freed alone. */ typedef struct yyjson_val yyjson_val; @@ -709,7 +792,7 @@ typedef struct yyjson_mut_doc yyjson_mut_doc; /** A mutable value for building JSON. A JSON Value has the same lifetime as its document. The memory is held by its - document and and cannot be freed alone. + document and cannot be freed alone. */ typedef struct yyjson_mut_val yyjson_mut_val; @@ -735,7 +818,7 @@ static const yyjson_read_flag YYJSON_READ_NOFLAG = 0; /** Read the input data in-situ. This option allows the reader to modify and use input data to store string values, which can increase reading speed slightly. - The caller should hold the input data before free the document. + The caller should hold the input data before freeing the document. The input data must be padded by at least `YYJSON_PADDING_SIZE` bytes. For example: `[1,2]` should be `[1,2]\0\0\0\0`, input length should be 5. */ static const yyjson_read_flag YYJSON_READ_INSITU = 1 << 0; @@ -749,7 +832,7 @@ static const yyjson_read_flag YYJSON_READ_STOP_WHEN_DONE = 1 << 1; such as `[1,2,3,]`, `{"a":1,"b":2,}` (non-standard). */ static const yyjson_read_flag YYJSON_READ_ALLOW_TRAILING_COMMAS = 1 << 2; -/** Allow C-style single-line and mult-line comments (non-standard). */ +/** Allow C-style single-line and multi-line comments (non-standard). */ static const yyjson_read_flag YYJSON_READ_ALLOW_COMMENTS = 1 << 3; /** Allow inf/nan number and literal, case-insensitive, @@ -875,6 +958,9 @@ static const yyjson_read_code YYJSON_READ_ERROR_FILE_READ = 13; /** Incomplete input during incremental parsing; parsing state is preserved. */ static const yyjson_read_code YYJSON_READ_ERROR_MORE = 14; +/** Read depth limit exceeded. */ +static const yyjson_read_code YYJSON_READ_ERROR_DEPTH = 15; + /** Error information for JSON reader. */ typedef struct yyjson_read_err { /** Error code, see `yyjson_read_code` for all possible values. */ @@ -897,12 +983,12 @@ typedef struct yyjson_read_err { 2. The `alc` is thread-safe or NULL. @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. - If this parameter is NULL, the function will fail and return NULL. + If `dat` is NULL, returns NULL. The `dat` will not be modified without the flag `YYJSON_READ_INSITU`, so you - can pass a `const char *` string and case it to `char *` if you don't use + can pass a `const char *` string and cast it to `char *` if you don't use the `YYJSON_READ_INSITU` flag. @param len The length of JSON data in bytes. - If this parameter is 0, the function will fail and return NULL. + If `len` is 0, returns NULL. @param flg The JSON read options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON reader. @@ -918,6 +1004,8 @@ yyjson_api yyjson_doc *yyjson_read_opts(char *dat, const yyjson_alc *alc, yyjson_read_err *err); +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Read a JSON file. @@ -927,7 +1015,7 @@ yyjson_api yyjson_doc *yyjson_read_opts(char *dat, @param path The JSON file's path. This should be a null-terminated string using the system's native encoding. - If this path is NULL or invalid, the function will fail and return NULL. + If `path` is NULL or invalid, returns NULL. @param flg The JSON read options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON reader. @@ -949,7 +1037,7 @@ yyjson_api yyjson_doc *yyjson_read_file(const char *path, @param fp The file pointer. The data will be read from the current position of the FILE to the end. - If this fp is NULL or invalid, the function will fail and return NULL. + If `fp` is NULL or invalid, returns NULL. @param flg The JSON read options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON reader. @@ -966,15 +1054,17 @@ yyjson_api yyjson_doc *yyjson_read_fp(FILE *fp, const yyjson_alc *alc, yyjson_read_err *err); +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + /** Read a JSON string. This function is thread-safe. @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. - If this parameter is NULL, the function will fail and return NULL. + If `dat` is NULL, returns NULL. @param len The length of JSON data in bytes. - If this parameter is 0, the function will fail and return NULL. + If `len` is 0, returns NULL. @param flg The JSON read options. Multiple options can be combined with `|` operator. 0 means no options. @return A new JSON document, or NULL if an error occurs. @@ -1007,9 +1097,9 @@ typedef struct yyjson_incr_state yyjson_incr_state; Flags for non-standard features (e.g. comments, trailing commas) are ignored. @param buf The JSON data, null-terminator is not required. - If this parameter is NULL, the function will fail and return NULL. + If `buf` is NULL, returns NULL. @param buf_len The length of the JSON data in `buf`. - If use `YYJSON_READ_INSITU`, `buf_len` should not include the padding size. + If using `YYJSON_READ_INSITU`, buf_len should not include the padding size. @param flg The JSON read options. Multiple options can be combined with `|` operator. @param alc The memory allocator used by JSON reader. @@ -1036,7 +1126,7 @@ yyjson_api yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, @param state The state for incremental reading, created using `yyjson_incr_new()`. @param len The number of bytes of JSON data available to parse. - If this parameter is 0, the function will fail and return NULL. + If `len` is 0, returns NULL. @param err A pointer to receive error information. @return A new JSON document, or NULL if an error occurs. When the document is no longer needed, it should be freed with @@ -1051,7 +1141,7 @@ yyjson_api void yyjson_incr_free(yyjson_incr_state *state); #endif /* YYJSON_DISABLE_INCR_READER */ /** - Returns the size of maximum memory usage to read a JSON data. + Returns the maximum memory usage to read a JSON document. You may use this value to avoid malloc() or calloc() call inside the reader to get better performance, or read multiple JSON with one piece of memory. @@ -1075,7 +1165,7 @@ yyjson_api void yyjson_incr_free(yyjson_incr_state *state); yyjson_alc alc; yyjson_alc_pool_init(&alc, buf, size); - // no more alloc() or realloc() call during reading + // no more malloc() or realloc() call during reading doc = yyjson_read_opts(dat1, len1, 0, &alc, NULL); yyjson_doc_free(doc); doc = yyjson_read_opts(dat2, len2, 0, &alc, NULL); @@ -1094,7 +1184,7 @@ yyjson_api_inline size_t yyjson_read_max_memory_usage(size_t len, for example: "[1,2,3,4]" size is 9, value count is 5. 2. Some broken JSON may cost more memory during reading, but fail at end, for example: "[[[[[[[[". - 3. yyjson use 16 bytes per value, see struct yyjson_val. + 3. yyjson uses 16 bytes per value, see struct yyjson_val. 4. yyjson use dynamic memory with a growth factor of 1.5. The max memory size is (json_size / 2 * 16 * 1.5 + padding). @@ -1113,9 +1203,9 @@ yyjson_api_inline size_t yyjson_read_max_memory_usage(size_t len, This function is thread-safe when data is not modified by other threads. @param dat The JSON data (UTF-8 without BOM), null-terminator is required. - If this parameter is NULL, the function will fail and return NULL. + If `dat` is NULL, returns NULL. @param val The output value where result is stored. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns NULL. The value will hold either UINT or SINT or REAL number; @param flg The JSON read options. Multiple options can be combined with `|` operator. 0 means no options. @@ -1192,6 +1282,10 @@ static const yyjson_write_flag YYJSON_WRITE_PRETTY_TWO_SPACES = 1 << 6; This can be helpful for text editors or NDJSON. */ static const yyjson_write_flag YYJSON_WRITE_NEWLINE_AT_END = 1 << 7; +/** Use lowercase hex digits in `\uXXXX` escape sequences instead of the default + uppercase. Only effective when `YYJSON_WRITE_ESCAPE_UNICODE` is also set. */ +static const yyjson_write_flag YYJSON_WRITE_LOWERCASE_HEX = 1 << 8; + /** The highest 8 bits of `yyjson_write_flag` and real number value's `tag` @@ -1225,7 +1319,7 @@ static const yyjson_write_code YYJSON_WRITE_SUCCESS = 0; /** Invalid parameter, such as NULL document. */ static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_PARAMETER = 1; -/** Memory allocation failure occurs. */ +/** Memory allocation failed. */ static const yyjson_write_code YYJSON_WRITE_ERROR_MEMORY_ALLOCATION = 2; /** Invalid value type in JSON document. */ @@ -1243,6 +1337,9 @@ static const yyjson_write_code YYJSON_WRITE_ERROR_FILE_WRITE = 6; /** Invalid unicode in string. */ static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_STRING = 7; +/** Nesting depth limit exceeded. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_DEPTH = 8; + /** Error information for JSON writer. */ typedef struct yyjson_write_err { /** Error code, see `yyjson_write_code` for all possible values. */ @@ -1266,7 +1363,7 @@ typedef struct yyjson_write_err { The `alc` is thread-safe or NULL. @param doc The JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1285,6 +1382,8 @@ yyjson_api char *yyjson_write_opts(const yyjson_doc *doc, size_t *len, yyjson_write_err *err); +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Write a document to JSON file with options. @@ -1294,10 +1393,10 @@ yyjson_api char *yyjson_write_opts(const yyjson_doc *doc, @param path The JSON file's path. This should be a null-terminated string using the system's native encoding. - If this path is NULL or invalid, the function will fail and return false. - If this file is not empty, the content will be discarded. + If `path` is NULL or invalid, returns false. + If the file is not empty, its content is discarded. @param doc The JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1319,9 +1418,9 @@ yyjson_api bool yyjson_write_file(const char *path, @param fp The file pointer. The data will be written to the current position of the file. - If this fp is NULL or invalid, the function will fail and return false. + If `fp` is NULL or invalid, returns false. @param doc The JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1338,13 +1437,39 @@ yyjson_api bool yyjson_write_fp(FILE *fp, const yyjson_alc *alc, yyjson_write_err *err); +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + +/** + Write a document into a buffer. + + This function does not allocate memory, but the buffer must be larger than the + final JSON size to allow temporary space. See `API.md` for details. + + @param buf The output buffer. + If `buf` is NULL, returns 0. + @param buf_len The buffer length. + If `buf_len` is too small, returns 0. + @param doc The JSON document. + If `doc` is NULL or has no root, returns 0. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return The number of bytes written (excluding the null terminator), + or 0 on failure. + */ +yyjson_api size_t yyjson_write_buf(char *buf, size_t buf_len, + const yyjson_doc *doc, + yyjson_write_flag flg, + yyjson_write_err *err); + /** Write a document to JSON string. This function is thread-safe. @param doc The JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param len A pointer to receive output length in bytes (not including the @@ -1369,7 +1494,7 @@ yyjson_api_inline char *yyjson_write(const yyjson_doc *doc, 2. The `alc` is thread-safe or NULL. @param doc The mutable JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1388,6 +1513,8 @@ yyjson_api char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, size_t *len, yyjson_write_err *err); +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Write a document to JSON file with options. @@ -1398,10 +1525,10 @@ yyjson_api char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, @param path The JSON file's path. This should be a null-terminated string using the system's native encoding. - If this path is NULL or invalid, the function will fail and return false. - If this file is not empty, the content will be discarded. + If `path` is NULL or invalid, returns false. + If the file is not empty, its content is discarded. @param doc The mutable JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1423,9 +1550,9 @@ yyjson_api bool yyjson_mut_write_file(const char *path, @param fp The file pointer. The data will be written to the current position of the file. - If this fp is NULL or invalid, the function will fail and return false. + If `fp` is NULL or invalid, returns false. @param doc The mutable JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1442,6 +1569,32 @@ yyjson_api bool yyjson_mut_write_fp(FILE *fp, const yyjson_alc *alc, yyjson_write_err *err); +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + +/** + Write a document into a buffer. + + This function does not allocate memory, but the buffer must be larger than the + final JSON size to allow temporary space. See `API.md` for details. + + @param buf The output buffer. + If `buf` is NULL, returns 0. + @param buf_len The buffer length. + If `buf_len` is too small, returns 0. + @param doc The JSON document. + If `doc` is NULL or has no root, returns 0. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return The number of bytes written (excluding the null terminator), + or 0 on failure. + */ +yyjson_api size_t yyjson_mut_write_buf(char *buf, size_t buf_len, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + yyjson_write_err *err); + /** Write a document to JSON string. @@ -1449,7 +1602,7 @@ yyjson_api bool yyjson_mut_write_fp(FILE *fp, The `doc` is not modified by other threads. @param doc The JSON document. - If this doc is NULL or has no root, the function will fail and return false. + If `doc` is NULL or has no root, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param len A pointer to receive output length in bytes (not including the @@ -1477,7 +1630,7 @@ yyjson_api_inline char *yyjson_mut_write(const yyjson_mut_doc *doc, The `alc` is thread-safe or NULL. @param val The JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1496,6 +1649,8 @@ yyjson_api char *yyjson_val_write_opts(const yyjson_val *val, size_t *len, yyjson_write_err *err); +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Write a value to JSON file with options. @@ -1505,10 +1660,10 @@ yyjson_api char *yyjson_val_write_opts(const yyjson_val *val, @param path The JSON file's path. This should be a null-terminated string using the system's native encoding. - If this path is NULL or invalid, the function will fail and return false. - If this file is not empty, the content will be discarded. + If `path` is NULL or invalid, returns false. + If the file is not empty, its content is discarded. @param val The JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1530,9 +1685,9 @@ yyjson_api bool yyjson_val_write_file(const char *path, @param fp The file pointer. The data will be written to the current position of the file. - If this path is NULL or invalid, the function will fail and return false. + If `fp` is NULL or invalid, returns false. @param val The JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1549,13 +1704,39 @@ yyjson_api bool yyjson_val_write_fp(FILE *fp, const yyjson_alc *alc, yyjson_write_err *err); +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + +/** + Write a value into a buffer. + + This function does not allocate memory, but the buffer must be larger than the + final JSON size to allow temporary space. See `API.md` for details. + + @param buf The output buffer. + If `buf` is NULL, returns 0. + @param buf_len The buffer length. + If `buf_len` is too small, returns 0. + @param val The JSON root value. + If `val` is NULL, returns 0. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return The number of bytes written (excluding the null terminator), + or 0 on failure. + */ +yyjson_api size_t yyjson_val_write_buf(char *buf, size_t buf_len, + const yyjson_val *val, + yyjson_write_flag flg, + yyjson_write_err *err); + /** Write a value to JSON string. This function is thread-safe. @param val The JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param len A pointer to receive output length in bytes (not including the @@ -1578,7 +1759,7 @@ yyjson_api_inline char *yyjson_val_write(const yyjson_val *val, 2. The `alc` is thread-safe or NULL. @param val The mutable JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1587,7 +1768,7 @@ yyjson_api_inline char *yyjson_val_write(const yyjson_val *val, null-terminator). Pass NULL if you don't need length information. @param err A pointer to receive error information. Pass NULL if you don't need error information. - @return A new JSON string, or NULL if an error occurs. + @return A new JSON string, or NULL if an error occurs. This string is encoded as UTF-8 with a null-terminator. When it's no longer needed, it should be freed with free() or alc->free(). */ @@ -1597,6 +1778,8 @@ yyjson_api char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, size_t *len, yyjson_write_err *err); +#if !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE + /** Write a value to JSON file with options. @@ -1607,10 +1790,10 @@ yyjson_api char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, @param path The JSON file's path. This should be a null-terminated string using the system's native encoding. - If this path is NULL or invalid, the function will fail and return false. - If this file is not empty, the content will be discarded. + If `path` is NULL or invalid, returns false. + If the file is not empty, its content is discarded. @param val The mutable JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1628,13 +1811,13 @@ yyjson_api bool yyjson_mut_val_write_file(const char *path, yyjson_write_err *err); /** - Write a value to JSON file with options. + Write a value to file pointer with options. @param fp The file pointer. The data will be written to the current position of the file. - If this path is NULL or invalid, the function will fail and return false. + If `fp` is NULL or invalid, returns false. @param val The mutable JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns false. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param alc The memory allocator used by JSON writer. @@ -1651,6 +1834,32 @@ yyjson_api bool yyjson_mut_val_write_fp(FILE *fp, const yyjson_alc *alc, yyjson_write_err *err); +#endif /* !YYJSON_FREESTANDING && !YYJSON_DISABLE_FILE */ + +/** + Write a value into a buffer. + + This function does not allocate memory, but the buffer must be larger than the + final JSON size to allow temporary space. See `API.md` for details. + + @param buf The output buffer. + If `buf` is NULL, returns 0. + @param buf_len The buffer length. + If `buf_len` is too small, returns 0. + @param val The JSON root value. + If `val` is NULL, returns 0. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return The number of bytes written (excluding the null terminator), + or 0 on failure. + */ +yyjson_api size_t yyjson_mut_val_write_buf(char *buf, size_t buf_len, + const yyjson_mut_val *val, + yyjson_write_flag flg, + yyjson_write_err *err); + /** Write a value to JSON string. @@ -1658,7 +1867,7 @@ yyjson_api bool yyjson_mut_val_write_fp(FILE *fp, The `val` is not modified by other threads. @param val The JSON root value. - If this parameter is NULL, the function will fail and return NULL. + If `val` is NULL, returns NULL. @param flg The JSON write options. Multiple options can be combined with `|` operator. 0 means no options. @param len A pointer to receive output length in bytes (not including the @@ -1677,9 +1886,9 @@ yyjson_api_inline char *yyjson_mut_val_write(const yyjson_mut_val *val, Write a JSON number. @param val A JSON number value to be converted to a string. - If this parameter is invalid, the function will fail and return NULL. + If `val` is invalid, returns NULL. @param buf A buffer to store the resulting null-terminated string. - If this parameter is NULL, the function will fail and return NULL. + If `buf` is NULL, returns NULL. For integer values, the buffer must be at least 21 bytes. For floating-point values, the buffer must be at least 40 bytes. @return On success, returns a pointer to the character after the last @@ -1711,17 +1920,17 @@ yyjson_api_inline char *yyjson_mut_write_number(const yyjson_mut_val *val, /** Returns the root value of this JSON document. Returns NULL if `doc` is NULL. */ -yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc); +yyjson_api_inline yyjson_val *yyjson_doc_get_root(const yyjson_doc *doc); /** Returns read size of input JSON data. Returns 0 if `doc` is NULL. For example: the read size of `[1,2,3]` is 7 bytes. */ -yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc); +yyjson_api_inline size_t yyjson_doc_get_read_size(const yyjson_doc *doc); /** Returns total value count in this JSON document. Returns 0 if `doc` is NULL. For example: the value count of `[1,2,3]` is 4. */ -yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc); +yyjson_api_inline size_t yyjson_doc_get_val_count(const yyjson_doc *doc); /** Release the JSON document and free the memory. After calling this function, the `doc` and all values from the `doc` are no @@ -1736,59 +1945,59 @@ yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc); /** Returns whether the JSON value is raw. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_raw(yyjson_val *val); +yyjson_api_inline bool yyjson_is_raw(const yyjson_val *val); /** Returns whether the JSON value is `null`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_null(yyjson_val *val); +yyjson_api_inline bool yyjson_is_null(const yyjson_val *val); /** Returns whether the JSON value is `true`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_true(yyjson_val *val); +yyjson_api_inline bool yyjson_is_true(const yyjson_val *val); /** Returns whether the JSON value is `false`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_false(yyjson_val *val); +yyjson_api_inline bool yyjson_is_false(const yyjson_val *val); /** Returns whether the JSON value is bool (true/false). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_bool(yyjson_val *val); +yyjson_api_inline bool yyjson_is_bool(const yyjson_val *val); /** Returns whether the JSON value is unsigned integer (uint64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_uint(yyjson_val *val); +yyjson_api_inline bool yyjson_is_uint(const yyjson_val *val); /** Returns whether the JSON value is signed integer (int64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_sint(yyjson_val *val); +yyjson_api_inline bool yyjson_is_sint(const yyjson_val *val); /** Returns whether the JSON value is integer (uint64_t/int64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_int(yyjson_val *val); +yyjson_api_inline bool yyjson_is_int(const yyjson_val *val); /** Returns whether the JSON value is real number (double). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_real(yyjson_val *val); +yyjson_api_inline bool yyjson_is_real(const yyjson_val *val); /** Returns whether the JSON value is number (uint64_t/int64_t/double). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_num(yyjson_val *val); +yyjson_api_inline bool yyjson_is_num(const yyjson_val *val); /** Returns whether the JSON value is string. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_str(yyjson_val *val); +yyjson_api_inline bool yyjson_is_str(const yyjson_val *val); /** Returns whether the JSON value is array. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_arr(yyjson_val *val); +yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val); /** Returns whether the JSON value is object. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_obj(yyjson_val *val); +yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val); /** Returns whether the JSON value is container (array/object). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val); +yyjson_api_inline bool yyjson_is_ctn(const yyjson_val *val); @@ -1798,139 +2007,142 @@ yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val); /** Returns the JSON value's type. Returns YYJSON_TYPE_NONE if `val` is NULL. */ -yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val); +yyjson_api_inline yyjson_type yyjson_get_type(const yyjson_val *val); /** Returns the JSON value's subtype. Returns YYJSON_SUBTYPE_NONE if `val` is NULL. */ -yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val); +yyjson_api_inline yyjson_subtype yyjson_get_subtype(const yyjson_val *val); /** Returns the JSON value's tag. Returns 0 if `val` is NULL. */ -yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val); +yyjson_api_inline uint8_t yyjson_get_tag(const yyjson_val *val); /** Returns the JSON value's type description. The return value should be one of these strings: "raw", "null", "string", "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ -yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val); +yyjson_api_inline const char *yyjson_get_type_desc(const yyjson_val *val); /** Returns the content if the value is raw. Returns NULL if `val` is NULL or type is not raw. */ -yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val); +yyjson_api_inline const char *yyjson_get_raw(const yyjson_val *val); /** Returns the content if the value is bool. Returns false if `val` is NULL or type is not bool. */ -yyjson_api_inline bool yyjson_get_bool(yyjson_val *val); +yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val); -/** Returns the content and cast to uint64_t. +/** Returns the content cast to uint64_t. Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val); +yyjson_api_inline uint64_t yyjson_get_uint(const yyjson_val *val); -/** Returns the content and cast to int64_t. +/** Returns the content cast to int64_t. Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val); +yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val); -/** Returns the content and cast to int. +/** Returns the content cast to int (may overflow). Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline int yyjson_get_int(yyjson_val *val); +yyjson_api_inline int yyjson_get_int(const yyjson_val *val); /** Returns the content if the value is real number, or 0.0 on error. Returns 0.0 if `val` is NULL or type is not real(double). */ -yyjson_api_inline double yyjson_get_real(yyjson_val *val); +yyjson_api_inline double yyjson_get_real(const yyjson_val *val); -/** Returns the content and typecast to `double` if the value is number. +/** Returns the content cast to `double` if the value is a number. Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ -yyjson_api_inline double yyjson_get_num(yyjson_val *val); +yyjson_api_inline double yyjson_get_num(const yyjson_val *val); /** Returns the content if the value is string. Returns NULL if `val` is NULL or type is not string. */ -yyjson_api_inline const char *yyjson_get_str(yyjson_val *val); +yyjson_api_inline const char *yyjson_get_str(const yyjson_val *val); -/** Returns the content length (string length, array size, object size. - Returns 0 if `val` is NULL or type is not string/array/object. */ -yyjson_api_inline size_t yyjson_get_len(yyjson_val *val); +/** Returns the content length for raw/string/array/object values. + Returns 0 if `val` is NULL. + The return value is unspecified for other types. */ +yyjson_api_inline size_t yyjson_get_len(const yyjson_val *val); -/** Returns whether the JSON value is equals to a string. - Returns false if input is NULL or type is not string. */ -yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str); +/** Returns whether the JSON value is equal to a string. + Returns false if `val` is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, + const char *str); -/** Returns whether the JSON value is equals to a string. +/** Returns whether the JSON value is equal to a string. The `str` should be a UTF-8 string, null-terminator is not required. - Returns false if input is NULL or type is not string. */ -yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, - size_t len); + Returns false if `val` is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_strn(const yyjson_val *val, + const char *str, size_t len); /** Returns whether two JSON values are equal (deep compare). - Returns false if input is NULL. + Returns false if `lhs` or `rhs` is NULL. @note the result may be inaccurate if object has duplicate keys. @warning This function is recursive and may cause a stack overflow if the object level is too deep. */ -yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); +yyjson_api_inline bool yyjson_equals(const yyjson_val *lhs, + const yyjson_val *rhs); /** Set the value to raw. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, const char *raw, size_t len); /** Set the value to null. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_null(yyjson_val *val); /** Set the value to bool. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_bool(yyjson_val *val, bool num); /** Set the value to uint. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_uint(yyjson_val *val, uint64_t num); /** Set the value to sint. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num); /** Set the value to int. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ -yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num); +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int64_t num); /** Set the value to float. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_float(yyjson_val *val, float num); /** Set the value to double. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_double(yyjson_val *val, double num); /** Set the value to real. - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_real(yyjson_val *val, double num); /** Set the floating-point number's output format to fixed-point notation. - Returns false if input is NULL or `val` is not real type. + Returns false if `val` is NULL or is not real type. @see YYJSON_WRITE_FP_TO_FIXED flag. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_fp_to_fixed(yyjson_val *val, int prec); /** Set the floating-point number's output format to single-precision. - Returns false if input is NULL or `val` is not real type. + Returns false if `val` is NULL or is not real type. @see YYJSON_WRITE_FP_TO_FLOAT flag. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_fp_to_float(yyjson_val *val, bool flt); /** Set the value to string (null-terminated). - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_str(yyjson_val *val, const char *str); /** Set the value to string (with length). - Returns false if input is NULL or `val` is object or array. + Returns false if `val` is NULL or is object or array. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, const char *str, size_t len); @@ -1938,7 +2150,7 @@ yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, /** Marks this string as not needing to be escaped during JSON writing. This can be used to avoid the overhead of escaping if the string contains only characters that do not require escaping. - Returns false if input is NULL or `val` is not string. + Returns false if `val` is NULL or is not string. @see YYJSON_SUBTYPE_NOESC subtype. @warning This will modify the `immutable` value, use with caution. */ yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc); @@ -1951,23 +2163,23 @@ yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc); /** Returns the number of elements in this array. Returns 0 if `arr` is NULL or type is not array. */ -yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr); +yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr); /** Returns the element at the specified position in this array. Returns NULL if array is NULL/empty or the index is out of bounds. @warning This function takes a linear search time if array is not flat. For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat. */ -yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx); +yyjson_api_inline yyjson_val *yyjson_arr_get(const yyjson_val *arr, size_t idx); /** Returns the first element of this array. Returns NULL if `arr` is NULL/empty or type is not array. */ -yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr); +yyjson_api_inline yyjson_val *yyjson_arr_get_first(const yyjson_val *arr); /** Returns the last element of this array. Returns NULL if `arr` is NULL/empty or type is not array. @warning This function takes a linear search time if array is not flat. For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat.*/ -yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr); +yyjson_api_inline yyjson_val *yyjson_arr_get_last(const yyjson_val *arr); @@ -1997,36 +2209,36 @@ typedef struct yyjson_arr_iter { Initialize an iterator for this array. @param arr The array to be iterated over. - If this parameter is NULL or not an array, `iter` will be set to empty. + If `arr` is NULL or not an array, `iter` is cleared. @param iter The iterator to be initialized. - If this parameter is NULL, the function will fail and return false. + If `iter` is NULL, returns false. @return true if the `iter` has been successfully initialized. @note The iterator does not need to be destroyed. */ -yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, +yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter); /** - Create an iterator with an array , same as `yyjson_arr_iter_init()`. + Create an iterator with an array, same as `yyjson_arr_iter_init()`. @param arr The array to be iterated over. - If this parameter is NULL or not an array, an empty iterator will returned. + If `arr` is NULL or not an array, returns an empty iterator. @return A new iterator for the array. @note The iterator does not need to be destroyed. */ -yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr); +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(const yyjson_val *arr); /** Returns whether the iteration has more elements. - If `iter` is NULL, this function will return false. + If `iter` is NULL, returns false. */ yyjson_api_inline bool yyjson_arr_iter_has_next(yyjson_arr_iter *iter); /** Returns the next element in the iteration, or NULL on end. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter); @@ -2059,7 +2271,7 @@ yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter); /** Returns the number of key-value pairs in this object. Returns 0 if `obj` is NULL or type is not object. */ -yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj); +yyjson_api_inline size_t yyjson_obj_size(const yyjson_val *obj); /** Returns the value to which the specified key is mapped. Returns NULL if this object contains no mapping for the key. @@ -2068,7 +2280,8 @@ yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj); The `key` should be a null-terminated UTF-8 string. @warning This function takes a linear search time. */ -yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, const char *key); +yyjson_api_inline yyjson_val *yyjson_obj_get(const yyjson_val *obj, + const char *key); /** Returns the value to which the specified key is mapped. Returns NULL if this object contains no mapping for the key. @@ -2078,8 +2291,8 @@ yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, const char *key); The `key_len` should be the length of the key, in bytes. @warning This function takes a linear search time. */ -yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, const char *key, - size_t key_len); +yyjson_api_inline yyjson_val *yyjson_obj_getn(const yyjson_val *obj, + const char *key, size_t key_len); @@ -2122,42 +2335,42 @@ typedef struct yyjson_obj_iter { Initialize an iterator for this object. @param obj The object to be iterated over. - If this parameter is NULL or not an object, `iter` will be set to empty. + If `obj` is NULL or not an object, `iter` is cleared. @param iter The iterator to be initialized. - If this parameter is NULL, the function will fail and return false. + If `iter` is NULL, returns false. @return true if the `iter` has been successfully initialized. @note The iterator does not need to be destroyed. */ -yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, +yyjson_api_inline bool yyjson_obj_iter_init(const yyjson_val *obj, yyjson_obj_iter *iter); /** Create an iterator with an object, same as `yyjson_obj_iter_init()`. @param obj The object to be iterated over. - If this parameter is NULL or not an object, an empty iterator will returned. + If `obj` is NULL or not an object, returns an empty iterator. @return A new iterator for the object. @note The iterator does not need to be destroyed. */ -yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj); +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(const yyjson_val *obj); /** Returns whether the iteration has more elements. - If `iter` is NULL, this function will return false. + If `iter` is NULL, returns false. */ yyjson_api_inline bool yyjson_obj_iter_has_next(yyjson_obj_iter *iter); /** Returns the next key in the iteration, or NULL on end. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_val *yyjson_obj_iter_next(yyjson_obj_iter *iter); /** Returns the value for key inside the iteration. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key); @@ -2173,7 +2386,7 @@ yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key); @param iter The object iterator, should not be NULL. @param key The key, should be a UTF-8 string with null-terminator. @return The value to which the specified key is mapped. - NULL if this object contains no mapping for the key or input is invalid. + NULL if the key is not found or arguments are invalid. @warning This function takes a linear search time if the key is not nearby. */ @@ -2191,9 +2404,9 @@ yyjson_api_inline yyjson_val *yyjson_obj_iter_get(yyjson_obj_iter *iter, @param iter The object iterator, should not be NULL. @param key The key, should be a UTF-8 string, null-terminator is not required. - @param key_len The the length of `key`, in bytes. + @param key_len The length of `key`, in bytes. @return The value to which the specified key is mapped. - NULL if this object contains no mapping for the key or input is invalid. + NULL if the key is not found or arguments are invalid. @warning This function takes a linear search time if the key is not nearby. */ @@ -2284,31 +2497,31 @@ yyjson_api yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc); This makes a `deep-copy` on the immutable document. If allocator is NULL, the default allocator will be used. @note `imut_doc` -> `mut_doc`. */ -yyjson_api yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, +yyjson_api yyjson_mut_doc *yyjson_doc_mut_copy(const yyjson_doc *doc, const yyjson_alc *alc); /** Copies and returns a new mutable document from input, returns NULL on error. This makes a `deep-copy` on the mutable document. If allocator is NULL, the default allocator will be used. @note `mut_doc` -> `mut_doc`. */ -yyjson_api yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, +yyjson_api yyjson_mut_doc *yyjson_mut_doc_mut_copy(const yyjson_mut_doc *doc, const yyjson_alc *alc); /** Copies and returns a new mutable value from input, returns NULL on error. This makes a `deep-copy` on the immutable value. - The memory was managed by mutable document. + The memory is managed by the mutable document. @note `imut_val` -> `mut_val`. */ yyjson_api yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *doc, - yyjson_val *val); + const yyjson_val *val); /** Copies and returns a new mutable value from input, returns NULL on error. This makes a `deep-copy` on the mutable value. - The memory was managed by mutable document. + The memory is managed by the mutable document. @note `mut_val` -> `mut_val`. @warning This function is recursive and may cause a stack overflow if the object level is too deep. */ yyjson_api yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, - yyjson_mut_val *val); + const yyjson_mut_val *val); /** Copies and returns a new immutable document from input, returns NULL on error. This makes a `deep-copy` on the mutable document. @@ -2316,7 +2529,7 @@ yyjson_api yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, @note `mut_doc` -> `imut_doc`. @warning This function is recursive and may cause a stack overflow if the object level is too deep. */ -yyjson_api yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *doc, +yyjson_api yyjson_doc *yyjson_mut_doc_imut_copy(const yyjson_mut_doc *doc, const yyjson_alc *alc); /** Copies and returns a new immutable document from input, @@ -2325,7 +2538,7 @@ yyjson_api yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *doc, @note `mut_val` -> `imut_doc`. @warning This function is recursive and may cause a stack overflow if the object level is too deep. */ -yyjson_api yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *val, +yyjson_api yyjson_doc *yyjson_mut_val_imut_copy(const yyjson_mut_val *val, const yyjson_alc *alc); @@ -2336,59 +2549,59 @@ yyjson_api yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *val, /** Returns whether the JSON value is raw. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_raw(const yyjson_mut_val *val); /** Returns whether the JSON value is `null`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_null(const yyjson_mut_val *val); /** Returns whether the JSON value is `true`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_true(const yyjson_mut_val *val); /** Returns whether the JSON value is `false`. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_false(const yyjson_mut_val *val); /** Returns whether the JSON value is bool (true/false). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_bool(const yyjson_mut_val *val); /** Returns whether the JSON value is unsigned integer (uint64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_uint(const yyjson_mut_val *val); /** Returns whether the JSON value is signed integer (int64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_sint(const yyjson_mut_val *val); /** Returns whether the JSON value is integer (uint64_t/int64_t). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_int(const yyjson_mut_val *val); /** Returns whether the JSON value is real number (double). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_real(const yyjson_mut_val *val); /** Returns whether the JSON value is number (uint/sint/real). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_num(const yyjson_mut_val *val); /** Returns whether the JSON value is string. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_str(const yyjson_mut_val *val); /** Returns whether the JSON value is array. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val); /** Returns whether the JSON value is object. Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val); /** Returns whether the JSON value is container (array/object). Returns false if `val` is NULL. */ -yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_is_ctn(const yyjson_mut_val *val); @@ -2398,144 +2611,147 @@ yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val); /** Returns the JSON value's type. Returns `YYJSON_TYPE_NONE` if `val` is NULL. */ -yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val); +yyjson_api_inline yyjson_type yyjson_mut_get_type(const yyjson_mut_val *val); /** Returns the JSON value's subtype. Returns `YYJSON_SUBTYPE_NONE` if `val` is NULL. */ -yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val); +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype( + const yyjson_mut_val *val); /** Returns the JSON value's tag. Returns 0 if `val` is NULL. */ -yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val); +yyjson_api_inline uint8_t yyjson_mut_get_tag(const yyjson_mut_val *val); /** Returns the JSON value's type description. The return value should be one of these strings: "raw", "null", "string", "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ -yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val); +yyjson_api_inline const char *yyjson_mut_get_type_desc( + const yyjson_mut_val *val); /** Returns the content if the value is raw. Returns NULL if `val` is NULL or type is not raw. */ -yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val); +yyjson_api_inline const char *yyjson_mut_get_raw(const yyjson_mut_val *val); /** Returns the content if the value is bool. Returns NULL if `val` is NULL or type is not bool. */ -yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val); +yyjson_api_inline bool yyjson_mut_get_bool(const yyjson_mut_val *val); /** Returns the content and cast to uint64_t. Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val); +yyjson_api_inline uint64_t yyjson_mut_get_uint(const yyjson_mut_val *val); /** Returns the content and cast to int64_t. Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val); +yyjson_api_inline int64_t yyjson_mut_get_sint(const yyjson_mut_val *val); /** Returns the content and cast to int. Returns 0 if `val` is NULL or type is not integer(sint/uint). */ -yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val); +yyjson_api_inline int yyjson_mut_get_int(const yyjson_mut_val *val); /** Returns the content if the value is real number. Returns 0.0 if `val` is NULL or type is not real(double). */ -yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val); +yyjson_api_inline double yyjson_mut_get_real(const yyjson_mut_val *val); -/** Returns the content and typecast to `double` if the value is number. +/** Returns the content cast to `double` if the value is a number. Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ -yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val); +yyjson_api_inline double yyjson_mut_get_num(const yyjson_mut_val *val); /** Returns the content if the value is string. Returns NULL if `val` is NULL or type is not string. */ -yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val); +yyjson_api_inline const char *yyjson_mut_get_str(const yyjson_mut_val *val); -/** Returns the content length (string length, array size, object size. - Returns 0 if `val` is NULL or type is not string/array/object. */ -yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val); +/** Returns the content length for raw/string/array/object values. + Returns 0 if `val` is NULL. + The return value is unspecified for other types. */ +yyjson_api_inline size_t yyjson_mut_get_len(const yyjson_mut_val *val); -/** Returns whether the JSON value is equals to a string. +/** Returns whether the JSON value is equal to a string. The `str` should be a null-terminated UTF-8 string. - Returns false if input is NULL or type is not string. */ -yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, + Returns false if `val` is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_str(const yyjson_mut_val *val, const char *str); -/** Returns whether the JSON value is equals to a string. +/** Returns whether the JSON value is equal to a string. The `str` should be a UTF-8 string, null-terminator is not required. - Returns false if input is NULL or type is not string. */ -yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, + Returns false if `val` is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_strn(const yyjson_mut_val *val, const char *str, size_t len); /** Returns whether two JSON values are equal (deep compare). - Returns false if input is NULL. + Returns false if `lhs` or `rhs` is NULL. @note the result may be inaccurate if object has duplicate keys. @warning This function is recursive and may cause a stack overflow if the object level is too deep. */ -yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, - yyjson_mut_val *rhs); +yyjson_api_inline bool yyjson_mut_equals(const yyjson_mut_val *lhs, + const yyjson_mut_val *rhs); /** Set the value to raw. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_raw(yyjson_mut_val *val, const char *raw, size_t len); /** Set the value to null. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_null(yyjson_mut_val *val); /** Set the value to bool. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_bool(yyjson_mut_val *val, bool num); /** Set the value to uint. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_uint(yyjson_mut_val *val, uint64_t num); /** Set the value to sint. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num); /** Set the value to int. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ -yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num); +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int64_t num); /** Set the value to float. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_float(yyjson_mut_val *val, float num); /** Set the value to double. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_double(yyjson_mut_val *val, double num); /** Set the value to real. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_real(yyjson_mut_val *val, double num); /** Set the floating-point number's output format to fixed-point notation. - Returns false if input is NULL or `val` is not real type. + Returns false if `val` is NULL or is not real type. @see YYJSON_WRITE_FP_TO_FIXED flag. - @warning This will modify the `immutable` value, use with caution. */ + @warning This will modify the `mutable` value, use with caution. */ yyjson_api_inline bool yyjson_mut_set_fp_to_fixed(yyjson_mut_val *val, int prec); /** Set the floating-point number's output format to single-precision. - Returns false if input is NULL or `val` is not real type. + Returns false if `val` is NULL or is not real type. @see YYJSON_WRITE_FP_TO_FLOAT flag. - @warning This will modify the `immutable` value, use with caution. */ + @warning This will modify the `mutable` value, use with caution. */ yyjson_api_inline bool yyjson_mut_set_fp_to_float(yyjson_mut_val *val, bool flt); /** Set the value to string (null-terminated). - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_str(yyjson_mut_val *val, const char *str); /** Set the value to string (with length). - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, const char *str, size_t len); @@ -2543,19 +2759,19 @@ yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, /** Marks this string as not needing to be escaped during JSON writing. This can be used to avoid the overhead of escaping if the string contains only characters that do not require escaping. - Returns false if input is NULL or `val` is not string. + Returns false if `val` is NULL or is not string. @see YYJSON_SUBTYPE_NOESC subtype. - @warning This will modify the `immutable` value, use with caution. */ + @warning This will modify the `mutable` value, use with caution. */ yyjson_api_inline bool yyjson_mut_set_str_noesc(yyjson_mut_val *val, bool noesc); /** Set the value to array. - Returns false if input is NULL. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_arr(yyjson_mut_val *val); -/** Set the value to array. - Returns false if input is NULL. +/** Set the value to object. + Returns false if `val` is NULL. @warning This function should not be used on an existing object or array. */ yyjson_api_inline bool yyjson_mut_set_obj(yyjson_mut_val *val); @@ -2668,21 +2884,23 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, /** Returns the number of elements in this array. Returns 0 if `arr` is NULL or type is not array. */ -yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr); +yyjson_api_inline size_t yyjson_mut_arr_size(const yyjson_mut_val *arr); /** Returns the element at the specified position in this array. Returns NULL if array is NULL/empty or the index is out of bounds. @warning This function takes a linear search time. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(const yyjson_mut_val *arr, size_t idx); /** Returns the first element of this array. Returns NULL if `arr` is NULL/empty or type is not array. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first(yyjson_mut_val *arr); +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first( + const yyjson_mut_val *arr); /** Returns the last element of this array. Returns NULL if `arr` is NULL/empty or type is not array. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last(yyjson_mut_val *arr); +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last( + const yyjson_mut_val *arr); @@ -2720,9 +2938,9 @@ typedef struct yyjson_mut_arr_iter { Initialize an iterator for this array. @param arr The array to be iterated over. - If this parameter is NULL or not an array, `iter` will be set to empty. + If `arr` is NULL or not an array, `iter` is cleared. @param iter The iterator to be initialized. - If this parameter is NULL, the function will fail and return false. + If `iter` is NULL, returns false. @return true if the `iter` has been successfully initialized. @note The iterator does not need to be destroyed. @@ -2731,10 +2949,10 @@ yyjson_api_inline bool yyjson_mut_arr_iter_init(yyjson_mut_val *arr, yyjson_mut_arr_iter *iter); /** - Create an iterator with an array , same as `yyjson_mut_arr_iter_init()`. + Create an iterator with an array, same as `yyjson_mut_arr_iter_init()`. @param arr The array to be iterated over. - If this parameter is NULL or not an array, an empty iterator will returned. + If `arr` is NULL or not an array, returns an empty iterator. @return A new iterator for the array. @note The iterator does not need to be destroyed. @@ -2744,21 +2962,21 @@ yyjson_api_inline yyjson_mut_arr_iter yyjson_mut_arr_iter_with( /** Returns whether the iteration has more elements. - If `iter` is NULL, this function will return false. + If `iter` is NULL, returns false. */ yyjson_api_inline bool yyjson_mut_arr_iter_has_next( yyjson_mut_arr_iter *iter); /** Returns the next element in the iteration, or NULL on end. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( yyjson_mut_arr_iter *iter); /** Removes and returns current element in the iteration. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( yyjson_mut_arr_iter *iter); @@ -2795,7 +3013,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( /** Creates and returns an empty mutable array. @param doc A mutable document, used for memory allocation only. - @return The new array. NULL if input is NULL or memory allocation failed. + @return The new array. NULL if `doc` is NULL or allocation fails. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc); @@ -2803,10 +3021,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc); Creates and returns a new mutable array with the given boolean values. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of boolean values. - @param count The value count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The value count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2821,10 +3039,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_bool( Creates and returns a new mutable array with the given sint numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of sint numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2839,10 +3057,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint( Creates and returns a new mutable array with the given uint numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of uint numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2857,10 +3075,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint( Creates and returns a new mutable array with the given real numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of real numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2875,10 +3093,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_real( Creates and returns a new mutable array with the given int8 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of int8 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2893,10 +3111,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint8( Creates and returns a new mutable array with the given int16 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of int16 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2911,10 +3129,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint16( Creates and returns a new mutable array with the given int32 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of int32 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2929,10 +3147,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint32( Creates and returns a new mutable array with the given int64 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of int64 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2947,10 +3165,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint64( Creates and returns a new mutable array with the given uint8 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of uint8 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2965,10 +3183,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint8( Creates and returns a new mutable array with the given uint16 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of uint16 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -2983,10 +3201,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint16( Creates and returns a new mutable array with the given uint32 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of uint32 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3001,10 +3219,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint32( Creates and returns a new mutable array with the given uint64 numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of uint64 numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3019,10 +3237,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint64( Creates and returns a new mutable array with the given float numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of float numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3037,10 +3255,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_float( Creates and returns a new mutable array with the given double numbers. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of double numbers. - @param count The number count. If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + @param count The number count. If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3056,12 +3274,12 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_double( will not be copied. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of UTF-8 null-terminator strings. - If this array contains NULL, the function will fail and return NULL. + If `vals` contains NULL, returns NULL. @param count The number of values in `vals`. - If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @warning The input strings are not copied, you should keep these strings unmodified for the lifetime of this JSON document. If these strings will be @@ -3081,13 +3299,13 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_str( lengths, these strings will not be copied. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of UTF-8 strings, null-terminator is not required. - If this array contains NULL, the function will fail and return NULL. + If `vals` contains NULL, returns NULL. @param lens A C array of string lengths, in bytes. @param count The number of strings in `vals`. - If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @warning The input strings are not copied, you should keep these strings unmodified for the lifetime of this JSON document. If these strings will be @@ -3108,12 +3326,12 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strn( will be copied. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of UTF-8 null-terminator strings. - If this array contains NULL, the function will fail and return NULL. + If `vals` contains NULL, returns NULL. @param count The number of values in `vals`. - If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3129,13 +3347,13 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strcpy( lengths, these strings will be copied. @param doc A mutable document, used for memory allocation only. - If this parameter is NULL, the function will fail and return NULL. + If `doc` is NULL, returns NULL. @param vals A C array of UTF-8 strings, null-terminator is not required. - If this array contains NULL, the function will fail and return NULL. + If `vals` contains NULL, returns NULL. @param lens A C array of string lengths, in bytes. @param count The number of strings in `vals`. - If this value is 0, an empty array will return. - @return The new array. NULL if input is invalid or memory allocation failed. + If this value is 0, an empty array is returned. + @return The new array. NULL if arguments are invalid or allocation fails. @b Example @code @@ -3160,7 +3378,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strncpy( @param val The value to be inserted. Returns false if it is NULL. @param idx The index to which to insert the new value. Returns false if the index is out of range. - @return Whether successful. + @return Whether the operation was successful. @warning This function takes a linear search time. */ yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, @@ -3171,7 +3389,7 @@ yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param val The value to be inserted. Returns false if it is NULL. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, yyjson_mut_val *val); @@ -3236,7 +3454,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_last( Returns false if it is NULL or not an array. @param idx The start index of the range (0 is the first). @param len The number of items in the range (can be 0). - @return Whether successful. + @return Whether the operation was successful. @warning This function takes a linear search time. */ yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, @@ -3246,7 +3464,7 @@ yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, Removes all values in this array. @param arr The array from which all of the values are to be removed. Returns false if it is NULL or not an array. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_clear(yyjson_mut_val *arr); @@ -3271,7 +3489,7 @@ yyjson_api_inline bool yyjson_mut_arr_rotate(yyjson_mut_val *arr, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param val The value to be inserted. Returns false if it is NULL. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, yyjson_mut_val *val); @@ -3281,7 +3499,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, @param doc The `doc` is only used for memory allocation. @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, yyjson_mut_val *arr); @@ -3291,7 +3509,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, @param doc The `doc` is only used for memory allocation. @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, yyjson_mut_val *arr); @@ -3301,7 +3519,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, @param doc The `doc` is only used for memory allocation. @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, yyjson_mut_val *arr); @@ -3312,7 +3530,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param val The bool value to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3324,7 +3542,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3336,7 +3554,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3348,7 +3566,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3360,7 +3578,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3372,7 +3590,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3384,7 +3602,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param num The number to be added. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3396,7 +3614,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param str A null-terminated UTF-8 string. - @return Whether successful. + @return Whether the operation was successful. @warning The input string is not copied, you should keep this string unmodified for the lifetime of this JSON document. */ @@ -3411,7 +3629,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_str(yyjson_mut_doc *doc, Returns false if it is NULL or not an array. @param str A UTF-8 string, null-terminator is not required. @param len The length of the string, in bytes. - @return Whether successful. + @return Whether the operation was successful. @warning The input string is not copied, you should keep this string unmodified for the lifetime of this JSON document. */ @@ -3426,7 +3644,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_strn(yyjson_mut_doc *doc, @param arr The array to which the value is to be inserted. Returns false if it is NULL or not an array. @param str A null-terminated UTF-8 string. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3439,7 +3657,7 @@ yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, Returns false if it is NULL or not an array. @param str A UTF-8 string, null-terminator is not required. @param len The length of the string, in bytes. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_arr_add_strncpy(yyjson_mut_doc *doc, yyjson_mut_val *arr, @@ -3474,7 +3692,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, /** Returns the number of key-value pairs in this object. Returns 0 if `obj` is NULL or type is not object. */ -yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj); +yyjson_api_inline size_t yyjson_mut_obj_size(const yyjson_mut_val *obj); /** Returns the value to which the specified key is mapped. Returns NULL if this object contains no mapping for the key. @@ -3483,7 +3701,7 @@ yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj); The `key` should be a null-terminated UTF-8 string. @warning This function takes a linear search time. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key); /** Returns the value to which the specified key is mapped. @@ -3494,7 +3712,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, The `key_len` should be the length of the key, in bytes. @warning This function takes a linear search time. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(const yyjson_mut_val *obj, const char *key, size_t key_len); @@ -3546,9 +3764,9 @@ typedef struct yyjson_mut_obj_iter { Initialize an iterator for this object. @param obj The object to be iterated over. - If this parameter is NULL or not an array, `iter` will be set to empty. + If `obj` is NULL or not an object, `iter` is cleared. @param iter The iterator to be initialized. - If this parameter is NULL, the function will fail and return false. + If `iter` is NULL, returns false. @return true if the `iter` has been successfully initialized. @note The iterator does not need to be destroyed. @@ -3557,10 +3775,10 @@ yyjson_api_inline bool yyjson_mut_obj_iter_init(yyjson_mut_val *obj, yyjson_mut_obj_iter *iter); /** - Create an iterator with an object, same as `yyjson_obj_iter_init()`. + Create an iterator with an object, same as `yyjson_mut_obj_iter_init()`. @param obj The object to be iterated over. - If this parameter is NULL or not an object, an empty iterator will returned. + If `obj` is NULL or not an object, returns an empty iterator. @return A new iterator for the object. @note The iterator does not need to be destroyed. @@ -3570,28 +3788,28 @@ yyjson_api_inline yyjson_mut_obj_iter yyjson_mut_obj_iter_with( /** Returns whether the iteration has more elements. - If `iter` is NULL, this function will return false. + If `iter` is NULL, returns false. */ yyjson_api_inline bool yyjson_mut_obj_iter_has_next( yyjson_mut_obj_iter *iter); /** Returns the next key in the iteration, or NULL on end. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_next( yyjson_mut_obj_iter *iter); /** Returns the value for key inside the iteration. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( yyjson_mut_val *key); /** Removes current key-value pair in the iteration, returns the removed value. - If `iter` is NULL, this function will return NULL. + If `iter` is NULL, returns NULL. */ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( yyjson_mut_obj_iter *iter); @@ -3608,7 +3826,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( @param iter The object iterator, should not be NULL. @param key The key, should be a UTF-8 string with null-terminator. @return The value to which the specified key is mapped. - NULL if this object contains no mapping for the key or input is invalid. + NULL if the key is not found or arguments are invalid. @warning This function takes a linear search time if the key is not nearby. */ @@ -3626,9 +3844,9 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get( @param iter The object iterator, should not be NULL. @param key The key, should be a UTF-8 string, null-terminator is not required. - @param key_len The the length of `key`, in bytes. + @param key_len The length of `key`, in bytes. @return The value to which the specified key is mapped. - NULL if this object contains no mapping for the key or input is invalid. + NULL if the key is not found or arguments are invalid. @warning This function takes a linear search time if the key is not nearby. */ @@ -3671,10 +3889,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj(yyjson_mut_doc *doc); /** Creates and returns a mutable object with keys and values, returns NULL on - error. The keys and values are not copied. The strings should be a - null-terminated UTF-8 string. + error. The keys and values are not copied. They should be null-terminated + UTF-8 strings. - @warning The input string is not copied, you should keep this string + @warning The input strings are not copied; you should keep them unmodified for the lifetime of this JSON document. @b Example @@ -3691,10 +3909,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, /** Creates and returns a mutable object with key-value pairs and pair count, - returns NULL on error. The keys and values are not copied. The strings should - be a null-terminated UTF-8 string. + returns NULL on error. The keys and values are not copied. They should be + null-terminated UTF-8 strings. - @warning The input string is not copied, you should keep this string + @warning The input strings are not copied; you should keep them unmodified for the lifetime of this JSON document. @b Example @@ -3715,25 +3933,25 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, /** Adds a key-value pair at the end of the object. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @param obj The object to which the new key-value pair is to be added. @param key The key, should be a string which is created by `yyjson_mut_str()`, `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. @param val The value to add to the object. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val); /** Sets a key-value pair at the end of the object. - This function may remove all key-value pairs for the given key before add. + This function may remove all key-value pairs for the given key before adding. @param obj The object to which the new key-value pair is to be added. @param key The key, should be a string which is created by `yyjson_mut_str()`, `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. @param val The value to add to the object. If this value is null, the behavior - is same as `yyjson_mut_obj_remove()`. - @return Whether successful. + is the same as `yyjson_mut_obj_remove()`. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, yyjson_mut_val *key, @@ -3741,13 +3959,13 @@ yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, /** Inserts a key-value pair to the object at the given position. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @param obj The object to which the new key-value pair is to be added. @param key The key, should be a string which is created by `yyjson_mut_str()`, `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. @param val The value to add to the object. @param idx The index to which to insert the new pair. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, yyjson_mut_val *key, @@ -3755,7 +3973,7 @@ yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, size_t idx); /** - Removes all key-value pair from the object with given key. + Removes all key-value pairs from the object with the given key. @param obj The object from which the key-value pair is to be removed. @param key The key, should be a string value. @return The first matched value, or NULL if no matched value. @@ -3765,7 +3983,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove(yyjson_mut_val *obj, yyjson_mut_val *key); /** - Removes all key-value pair from the object with given key. + Removes all key-value pairs from the object with the given key. @param obj The object from which the key-value pair is to be removed. @param key The key, should be a UTF-8 string with null-terminator. @return The first matched value, or NULL if no matched value. @@ -3775,7 +3993,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_key( yyjson_mut_val *obj, const char *key); /** - Removes all key-value pair from the object with given key. + Removes all key-value pairs from the object with the given key. @param obj The object from which the key-value pair is to be removed. @param key The key, should be a UTF-8 string, null-terminator is not required. @param key_len The length of the key. @@ -3788,17 +4006,17 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_keyn( /** Removes all key-value pairs in this object. @param obj The object from which all of the values are to be removed. - @return Whether successful. + @return Whether the operation was successful. */ yyjson_api_inline bool yyjson_mut_obj_clear(yyjson_mut_val *obj); /** Replaces value from the object with given key. - If the key is not exist, or the value is NULL, it will fail. + If the key does not exist, or the value is NULL, it will fail. @param obj The object to which the value is to be replaced. @param key The key, should be a string value. @param val The value to replace into the object. - @return Whether successful. + @return Whether the operation was successful. @warning This function takes a linear search time. */ yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, @@ -3811,7 +4029,7 @@ yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, `{"b":2,"c":3,"d":4,"a":1}`. @param obj The object to be rotated. @param idx Index (or times) to rotate. - @return Whether successful. + @return Whether the operation was successful. @warning This function takes a linear search time. */ yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, @@ -3825,7 +4043,7 @@ yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, /** Adds a `null` value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3835,7 +4053,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, /** Adds a `true` value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3845,7 +4063,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_true(yyjson_mut_doc *doc, /** Adds a `false` value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3855,7 +4073,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_false(yyjson_mut_doc *doc, /** Adds a bool value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3865,7 +4083,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, /** Adds an unsigned integer value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3875,7 +4093,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_uint(yyjson_mut_doc *doc, /** Adds a signed integer value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3885,7 +4103,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_sint(yyjson_mut_doc *doc, /** Adds an int value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3895,7 +4113,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, /** Adds a float value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3905,7 +4123,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_float(yyjson_mut_doc *doc, /** Adds a double value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3915,7 +4133,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_double(yyjson_mut_doc *doc, /** Adds a real value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3925,7 +4143,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_real(yyjson_mut_doc *doc, /** Adds a string value at the end of the object. The `key` and `val` should be null-terminated UTF-8 strings. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key/value strings are not copied, you should keep these strings unmodified for the lifetime of this JSON document. */ @@ -3937,7 +4155,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_str(yyjson_mut_doc *doc, The `key` should be a null-terminated UTF-8 string. The `val` should be a UTF-8 string, null-terminator is not required. The `len` should be the length of the `val`, in bytes. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key/value strings are not copied, you should keep these strings unmodified for the lifetime of this JSON document. */ @@ -3949,7 +4167,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_strn(yyjson_mut_doc *doc, /** Adds a string value at the end of the object. The `key` and `val` should be null-terminated UTF-8 strings. The value string is copied. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -3962,7 +4180,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, The `key` should be a null-terminated UTF-8 string. The `val` should be a UTF-8 string, null-terminator is not required. The `len` should be the length of the `val`, in bytes. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key strings are not copied, you should keep these strings unmodified for the lifetime of this JSON document. */ @@ -3974,7 +4192,7 @@ yyjson_api_inline bool yyjson_mut_obj_add_strncpy(yyjson_mut_doc *doc, /** Creates and adds a new array to the target object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep these strings unmodified for the lifetime of this JSON document. @@ -3987,7 +4205,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc, /** Creates and adds a new object to the target object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep these strings unmodified for the lifetime of this JSON document. @@ -3999,7 +4217,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc, /** Adds a JSON value at the end of the object. The `key` should be a null-terminated UTF-8 string. - This function allows duplicated key in one object. + This function allows duplicate keys in one object. @warning The key string is not copied, you should keep the string unmodified for the lifetime of this JSON document. */ @@ -4073,7 +4291,7 @@ static const yyjson_ptr_code YYJSON_PTR_ERR_NONE = 0; /** Invalid input parameter, such as NULL input. */ static const yyjson_ptr_code YYJSON_PTR_ERR_PARAMETER = 1; -/** JSON pointer syntax error, such as invalid escape, token no prefix. */ +/** JSON pointer syntax error, such as invalid escape or missing prefix. */ static const yyjson_ptr_code YYJSON_PTR_ERR_SYNTAX = 2; /** JSON pointer resolve failed, such as index out of range, key not found. */ @@ -4149,7 +4367,7 @@ typedef struct yyjson_ptr_ctx { @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(const yyjson_doc *doc, const char *ptr); /** @@ -4160,7 +4378,7 @@ yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(const yyjson_doc *doc, const char *ptr, size_t len); /** @@ -4172,7 +4390,7 @@ yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(const yyjson_doc *doc, const char *ptr, size_t len, yyjson_ptr_err *err); @@ -4183,7 +4401,7 @@ yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_get(const yyjson_val *val, const char *ptr); /** @@ -4194,7 +4412,7 @@ yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_getn(const yyjson_val *val, const char *ptr, size_t len); /** @@ -4206,7 +4424,7 @@ yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_getx(const yyjson_val *val, const char *ptr, size_t len, yyjson_ptr_err *err); @@ -4217,8 +4435,8 @@ yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, - const char *ptr); +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get( + const yyjson_mut_doc *doc, const char *ptr); /** Get value by a JSON Pointer. @@ -4228,9 +4446,8 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, - const char *ptr, - size_t len); +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn( + const yyjson_mut_doc *doc, const char *ptr, size_t len); /** Get value by a JSON Pointer. @@ -4242,11 +4459,9 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, @return The value referenced by the JSON pointer. NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, - const char *ptr, - size_t len, - yyjson_ptr_ctx *ctx, - yyjson_ptr_err *err); +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx( + const yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); /** Get value by a JSON Pointer. @@ -4255,7 +4470,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(const yyjson_mut_val *val, const char *ptr); /** @@ -4266,7 +4481,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(const yyjson_mut_val *val, const char *ptr, size_t len); @@ -4280,7 +4495,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, @return The value referenced by the JSON pointer. NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. */ -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(const yyjson_mut_val *val, const char *ptr, size_t len, yyjson_ptr_ctx *ctx, @@ -4317,7 +4532,7 @@ yyjson_api_inline bool yyjson_mut_doc_ptr_addn(yyjson_mut_doc *doc, @param ptr The JSON pointer string (UTF-8, null-terminator is not required). @param len The length of `ptr` in bytes. @param new_val The value to be added. - @param create_parent Whether to create parent nodes if not exist. + @param create_parent Whether to create parent nodes if they do not exist. @param ctx A pointer to store the result context, or NULL if not needed. @param err A pointer to store the error information, or NULL if not needed. @return true if JSON pointer is valid and new value is added, false otherwise. @@ -4365,7 +4580,7 @@ yyjson_api_inline bool yyjson_mut_ptr_addn(yyjson_mut_val *val, @param len The length of `ptr` in bytes. @param doc Only used to create new values when needed. @param new_val The value to be added. - @param create_parent Whether to create parent nodes if not exist. + @param create_parent Whether to create parent nodes if they do not exist. @param ctx A pointer to store the result context, or NULL if not needed. @param err A pointer to store the error information, or NULL if not needed. @return true if JSON pointer is valid and new value is added, false otherwise. @@ -4411,7 +4626,7 @@ yyjson_api_inline bool yyjson_mut_doc_ptr_setn(yyjson_mut_doc *doc, @param ptr The JSON pointer string (UTF-8, null-terminator is not required). @param len The length of `ptr` in bytes. @param new_val The value to be set, pass NULL to remove. - @param create_parent Whether to create parent nodes if not exist. + @param create_parent Whether to create parent nodes if they do not exist. @param ctx A pointer to store the result context, or NULL if not needed. @param err A pointer to store the error information, or NULL if not needed. @return true if JSON pointer is valid and new value is set, false otherwise. @@ -4462,7 +4677,7 @@ yyjson_api_inline bool yyjson_mut_ptr_setn(yyjson_mut_val *val, @param len The length of `ptr` in bytes. @param new_val The value to be set, pass NULL to remove. @param doc Only used to create new values when needed. - @param create_parent Whether to create parent nodes if not exist. + @param create_parent Whether to create parent nodes if they do not exist. @param ctx A pointer to store the result context, or NULL if not needed. @param err A pointer to store the error information, or NULL if not needed. @return true if JSON pointer is valid and new value is set, false otherwise. @@ -4629,7 +4844,7 @@ yyjson_api_inline bool yyjson_ptr_ctx_append(yyjson_ptr_ctx *ctx, @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. @param val New value to be replaced. @return true on success or false on fail. - @note If success, the old value will be returned via `ctx->old`. + @note On success, the old value will be returned via `ctx->old`. */ yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, yyjson_mut_val *val); @@ -4638,7 +4853,7 @@ yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, Remove value by JSON pointer context. @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. @return true on success or false on fail. - @note If success, the old value will be returned via `ctx->old`. + @note On success, the old value will be returned via `ctx->old`. */ yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx); @@ -4658,7 +4873,7 @@ static const yyjson_patch_code YYJSON_PATCH_SUCCESS = 0; /** Invalid parameter, such as NULL input or non-array patch. */ static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_PARAMETER = 1; -/** Memory allocation failure occurs. */ +/** Memory allocation failed. */ static const yyjson_patch_code YYJSON_PATCH_ERROR_MEMORY_ALLOCATION = 2; /** JSON patch operation is not object type. */ @@ -4670,7 +4885,7 @@ static const yyjson_patch_code YYJSON_PATCH_ERROR_MISSING_KEY = 4; /** JSON patch operation member is invalid. */ static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_MEMBER = 5; -/** JSON patch operation `test` not equal. */ +/** JSON patch `test` operation failed (values not equal). */ static const yyjson_patch_code YYJSON_PATCH_ERROR_EQUAL = 6; /** JSON patch operation failed on JSON pointer. */ @@ -4695,8 +4910,8 @@ typedef struct yyjson_patch_err { Returns NULL if the patch could not be applied. */ yyjson_api yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, - yyjson_val *orig, - yyjson_val *patch, + const yyjson_val *orig, + const yyjson_val *patch, yyjson_patch_err *err); /** @@ -4706,8 +4921,8 @@ yyjson_api yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, Returns NULL if the patch could not be applied. */ yyjson_api yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, - yyjson_mut_val *orig, - yyjson_mut_val *patch, + const yyjson_mut_val *orig, + const yyjson_mut_val *patch, yyjson_patch_err *err); @@ -4726,8 +4941,8 @@ yyjson_api yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, object level is too deep. */ yyjson_api yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, - yyjson_val *orig, - yyjson_val *patch); + const yyjson_val *orig, + const yyjson_val *patch); /** Creates and returns a merge-patched JSON value (RFC 7386). @@ -4738,8 +4953,8 @@ yyjson_api yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, object level is too deep. */ yyjson_api yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, - yyjson_mut_val *orig, - yyjson_mut_val *patch); + const yyjson_mut_val *orig, + const yyjson_mut_val *patch); #endif /* YYJSON_DISABLE_UTILS */ @@ -4774,7 +4989,7 @@ struct yyjson_doc { yyjson_alc alc; /** The total number of bytes read when parsing JSON (nonzero). */ size_t dat_read; - /** The total number of value read when parsing JSON (nonzero). */ + /** The total number of values read when parsing JSON (nonzero). */ size_t val_read; /** The string pool used by JSON values (nullable). */ char *str_pool; @@ -4799,7 +5014,7 @@ struct yyjson_doc { earlier versions are uncertain. @param str The C string. - @param len The returnd value from strlen(str). + @param len The returned value from strlen(str). */ yyjson_api_inline bool unsafe_yyjson_is_str_noesc(const char *str, size_t len) { #if YYJSON_HAS_CONSTANT_P && \ @@ -4851,154 +5066,156 @@ yyjson_api_inline double unsafe_yyjson_u64_to_f64(uint64_t num) { #endif } -yyjson_api_inline yyjson_type unsafe_yyjson_get_type(void *val) { - uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; +yyjson_api_inline yyjson_type unsafe_yyjson_get_type(const void *val) { + uint8_t tag = (uint8_t)((const yyjson_val *)val)->tag; return (yyjson_type)(tag & YYJSON_TYPE_MASK); } -yyjson_api_inline yyjson_subtype unsafe_yyjson_get_subtype(void *val) { - uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; +yyjson_api_inline yyjson_subtype unsafe_yyjson_get_subtype(const void *val) { + uint8_t tag = (uint8_t)((const yyjson_val *)val)->tag; return (yyjson_subtype)(tag & YYJSON_SUBTYPE_MASK); } -yyjson_api_inline uint8_t unsafe_yyjson_get_tag(void *val) { - uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; +yyjson_api_inline uint8_t unsafe_yyjson_get_tag(const void *val) { + uint8_t tag = (uint8_t)((const yyjson_val *)val)->tag; return (uint8_t)(tag & YYJSON_TAG_MASK); } -yyjson_api_inline bool unsafe_yyjson_is_raw(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_raw(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_RAW; } -yyjson_api_inline bool unsafe_yyjson_is_null(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_null(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NULL; } -yyjson_api_inline bool unsafe_yyjson_is_bool(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_bool(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_BOOL; } -yyjson_api_inline bool unsafe_yyjson_is_num(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_num(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NUM; } -yyjson_api_inline bool unsafe_yyjson_is_str(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_str(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_STR; } -yyjson_api_inline bool unsafe_yyjson_is_arr(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_arr(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_ARR; } -yyjson_api_inline bool unsafe_yyjson_is_obj(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_obj(const void *val) { return unsafe_yyjson_get_type(val) == YYJSON_TYPE_OBJ; } -yyjson_api_inline bool unsafe_yyjson_is_ctn(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_ctn(const void *val) { uint8_t mask = YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ; return (unsafe_yyjson_get_tag(val) & mask) == mask; } -yyjson_api_inline bool unsafe_yyjson_is_uint(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_uint(const void *val) { const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; return unsafe_yyjson_get_tag(val) == patt; } -yyjson_api_inline bool unsafe_yyjson_is_sint(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_sint(const void *val) { const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT; return unsafe_yyjson_get_tag(val) == patt; } -yyjson_api_inline bool unsafe_yyjson_is_int(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_int(const void *val) { const uint8_t mask = YYJSON_TAG_MASK & (~YYJSON_SUBTYPE_SINT); const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; return (unsafe_yyjson_get_tag(val) & mask) == patt; } -yyjson_api_inline bool unsafe_yyjson_is_real(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_real(const void *val) { const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; return unsafe_yyjson_get_tag(val) == patt; } -yyjson_api_inline bool unsafe_yyjson_is_true(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_true(const void *val) { const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE; return unsafe_yyjson_get_tag(val) == patt; } -yyjson_api_inline bool unsafe_yyjson_is_false(void *val) { +yyjson_api_inline bool unsafe_yyjson_is_false(const void *val) { const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE; return unsafe_yyjson_get_tag(val) == patt; } -yyjson_api_inline bool unsafe_yyjson_arr_is_flat(yyjson_val *val) { +yyjson_api_inline bool unsafe_yyjson_arr_is_flat(const yyjson_val *val) { size_t ofs = val->uni.ofs; size_t len = (size_t)(val->tag >> YYJSON_TAG_BIT); return len * sizeof(yyjson_val) + sizeof(yyjson_val) == ofs; } -yyjson_api_inline const char *unsafe_yyjson_get_raw(void *val) { - return ((yyjson_val *)val)->uni.str; +yyjson_api_inline const char *unsafe_yyjson_get_raw(const void *val) { + return ((const yyjson_val *)val)->uni.str; } -yyjson_api_inline bool unsafe_yyjson_get_bool(void *val) { +yyjson_api_inline bool unsafe_yyjson_get_bool(const void *val) { uint8_t tag = unsafe_yyjson_get_tag(val); return (bool)((tag & YYJSON_SUBTYPE_MASK) >> YYJSON_TYPE_BIT); } -yyjson_api_inline uint64_t unsafe_yyjson_get_uint(void *val) { - return ((yyjson_val *)val)->uni.u64; +yyjson_api_inline uint64_t unsafe_yyjson_get_uint(const void *val) { + return ((const yyjson_val *)val)->uni.u64; } -yyjson_api_inline int64_t unsafe_yyjson_get_sint(void *val) { - return ((yyjson_val *)val)->uni.i64; +yyjson_api_inline int64_t unsafe_yyjson_get_sint(const void *val) { + return ((const yyjson_val *)val)->uni.i64; } -yyjson_api_inline int unsafe_yyjson_get_int(void *val) { - return (int)((yyjson_val *)val)->uni.i64; +yyjson_api_inline int unsafe_yyjson_get_int(const void *val) { + return (int)((const yyjson_val *)val)->uni.i64; } -yyjson_api_inline double unsafe_yyjson_get_real(void *val) { - return ((yyjson_val *)val)->uni.f64; +yyjson_api_inline double unsafe_yyjson_get_real(const void *val) { + return ((const yyjson_val *)val)->uni.f64; } -yyjson_api_inline double unsafe_yyjson_get_num(void *val) { +yyjson_api_inline double unsafe_yyjson_get_num(const void *val) { uint8_t tag = unsafe_yyjson_get_tag(val); if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL)) { - return ((yyjson_val *)val)->uni.f64; + return ((const yyjson_val *)val)->uni.f64; } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT)) { - return (double)((yyjson_val *)val)->uni.i64; + return (double)((const yyjson_val *)val)->uni.i64; } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT)) { - return unsafe_yyjson_u64_to_f64(((yyjson_val *)val)->uni.u64); + return unsafe_yyjson_u64_to_f64(((const yyjson_val *)val)->uni.u64); } return 0.0; } -yyjson_api_inline const char *unsafe_yyjson_get_str(void *val) { - return ((yyjson_val *)val)->uni.str; +yyjson_api_inline const char *unsafe_yyjson_get_str(const void *val) { + return ((const yyjson_val *)val)->uni.str; } -yyjson_api_inline size_t unsafe_yyjson_get_len(void *val) { - return (size_t)(((yyjson_val *)val)->tag >> YYJSON_TAG_BIT); +yyjson_api_inline size_t unsafe_yyjson_get_len(const void *val) { + return (size_t)(((const yyjson_val *)val)->tag >> YYJSON_TAG_BIT); } -yyjson_api_inline yyjson_val *unsafe_yyjson_get_first(yyjson_val *ctn) { - return ctn + 1; +yyjson_api_inline yyjson_val *unsafe_yyjson_get_first(const yyjson_val *ctn) { + return yyjson_constcast(yyjson_val *)ctn + 1; } -yyjson_api_inline yyjson_val *unsafe_yyjson_get_next(yyjson_val *val) { +yyjson_api_inline yyjson_val *unsafe_yyjson_get_next(const yyjson_val *val) { bool is_ctn = unsafe_yyjson_is_ctn(val); size_t ctn_ofs = val->uni.ofs; size_t ofs = (is_ctn ? ctn_ofs : sizeof(yyjson_val)); - return (yyjson_val *)(void *)((uint8_t *)val + ofs); + uint8_t *ptr = yyjson_constcast(uint8_t *)val; + return (yyjson_val *)(void *)(ptr + ofs); } -yyjson_api_inline bool unsafe_yyjson_equals_strn(void *val, const char *str, - size_t len) { +yyjson_api_inline bool unsafe_yyjson_equals_strn(const void *val, + const char *str, size_t len) { return unsafe_yyjson_get_len(val) == len && - memcmp(((yyjson_val *)val)->uni.str, str, len) == 0; + memcmp(((const yyjson_val *)val)->uni.str, str, len) == 0; } -yyjson_api_inline bool unsafe_yyjson_equals_str(void *val, const char *str) { +yyjson_api_inline bool unsafe_yyjson_equals_str(const void *val, + const char *str) { return unsafe_yyjson_equals_strn(val, str, strlen(str)); } @@ -5115,15 +5332,15 @@ yyjson_api_inline void unsafe_yyjson_set_obj(void *val, size_t size) { * MARK: - JSON Document API (Implementation) *============================================================================*/ -yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc) { +yyjson_api_inline yyjson_val *yyjson_doc_get_root(const yyjson_doc *doc) { return doc ? doc->root : NULL; } -yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc) { +yyjson_api_inline size_t yyjson_doc_get_read_size(const yyjson_doc *doc) { return doc ? doc->dat_read : 0; } -yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc) { +yyjson_api_inline size_t yyjson_doc_get_val_count(const yyjson_doc *doc) { return doc ? doc->val_read : 0; } @@ -5142,59 +5359,59 @@ yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc) { * MARK: - JSON Value Type API (Implementation) *============================================================================*/ -yyjson_api_inline bool yyjson_is_raw(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_raw(const yyjson_val *val) { return val ? unsafe_yyjson_is_raw(val) : false; } -yyjson_api_inline bool yyjson_is_null(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_null(const yyjson_val *val) { return val ? unsafe_yyjson_is_null(val) : false; } -yyjson_api_inline bool yyjson_is_true(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_true(const yyjson_val *val) { return val ? unsafe_yyjson_is_true(val) : false; } -yyjson_api_inline bool yyjson_is_false(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_false(const yyjson_val *val) { return val ? unsafe_yyjson_is_false(val) : false; } -yyjson_api_inline bool yyjson_is_bool(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_bool(const yyjson_val *val) { return val ? unsafe_yyjson_is_bool(val) : false; } -yyjson_api_inline bool yyjson_is_uint(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_uint(const yyjson_val *val) { return val ? unsafe_yyjson_is_uint(val) : false; } -yyjson_api_inline bool yyjson_is_sint(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_sint(const yyjson_val *val) { return val ? unsafe_yyjson_is_sint(val) : false; } -yyjson_api_inline bool yyjson_is_int(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_int(const yyjson_val *val) { return val ? unsafe_yyjson_is_int(val) : false; } -yyjson_api_inline bool yyjson_is_real(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_real(const yyjson_val *val) { return val ? unsafe_yyjson_is_real(val) : false; } -yyjson_api_inline bool yyjson_is_num(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_num(const yyjson_val *val) { return val ? unsafe_yyjson_is_num(val) : false; } -yyjson_api_inline bool yyjson_is_str(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_str(const yyjson_val *val) { return val ? unsafe_yyjson_is_str(val) : false; } -yyjson_api_inline bool yyjson_is_arr(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val) { return val ? unsafe_yyjson_is_arr(val) : false; } -yyjson_api_inline bool yyjson_is_obj(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val) { return val ? unsafe_yyjson_is_obj(val) : false; } -yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val) { +yyjson_api_inline bool yyjson_is_ctn(const yyjson_val *val) { return val ? unsafe_yyjson_is_ctn(val) : false; } @@ -5204,19 +5421,19 @@ yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val) { * MARK: - JSON Value Content API (Implementation) *============================================================================*/ -yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val) { +yyjson_api_inline yyjson_type yyjson_get_type(const yyjson_val *val) { return val ? unsafe_yyjson_get_type(val) : YYJSON_TYPE_NONE; } -yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val) { +yyjson_api_inline yyjson_subtype yyjson_get_subtype(const yyjson_val *val) { return val ? unsafe_yyjson_get_subtype(val) : YYJSON_SUBTYPE_NONE; } -yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val) { +yyjson_api_inline uint8_t yyjson_get_tag(const yyjson_val *val) { return val ? unsafe_yyjson_get_tag(val) : 0; } -yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val) { +yyjson_api_inline const char *yyjson_get_type_desc(const yyjson_val *val) { switch (yyjson_get_tag(val)) { case YYJSON_TYPE_RAW | YYJSON_SUBTYPE_NONE: return "raw"; case YYJSON_TYPE_NULL | YYJSON_SUBTYPE_NONE: return "null"; @@ -5233,43 +5450,44 @@ yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val) { } } -yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val) { +yyjson_api_inline const char *yyjson_get_raw(const yyjson_val *val) { return yyjson_is_raw(val) ? unsafe_yyjson_get_raw(val) : NULL; } -yyjson_api_inline bool yyjson_get_bool(yyjson_val *val) { +yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val) { return yyjson_is_bool(val) ? unsafe_yyjson_get_bool(val) : false; } -yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val) { +yyjson_api_inline uint64_t yyjson_get_uint(const yyjson_val *val) { return yyjson_is_int(val) ? unsafe_yyjson_get_uint(val) : 0; } -yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val) { +yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val) { return yyjson_is_int(val) ? unsafe_yyjson_get_sint(val) : 0; } -yyjson_api_inline int yyjson_get_int(yyjson_val *val) { +yyjson_api_inline int yyjson_get_int(const yyjson_val *val) { return yyjson_is_int(val) ? unsafe_yyjson_get_int(val) : 0; } -yyjson_api_inline double yyjson_get_real(yyjson_val *val) { +yyjson_api_inline double yyjson_get_real(const yyjson_val *val) { return yyjson_is_real(val) ? unsafe_yyjson_get_real(val) : 0.0; } -yyjson_api_inline double yyjson_get_num(yyjson_val *val) { +yyjson_api_inline double yyjson_get_num(const yyjson_val *val) { return val ? unsafe_yyjson_get_num(val) : 0.0; } -yyjson_api_inline const char *yyjson_get_str(yyjson_val *val) { +yyjson_api_inline const char *yyjson_get_str(const yyjson_val *val) { return yyjson_is_str(val) ? unsafe_yyjson_get_str(val) : NULL; } -yyjson_api_inline size_t yyjson_get_len(yyjson_val *val) { +yyjson_api_inline size_t yyjson_get_len(const yyjson_val *val) { return val ? unsafe_yyjson_get_len(val) : 0; } -yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str) { +yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, + const char *str) { if (yyjson_likely(val && str)) { return unsafe_yyjson_is_str(val) && unsafe_yyjson_equals_str(val, str); @@ -5277,8 +5495,8 @@ yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str) { return false; } -yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, - size_t len) { +yyjson_api_inline bool yyjson_equals_strn(const yyjson_val *val, + const char *str, size_t len) { if (yyjson_likely(val && str)) { return unsafe_yyjson_is_str(val) && unsafe_yyjson_equals_strn(val, str, len); @@ -5286,9 +5504,11 @@ yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, return false; } -yyjson_api bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); +yyjson_api bool unsafe_yyjson_equals(const yyjson_val *lhs, + const yyjson_val *rhs); -yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { +yyjson_api_inline bool yyjson_equals(const yyjson_val *lhs, + const yyjson_val *rhs) { if (yyjson_unlikely(!lhs || !rhs)) return false; return unsafe_yyjson_equals(lhs, rhs); } @@ -5296,6 +5516,7 @@ yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, const char *raw, size_t len) { if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + if (yyjson_unlikely(!raw)) return false; unsafe_yyjson_set_raw(val, raw, len); return true; } @@ -5324,9 +5545,9 @@ yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num) { return true; } -yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num) { +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int64_t num) { if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; - unsafe_yyjson_set_sint(val, (int64_t)num); + unsafe_yyjson_set_sint(val, num); return true; } @@ -5387,11 +5608,12 @@ yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc) { * MARK: - JSON Array API (Implementation) *============================================================================*/ -yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr) { +yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr) { return yyjson_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; } -yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx) { +yyjson_api_inline yyjson_val *yyjson_arr_get(const yyjson_val *arr, + size_t idx) { if (yyjson_likely(yyjson_is_arr(arr))) { if (yyjson_likely(unsafe_yyjson_get_len(arr) > idx)) { yyjson_val *val = unsafe_yyjson_get_first(arr); @@ -5406,7 +5628,7 @@ yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx) { return NULL; } -yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr) { +yyjson_api_inline yyjson_val *yyjson_arr_get_first(const yyjson_val *arr) { if (yyjson_likely(yyjson_is_arr(arr))) { if (yyjson_likely(unsafe_yyjson_get_len(arr) > 0)) { return unsafe_yyjson_get_first(arr); @@ -5415,7 +5637,7 @@ yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr) { return NULL; } -yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr) { +yyjson_api_inline yyjson_val *yyjson_arr_get_last(const yyjson_val *arr) { if (yyjson_likely(yyjson_is_arr(arr))) { size_t len = unsafe_yyjson_get_len(arr); if (yyjson_likely(len > 0)) { @@ -5437,7 +5659,7 @@ yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr) { * MARK: - JSON Array Iterator API (Implementation) *============================================================================*/ -yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, +yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter) { if (yyjson_likely(yyjson_is_arr(arr) && iter)) { iter->idx = 0; @@ -5449,7 +5671,7 @@ yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, return false; } -yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr) { +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(const yyjson_val *arr) { yyjson_arr_iter iter; yyjson_arr_iter_init(arr, &iter); return iter; @@ -5476,16 +5698,16 @@ yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter) { * MARK: - JSON Object API (Implementation) *============================================================================*/ -yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj) { +yyjson_api_inline size_t yyjson_obj_size(const yyjson_val *obj) { return yyjson_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; } -yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, +yyjson_api_inline yyjson_val *yyjson_obj_get(const yyjson_val *obj, const char *key) { return yyjson_obj_getn(obj, key, key ? strlen(key) : 0); } -yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, +yyjson_api_inline yyjson_val *yyjson_obj_getn(const yyjson_val *obj, const char *_key, size_t key_len) { if (yyjson_likely(yyjson_is_obj(obj) && _key)) { @@ -5505,20 +5727,20 @@ yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, * MARK: - JSON Object Iterator API (Implementation) *============================================================================*/ -yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, +yyjson_api_inline bool yyjson_obj_iter_init(const yyjson_val *obj, yyjson_obj_iter *iter) { if (yyjson_likely(yyjson_is_obj(obj) && iter)) { iter->idx = 0; iter->max = unsafe_yyjson_get_len(obj); iter->cur = unsafe_yyjson_get_first(obj); - iter->obj = obj; + iter->obj = yyjson_constcast(yyjson_val *)obj; return true; } if (iter) memset(iter, 0, sizeof(yyjson_obj_iter)); return false; } -yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj) { +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(const yyjson_val *obj) { yyjson_obj_iter iter; yyjson_obj_iter_init(obj, &iter); return iter; @@ -5584,7 +5806,7 @@ yyjson_api_inline yyjson_val *yyjson_obj_iter_getn(yyjson_obj_iter *iter, /** Mutable JSON value, 24 bytes. - The 'tag' and 'uni' field is same as immutable value. + The 'tag' and 'uni' fields are the same as immutable value. The 'next' field links all elements inside the container to be a cycle. */ struct yyjson_mut_val { @@ -5615,7 +5837,7 @@ typedef struct yyjson_str_pool { /** A memory chunk in value memory pool. - `sizeof(yyjson_val_chunk)` should not larger than `sizeof(yyjson_mut_val)`. + `sizeof(yyjson_val_chunk)` should not be larger than `sizeof(yyjson_mut_val)`. */ typedef struct yyjson_val_chunk { struct yyjson_val_chunk *next; /* next chunk linked list */ @@ -5658,6 +5880,10 @@ yyjson_api_inline char *unsafe_yyjson_mut_str_alc(yyjson_mut_doc *doc, char *mem; const yyjson_alc *alc = &doc->alc; yyjson_str_pool *pool = &doc->str_pool; + /* `len + 1` is used below to reserve space for a null terminator; + reject the value that would wrap it to 0 and produce an under-sized + allocation with an out-of-bounds memcpy at the call sites. */ + if (yyjson_unlikely(len == (size_t)-1)) return NULL; if (yyjson_unlikely((size_t)(pool->end - pool->cur) <= len)) { if (yyjson_unlikely(!unsafe_yyjson_str_pool_grow(pool, alc, len + 1))) { return NULL; @@ -5713,59 +5939,59 @@ yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, * MARK: - Mutable JSON Value Type API (Implementation) *============================================================================*/ -yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_raw(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_raw(val) : false; } -yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_null(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_null(val) : false; } -yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_true(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_true(val) : false; } -yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_false(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_false(val) : false; } -yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_bool(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_bool(val) : false; } -yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_uint(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_uint(val) : false; } -yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_sint(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_sint(val) : false; } -yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_int(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_int(val) : false; } -yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_real(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_real(val) : false; } -yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_num(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_num(val) : false; } -yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_str(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_str(val) : false; } -yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_arr(val) : false; } -yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_obj(val) : false; } -yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val) { +yyjson_api_inline bool yyjson_mut_is_ctn(const yyjson_mut_val *val) { return val ? unsafe_yyjson_is_ctn(val) : false; } @@ -5775,73 +6001,75 @@ yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val) { * MARK: - Mutable JSON Value Content API (Implementation) *============================================================================*/ -yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val) { - return yyjson_get_type((yyjson_val *)val); +yyjson_api_inline yyjson_type yyjson_mut_get_type(const yyjson_mut_val *val) { + return yyjson_get_type((const yyjson_val *)val); } -yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val) { - return yyjson_get_subtype((yyjson_val *)val); +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype( + const yyjson_mut_val *val) { + return yyjson_get_subtype((const yyjson_val *)val); } -yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val) { - return yyjson_get_tag((yyjson_val *)val); +yyjson_api_inline uint8_t yyjson_mut_get_tag(const yyjson_mut_val *val) { + return yyjson_get_tag((const yyjson_val *)val); } -yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val) { - return yyjson_get_type_desc((yyjson_val *)val); +yyjson_api_inline const char *yyjson_mut_get_type_desc( + const yyjson_mut_val *val) { + return yyjson_get_type_desc((const yyjson_val *)val); } -yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val) { - return yyjson_get_raw((yyjson_val *)val); +yyjson_api_inline const char *yyjson_mut_get_raw(const yyjson_mut_val *val) { + return yyjson_get_raw((const yyjson_val *)val); } -yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val) { - return yyjson_get_bool((yyjson_val *)val); +yyjson_api_inline bool yyjson_mut_get_bool(const yyjson_mut_val *val) { + return yyjson_get_bool((const yyjson_val *)val); } -yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val) { - return yyjson_get_uint((yyjson_val *)val); +yyjson_api_inline uint64_t yyjson_mut_get_uint(const yyjson_mut_val *val) { + return yyjson_get_uint((const yyjson_val *)val); } -yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val) { - return yyjson_get_sint((yyjson_val *)val); +yyjson_api_inline int64_t yyjson_mut_get_sint(const yyjson_mut_val *val) { + return yyjson_get_sint((const yyjson_val *)val); } -yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val) { - return yyjson_get_int((yyjson_val *)val); +yyjson_api_inline int yyjson_mut_get_int(const yyjson_mut_val *val) { + return yyjson_get_int((const yyjson_val *)val); } -yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val) { - return yyjson_get_real((yyjson_val *)val); +yyjson_api_inline double yyjson_mut_get_real(const yyjson_mut_val *val) { + return yyjson_get_real((const yyjson_val *)val); } -yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val) { - return yyjson_get_num((yyjson_val *)val); +yyjson_api_inline double yyjson_mut_get_num(const yyjson_mut_val *val) { + return yyjson_get_num((const yyjson_val *)val); } -yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val) { - return yyjson_get_str((yyjson_val *)val); +yyjson_api_inline const char *yyjson_mut_get_str(const yyjson_mut_val *val) { + return yyjson_get_str((const yyjson_val *)val); } -yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val) { - return yyjson_get_len((yyjson_val *)val); +yyjson_api_inline size_t yyjson_mut_get_len(const yyjson_mut_val *val) { + return yyjson_get_len((const yyjson_val *)val); } -yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, +yyjson_api_inline bool yyjson_mut_equals_str(const yyjson_mut_val *val, const char *str) { - return yyjson_equals_str((yyjson_val *)val, str); + return yyjson_equals_str((const yyjson_val *)val, str); } -yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, +yyjson_api_inline bool yyjson_mut_equals_strn(const yyjson_mut_val *val, const char *str, size_t len) { - return yyjson_equals_strn((yyjson_val *)val, str, len); + return yyjson_equals_strn((const yyjson_val *)val, str, len); } -yyjson_api bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, - yyjson_mut_val *rhs); +yyjson_api bool unsafe_yyjson_mut_equals(const yyjson_mut_val *lhs, + const yyjson_mut_val *rhs); -yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, - yyjson_mut_val *rhs) { +yyjson_api_inline bool yyjson_mut_equals(const yyjson_mut_val *lhs, + const yyjson_mut_val *rhs) { if (yyjson_unlikely(!lhs || !rhs)) return false; return unsafe_yyjson_mut_equals(lhs, rhs); } @@ -5877,9 +6105,9 @@ yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num) { return true; } -yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num) { +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int64_t num) { if (yyjson_unlikely(!val)) return false; - unsafe_yyjson_set_sint(val, (int64_t)num); + unsafe_yyjson_set_sint(val, num); return true; } @@ -6095,11 +6323,11 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, * MARK: - Mutable JSON Array API (Implementation) *============================================================================*/ -yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr) { +yyjson_api_inline size_t yyjson_mut_arr_size(const yyjson_mut_val *arr) { return yyjson_mut_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; } -yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(const yyjson_mut_val *arr, size_t idx) { if (yyjson_likely(idx < yyjson_mut_arr_size(arr))) { yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; @@ -6110,7 +6338,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, } yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first( - yyjson_mut_val *arr) { + const yyjson_mut_val *arr) { if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { return ((yyjson_mut_val *)arr->uni.ptr)->next; } @@ -6118,7 +6346,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first( } yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last( - yyjson_mut_val *arr) { + const yyjson_mut_val *arr) { if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { return ((yyjson_mut_val *)arr->uni.ptr); } @@ -6170,7 +6398,8 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( yyjson_mut_arr_iter *iter) { - if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + if (yyjson_likely(iter && iter->pre && + 0 < iter->idx && iter->idx <= iter->max)) { yyjson_mut_val *prev = iter->pre; yyjson_mut_val *cur = iter->cur; yyjson_mut_val *next = cur->next; @@ -6180,6 +6409,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( unsafe_yyjson_set_len(iter->arr, iter->max); prev->next = next; iter->cur = prev; + iter->pre = NULL; return cur; } return NULL; @@ -6539,7 +6769,7 @@ yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, yyjson_mut_val *prev, *next; bool tail_removed; size_t len = unsafe_yyjson_get_len(arr); - if (yyjson_unlikely(_idx + _len > len)) return false; + if (yyjson_unlikely(_len > len || _idx > len - _len)) return false; if (yyjson_unlikely(_len == 0)) return true; unsafe_yyjson_set_len(arr, len - _len); if (yyjson_unlikely(len == _len)) return true; @@ -6747,16 +6977,16 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, * MARK: - Mutable JSON Object API (Implementation) *============================================================================*/ -yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj) { +yyjson_api_inline size_t yyjson_mut_obj_size(const yyjson_mut_val *obj) { return yyjson_mut_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; } -yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key) { return yyjson_mut_obj_getn(obj, key, key ? strlen(key) : 0); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(const yyjson_mut_val *obj, const char *_key, size_t key_len) { size_t len = yyjson_mut_obj_size(obj); @@ -6820,7 +7050,8 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( yyjson_mut_obj_iter *iter) { - if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + if (yyjson_likely(iter && iter->pre && + 0 < iter->idx && iter->idx <= iter->max)) { yyjson_mut_val *prev = iter->pre; yyjson_mut_val *cur = iter->cur; yyjson_mut_val *next = cur->next->next; @@ -6830,6 +7061,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( unsafe_yyjson_set_len(iter->obj, iter->max); prev->next->next = next; iter->cur = prev; + iter->pre = NULL; return cur->next; } return NULL; @@ -6851,7 +7083,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_getn( cur = cur->next->next; if (unsafe_yyjson_equals_strn(cur, key, key_len)) { iter->idx += idx; - if (iter->idx > max) iter->idx -= max + 1; + if (iter->idx > max) iter->idx -= max; iter->pre = pre; iter->cur = cur; return cur->next; @@ -6882,7 +7114,9 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, const char **keys, const char **vals, size_t count) { - if (yyjson_likely(doc && ((count > 0 && keys && vals) || (count == 0)))) { + if (yyjson_likely(doc && ((count > 0 && count < + (~(size_t)0) / sizeof(yyjson_mut_val) / 2 && + keys && vals) || (count == 0)))) { yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); if (yyjson_likely(obj)) { obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; @@ -6891,8 +7125,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, for (i = 0; i < count; i++) { yyjson_mut_val *key = obj + (i * 2 + 1); yyjson_mut_val *val = obj + (i * 2 + 2); - uint64_t key_len = (uint64_t)strlen(keys[i]); - uint64_t val_len = (uint64_t)strlen(vals[i]); + uint64_t key_len, val_len; + if (yyjson_unlikely(!keys[i] || !vals[i])) return NULL; + key_len = (uint64_t)strlen(keys[i]); + val_len = (uint64_t)strlen(vals[i]); key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; key->uni.str = keys[i]; @@ -6912,7 +7148,9 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, const char **pairs, size_t count) { - if (yyjson_likely(doc && ((count > 0 && pairs) || (count == 0)))) { + if (yyjson_likely(doc && ((count > 0 && count < + (~(size_t)0) / sizeof(yyjson_mut_val) / 2 && + pairs) || (count == 0)))) { yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); if (yyjson_likely(obj)) { obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; @@ -6923,8 +7161,10 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, yyjson_mut_val *val = obj + (i * 2 + 2); const char *key_str = pairs[i * 2 + 0]; const char *val_str = pairs[i * 2 + 1]; - uint64_t key_len = (uint64_t)strlen(key_str); - uint64_t val_len = (uint64_t)strlen(val_str); + uint64_t key_len, val_len; + if (yyjson_unlikely(!key_str || !val_str)) return NULL; + key_len = (uint64_t)strlen(key_str); + val_len = (uint64_t)strlen(val_str); key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; key->uni.str = key_str; @@ -7376,12 +7616,12 @@ yyjson_api_inline bool yyjson_mut_obj_rename_keyn(yyjson_mut_doc *doc, } while(false) /* require: val != NULL, *ptr == '/', len > 0 */ -yyjson_api yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, +yyjson_api yyjson_val *unsafe_yyjson_ptr_getx(const yyjson_val *val, const char *ptr, size_t len, yyjson_ptr_err *err); /* require: val != NULL, *ptr == '/', len > 0 */ -yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_getx(yyjson_mut_val *val, +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_getx(const yyjson_mut_val *val, const char *ptr, size_t len, yyjson_ptr_ctx *ctx, @@ -7408,18 +7648,18 @@ yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_removex(yyjson_mut_val *val, yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); -yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(const yyjson_doc *doc, const char *ptr) { if (yyjson_unlikely(!ptr)) return NULL; return yyjson_doc_ptr_getn(doc, ptr, strlen(ptr)); } -yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(const yyjson_doc *doc, const char *ptr, size_t len) { return yyjson_doc_ptr_getx(doc, ptr, len, NULL); } -yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(const yyjson_doc *doc, const char *ptr, size_t len, yyjson_ptr_err *err) { yyjson_ptr_set_err(NONE, NULL); @@ -7441,18 +7681,18 @@ yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, return unsafe_yyjson_ptr_getx(doc->root, ptr, len, err); } -yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_get(const yyjson_val *val, const char *ptr) { if (yyjson_unlikely(!ptr)) return NULL; return yyjson_ptr_getn(val, ptr, strlen(ptr)); } -yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_getn(const yyjson_val *val, const char *ptr, size_t len) { return yyjson_ptr_getx(val, ptr, len, NULL); } -yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, +yyjson_api_inline yyjson_val *yyjson_ptr_getx(const yyjson_val *val, const char *ptr, size_t len, yyjson_ptr_err *err) { yyjson_ptr_set_err(NONE, NULL); @@ -7461,7 +7701,7 @@ yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, return NULL; } if (yyjson_unlikely(len == 0)) { - return val; + return yyjson_constcast(yyjson_val *)val; } if (yyjson_unlikely(*ptr != '/')) { yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); @@ -7470,23 +7710,20 @@ yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, return unsafe_yyjson_ptr_getx(val, ptr, len, err); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, - const char *ptr) { +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get( + const yyjson_mut_doc *doc, const char *ptr) { if (!ptr) return NULL; return yyjson_mut_doc_ptr_getn(doc, ptr, strlen(ptr)); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, - const char *ptr, - size_t len) { +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn( + const yyjson_mut_doc *doc, const char *ptr, size_t len) { return yyjson_mut_doc_ptr_getx(doc, ptr, len, NULL, NULL); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, - const char *ptr, - size_t len, - yyjson_ptr_ctx *ctx, - yyjson_ptr_err *err) { +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx( + const yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { yyjson_ptr_set_err(NONE, NULL); if (ctx) memset(ctx, 0, sizeof(*ctx)); @@ -7508,19 +7745,19 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, return unsafe_yyjson_mut_ptr_getx(doc->root, ptr, len, ctx, err); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(const yyjson_mut_val *val, const char *ptr) { if (!ptr) return NULL; return yyjson_mut_ptr_getn(val, ptr, strlen(ptr)); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(const yyjson_mut_val *val, const char *ptr, size_t len) { return yyjson_mut_ptr_getx(val, ptr, len, NULL, NULL); } -yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(const yyjson_mut_val *val, const char *ptr, size_t len, yyjson_ptr_ctx *ctx, @@ -7533,7 +7770,7 @@ yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, return NULL; } if (yyjson_unlikely(len == 0)) { - return val; + return yyjson_constcast(yyjson_mut_val *)val; } if (yyjson_unlikely(*ptr != '/')) { yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); @@ -8031,7 +8268,7 @@ yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx) { Returns true if value at `ptr` exists and is the correct type, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_bool( - yyjson_val *root, const char *ptr, bool *value) { + const yyjson_val *root, const char *ptr, bool *value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && yyjson_is_bool(val)) { *value = unsafe_yyjson_get_bool(val); @@ -8046,7 +8283,7 @@ yyjson_api_inline bool yyjson_ptr_get_bool( that fits in `uint64_t`. Returns true if successful, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_uint( - yyjson_val *root, const char *ptr, uint64_t *value) { + const yyjson_val *root, const char *ptr, uint64_t *value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && val) { uint64_t ret = val->uni.u64; @@ -8064,7 +8301,7 @@ yyjson_api_inline bool yyjson_ptr_get_uint( that fits in `int64_t`. Returns true if successful, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_sint( - yyjson_val *root, const char *ptr, int64_t *value) { + const yyjson_val *root, const char *ptr, int64_t *value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && val) { int64_t ret = val->uni.i64; @@ -8082,7 +8319,7 @@ yyjson_api_inline bool yyjson_ptr_get_sint( Returns true if value at `ptr` exists and is the correct type, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_real( - yyjson_val *root, const char *ptr, double *value) { + const yyjson_val *root, const char *ptr, double *value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && yyjson_is_real(val)) { *value = unsafe_yyjson_get_real(val); @@ -8098,7 +8335,7 @@ yyjson_api_inline bool yyjson_ptr_get_real( Returns true if value at `ptr` exists and is the correct type, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_num( - yyjson_val *root, const char *ptr, double *value) { + const yyjson_val *root, const char *ptr, double *value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && yyjson_is_num(val)) { *value = unsafe_yyjson_get_num(val); @@ -8113,7 +8350,7 @@ yyjson_api_inline bool yyjson_ptr_get_num( Returns true if value at `ptr` exists and is the correct type, otherwise false. */ yyjson_api_inline bool yyjson_ptr_get_str( - yyjson_val *root, const char *ptr, const char **value) { + const yyjson_val *root, const char *ptr, const char **value) { yyjson_val *val = yyjson_ptr_get(root, ptr); if (value && yyjson_is_str(val)) { *value = unsafe_yyjson_get_str(val); @@ -8215,8 +8452,8 @@ yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_get_pointer( #if defined(__clang__) # pragma clang diagnostic pop -#elif defined(__GNUC__) -# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +#elif YYJSON_IS_REAL_GCC +# if yyjson_gcc_available(4, 6, 0) # pragma GCC diagnostic pop # endif #elif defined(_MSC_VER) From 29818d2dcac9eb353dedbe87d06bdb52af3cd153 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Fri, 11 Sep 2026 10:36:16 +0800 Subject: [PATCH 20/76] =?UTF-8?q?Top=20(macOS):=20improves=20performance?= =?UTF-8?q?=20and=20accuracy=E2=80=8C=20of=20process=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/detection/top/top_apple.c | 69 ++++++++++++++--------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/src/detection/top/top_apple.c b/src/detection/top/top_apple.c index 6a1692b0e8..b37793e867 100644 --- a/src/detection/top/top_apple.c +++ b/src/detection/top/top_apple.c @@ -6,59 +6,46 @@ #include const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { - int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; - size_t length; - - if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { - return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, nullptr}) failed"; + int npids = proc_listallpids(nullptr, 0); + if (npids <= 0) { + return "proc_listallpids(nullptr, 0) failed"; } - - // The process table may change between the two sysctl calls; retry with a larger buffer. - length += length / 8 + sizeof(struct kinfo_proc); - FF_AUTO_FREE struct kinfo_proc* processes = malloc(length); - - if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { - return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL, processes}) failed"; + FF_AUTO_FREE pid_t* pids = malloc((uint32_t) (npids + npids / 8 + 1) * sizeof(pid_t)); + npids = proc_listallpids(pids, npids); + if (npids <= 0) { + return "proc_listallpids(pids, bufferSize) failed"; } - uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc)); + uint32_t count = (uint32_t) npids; for (uint32_t i = 0; i < count; ++i) { - const struct kinfo_proc* proc = &processes[i]; - if (proc->kp_proc.p_flag & P_SYSTEM) { + pid_t pid = pids[i]; + + struct proc_taskallinfo proc; + if (proc_pidinfo(pid, PROC_PIDTASKALLINFO, 0, &proc, sizeof(proc)) != sizeof(proc)) { continue; } - pid_t pid = proc->kp_proc.p_pid; - struct rusage_info_v2 rusage; - if (proc_pid_rusage(pid, RUSAGE_INFO_V2, (rusage_info_t*) &rusage) != 0) { - continue; // The process may have exited + if (proc.pbsd.pbi_flags & PROC_FLAG_SYSTEM) { + continue; } FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); - ffStrbufInitS(&item->name, proc->kp_proc.p_comm); + ffStrbufInitS(&item->name, proc.pbsd.pbi_name); + if (item->name.length == 0) { + ffStrbufInitS(&item->name, proc.pbsd.pbi_comm); + } item->pid = (uint32_t) pid; - // Note: Do NOT use proc->kp_proc.p_pctcpu for CPU usage. p_pctcpu is a - // decaying average (fixpt_t with FSCALE=2048) updated roughly once per - // second by the kernel. It is heavily smoothed, lags short bursts, has - // low resolution, and its multicore scaling (>100%) is inconsistent - // across XNU versions. It also breaks the unified model where - // FFTopProcessSnapshot.cpuTime is cumulative time and top.c computes - // (new - old) / elapsed * 100 for all platforms. proc_pid_rusage with - // RUSAGE_INFO_V2 provides precise cumulative ri_user_time + - // ri_system_time in nanoseconds and is the modern recommended API on - // Darwin, consistent with Linux/BSD differential sampling and accurate - // for the short waitTime interval (e.g. 100ms). - item->cpuTime = (rusage.ri_user_time + rusage.ri_system_time) / 1000000u; // ns -> ms - item->memBytes = rusage.ri_resident_size; - item->bytesRead = rusage.ri_diskio_bytesread; - item->bytesWritten = rusage.ri_diskio_byteswritten; - item->startTime = rusage.ri_proc_start_abstime; - item->threads = 0; - if (showTypes & FF_TOP_TYPE_THREADS) { - struct proc_taskinfo taskInfo; - if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) == sizeof(taskInfo)) { - item->threads = (uint32_t) taskInfo.pti_threadnum; + item->cpuTime = (proc.ptinfo.pti_total_user + proc.ptinfo.pti_total_system) / 1000000u; // ns -> ms + item->memBytes = proc.ptinfo.pti_resident_size; + item->startTime = proc.pbsd.pbi_start_tvsec * 1000u + proc.pbsd.pbi_start_tvusec / 1000u; // convert to ms + item->threads = (uint32_t) proc.ptinfo.pti_threadnum; + + if (showTypes & FF_TOP_TYPE_DISK) { + struct rusage_info_v2 rusage; + if (proc_pid_rusage(pid, RUSAGE_INFO_V2, (rusage_info_t*) &rusage) == 0) { + item->bytesRead = rusage.ri_diskio_bytesread; + item->bytesWritten = rusage.ri_diskio_byteswritten; } } } From 9a9f82b59efaf98073d450c71093a4492fc7be48 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Fri, 11 Sep 2026 10:40:44 +0800 Subject: [PATCH 21/76] Processes (macOS): adds a comment --- src/detection/processes/processes_apple.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/detection/processes/processes_apple.c b/src/detection/processes/processes_apple.c index 719b1b1da8..727aeac02d 100644 --- a/src/detection/processes/processes_apple.c +++ b/src/detection/processes/processes_apple.c @@ -31,10 +31,10 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu pid_t pid = proc->kp_proc.p_pid; struct proc_taskinfo taskInfo; - if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) != sizeof(taskInfo)) { - continue; + if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) == sizeof(taskInfo)) { + // NOTE: This fails for system processes + result->threads += (uint32_t) taskInfo.pti_threadnum; } - result->threads += (uint32_t) taskInfo.pti_threadnum; } return nullptr; From f37a5a2543e4c7227f2f238cafdaf45cb657c552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 11 Sep 2026 23:38:10 +0800 Subject: [PATCH 22/76] OpenBSD: removes kvm dep --- CMakeLists.txt | 1 - src/common/impl/FFPlatform_unix.c | 63 ++++++++++++++---------- src/common/impl/processing_linux.c | 35 ++++++------- src/detection/displayserver/linux/wmde.c | 39 +++++++++------ src/detection/lm/lm_linux.c | 33 ++++++++----- src/detection/processes/processes_obsd.c | 24 ++++----- src/detection/top/top_obsd.c | 45 +++++++++-------- 7 files changed, 132 insertions(+), 108 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d52ecd8e4..3fa14120b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1930,7 +1930,6 @@ elseif(FreeBSD) elseif(OpenBSD) target_link_libraries(libfastfetch PRIVATE "m" - PRIVATE "kvm" PRIVATE "sndio" PRIVATE "util" ) diff --git a/src/common/impl/FFPlatform_unix.c b/src/common/impl/FFPlatform_unix.c index 92f0e86349..2c0ab9db34 100644 --- a/src/common/impl/FFPlatform_unix.c +++ b/src/common/impl/FFPlatform_unix.c @@ -4,6 +4,7 @@ #include "common/strutil.h" #include "common/io.h" #include "common/path.h" +#include "common/mallocHelper.h" #include #include @@ -19,7 +20,6 @@ #elif defined(__OpenBSD__) #include #include - #include #include "common/path.h" #elif defined(__HAIKU__) #include @@ -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,38 @@ 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 ~10% + FF_AUTO_FREE struct kinfo_file* kf = (struct kinfo_file*) malloc(fileSize); + if (kf) { + int rv; + do { + fileMib[5] = (int) (fileSize / sizeof(struct kinfo_file)); + rv = sysctl(fileMib, ARRAY_SIZE(fileMib), kf, &fileSize, nullptr, 0); + } while (rv == -1 && errno == ENOMEM); + + 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; } - break; } + // 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 +138,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); diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c index e43aff2b9f..5783f880fc 100644 --- a/src/common/impl/processing_linux.c +++ b/src/common/impl/processing_linux.c @@ -28,7 +28,6 @@ #elif defined(__OpenBSD__) #include #include - #include #elif defined(__NetBSD__) #include #include @@ -423,12 +422,13 @@ void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, cons #elif defined(__OpenBSD__) - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - int count = 0; - const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count); - if (proc) { - char** argv = kvm_getargv(kd, proc, 0); - if (argv) { + char argvBuf[ARG_MAX]; + size_t argvSize = sizeof(argvBuf); + int argvMib[] = { CTL_KERN, KERN_PROC_ARGS, 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) { const char* arg0 = argv[0]; if (arg0[0] == '-') { arg0++; @@ -436,7 +436,6 @@ void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, cons ffStrbufSetS(exe, arg0); } } - kvm_close(kd); #elif defined(__HAIKU__) @@ -633,21 +632,19 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i #elif defined(__OpenBSD__) - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - int count = 0; - const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count); - if (proc) { - ffStrbufSetS(name, proc->p_comm); + struct kinfo_proc proc; + size_t size = sizeof(proc); + int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, (int) sizeof(struct kinfo_proc), 1 }; + if (sysctl(mib, ARRAY_SIZE(mib), &proc, &size, nullptr, 0) == 0) { + ffStrbufSetS(name, proc.p_comm); if (ppid) { - *ppid = proc->p_ppid; + *ppid = proc.p_ppid; } if (tty) { - *tty = (int) proc->p_tdev; + *tty = (int) proc.p_tdev; } - } - kvm_close(kd); - if (!proc) { - return "kvm_getprocs() failed"; + } else { + return "sysctl(KERN_PROC_PID) failed"; } #elif defined(__HAIKU__) diff --git a/src/detection/displayserver/linux/wmde.c b/src/detection/displayserver/linux/wmde.c index 6984d1d0c3..984bdbeda4 100644 --- a/src/detection/displayserver/linux/wmde.c +++ b/src/detection/displayserver/linux/wmde.c @@ -16,7 +16,6 @@ #elif __OpenBSD__ #include #include - #include #elif __sun #include #elif __NetBSD__ @@ -311,25 +310,33 @@ static const char* getFromProcesses(FFDisplayServerResult* result) { } } #elif __OpenBSD__ - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - int count = 0; - const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_UID, (int) userId, sizeof(*proc), &count); - if (proc) { - for (int i = 0; i < count; ++i) { - if (result->dePrettyName.length == 0) { - applyPrettyNameIfDE(result, proc[i].p_comm); - } + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_UID, (int) userId, (int) sizeof(struct kinfo_proc), 0 }; + size_t length = 0; - if (result->wmPrettyName.length == 0) { - applyNameIfWM(result, proc[i].p_comm); - } + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed"; + } - if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { - break; - } + FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); + request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch + if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed"; + } + + int count = (int) (length / sizeof(struct kinfo_proc)); + for (int i = 0; i < count; ++i) { + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, procs[i].p_comm); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, procs[i].p_comm); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; } } - kvm_close(kd); #elif __sun FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); if (procdir == nullptr) { diff --git a/src/detection/lm/lm_linux.c b/src/detection/lm/lm_linux.c index e4f18c19e0..2abfab97a8 100644 --- a/src/detection/lm/lm_linux.c +++ b/src/detection/lm/lm_linux.c @@ -18,7 +18,6 @@ #elif __OpenBSD__ #include #include - #include #elif __sun #include #elif __NetBSD__ @@ -55,7 +54,7 @@ static const char* getSshdVersion(FFstrbuf* version) { #ifdef FF_HAVE_ZLIB #include "common/library.h" #include "common/path.h" - + #include #include @@ -274,19 +273,27 @@ const char* detectByProcesses(FFLMResult* result) { } } #elif __OpenBSD__ - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - int count = 0; - const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_UID, 0, sizeof(*proc), &count); - if (proc) { - for (int i = 0; i < count; ++i) { - const char* lm = testLms(proc[i].p_comm); - if (lm) { - ffStrbufSetStatic(&result->service, lm); - break; - } + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_UID, 0, (int) sizeof(struct kinfo_proc), 0 }; + size_t length = 0; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed"; + } + + FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); + request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch + if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed"; + } + + int count = (int) (length / sizeof(struct kinfo_proc)); + for (int i = 0; i < count; ++i) { + const char* lm = testLms(procs[i].p_comm); + if (lm) { + ffStrbufSetStatic(&result->service, lm); + break; } } - kvm_close(kd); #elif __sun FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); if (procdir == nullptr) { diff --git a/src/detection/processes/processes_obsd.c b/src/detection/processes/processes_obsd.c index 8c50065a07..e07d45b1b6 100644 --- a/src/detection/processes/processes_obsd.c +++ b/src/detection/processes/processes_obsd.c @@ -4,25 +4,22 @@ #include #include -#include const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResult* result) { - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - if (!kd) { - return "kvm_open() failed"; + int request[] = { CTL_KERN, KERN_PROC, (options->countKprocs ? KERN_PROC_KTHREAD : KERN_PROC_ALL) | KERN_PROC_SHOW_THREADS, 0, (int) sizeof(struct kinfo_proc), 0 }; + size_t length = 0; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed"; } - int count = 0; - // KERN_PROC_ALL returns all user-level processes - // KERN_PROC_KTHREAD returns all processes, including user-level processes (despite the name) - const struct kinfo_proc* procs = kvm_getprocs(kd, - (options->countKprocs ? KERN_PROC_KTHREAD : KERN_PROC_ALL) | KERN_PROC_SHOW_THREADS, - 0, sizeof(struct kinfo_proc), &count); - if (!procs) { - kvm_close(kd); - return "kvm_getprocs() failed"; + FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); + request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch + if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, procs) failed"; } + int count = (int) (length / sizeof(struct kinfo_proc)); for (int i = 0; i < count; ++i) { const struct kinfo_proc* proc = &procs[i]; @@ -32,6 +29,5 @@ const char* ffDetectProcesses(const FFProcessesOptions* options, FFProcessesResu } } - kvm_close(kd); return nullptr; } diff --git a/src/detection/top/top_obsd.c b/src/detection/top/top_obsd.c index 365ecfe7f3..6c56ea5494 100644 --- a/src/detection/top/top_obsd.c +++ b/src/detection/top/top_obsd.c @@ -1,24 +1,26 @@ #include "top.h" +#include "common/mallocHelper.h" + #include #include // DEV_BSIZE #include -#include const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { - kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); - if (!kd) { - return "kvm_open() failed"; + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0, (int) sizeof(struct kinfo_proc), 0 }; + size_t length = 0; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, nullptr) failed"; } - int count = 0; - // KERN_PROC_ALL returns all user-level processes, excluding kernel processes and threads - const struct kinfo_proc* processes = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &count); - if (!processes) { - kvm_close(kd); - return "kvm_getprocs() failed"; + FF_AUTO_FREE struct kinfo_proc* processes = (struct kinfo_proc*) malloc(length); + request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch + if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}, processes) failed"; } + int count = (int) (length / sizeof(struct kinfo_proc)); const uint32_t pageSize = instance.state.platform.sysinfo.pageSize; for (int i = 0; i < count; ++i) { @@ -41,21 +43,24 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { } if (showTypes & FF_TOP_TYPE_THREADS) { - int threadCount = 0; - const struct kinfo_proc* threads = kvm_getprocs(kd, - KERN_PROC_ALL | KERN_PROC_SHOW_THREADS, 0, sizeof(struct kinfo_proc), &threadCount); - if (threads) { - for (uint32_t i = 0; i < snapshots->length; ++i) { - FFTopProcessSnapshot* item = FF_LIST_GET(FFTopProcessSnapshot, *snapshots, i); - for (int j = 0; j < threadCount; ++j) { - if (threads[j].p_pid == (pid_t) item->pid && threads[j].p_tid != -1) { - ++item->threads; + int threadRequest[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL | KERN_PROC_SHOW_THREADS, 0, (int) sizeof(struct kinfo_proc), 0 }; + size_t threadLength = 0; + if (sysctl(threadRequest, ARRAY_SIZE(threadRequest), nullptr, &threadLength, nullptr, 0) == 0 && threadLength > 0) { + FF_AUTO_FREE struct kinfo_proc* threads = (struct kinfo_proc*) malloc(threadLength); + threadRequest[5] = (int) (threadLength / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch + if (sysctl(threadRequest, ARRAY_SIZE(threadRequest), threads, &threadLength, nullptr, 0) == 0) { + int threadCount = (int) (threadLength / sizeof(struct kinfo_proc)); + for (uint32_t i = 0; i < snapshots->length; ++i) { + FFTopProcessSnapshot* item = FF_LIST_GET(FFTopProcessSnapshot, *snapshots, i); + for (int j = 0; j < threadCount; ++j) { + if (threads[j].p_pid == (pid_t) item->pid && threads[j].p_tid != -1) { + ++item->threads; + } } } } } } - kvm_close(kd); return nullptr; } From eff851453678b352b4fbe97e4f293e77ac218cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 11 Sep 2026 23:47:05 +0800 Subject: [PATCH 23/76] CI (macOS): builds Intel version with macport --- .github/workflows/build-macos-hosts.yml | 21 +++++++++++++++++++-- CMakeLists.txt | 7 ++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-macos-hosts.yml b/.github/workflows/build-macos-hosts.yml index d805bf70dd..a145ac8dc2 100644 --- a/.github/workflows/build-macos-hosts.yml +++ b/.github/workflows/build-macos-hosts.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fa14120b8..685f2f6e62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1458,10 +1458,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) From 61e3f6108cecb15c40864b9014ec871cdf185a75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:38:17 +0800 Subject: [PATCH 24/76] CI: Bump the github-actions group with 3 updates (#2569) Bumps the github-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [vmactions/solaris-vm](https://github.com/vmactions/solaris-vm). Updates `github/codeql-action/init` from 4.37.8 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/analyze` from 4.37.8 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `vmactions/solaris-vm` from 1.3.8 to 1.3.9 - [Release notes](https://github.com/vmactions/solaris-vm/releases) - [Commits](https://github.com/vmactions/solaris-vm/compare/315163f088b66e55bbcc45928bd224d4973b2312...96d8d976f9e67d82ec6c7e8ce9c1060731f9e21c) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: vmactions/solaris-vm dependency-version: 1.3.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-linux-hosts.yml | 4 ++-- .github/workflows/build-solaris-amd64.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux-hosts.yml b/.github/workflows/build-linux-hosts.yml index 51fe44c27a..fafaef28c4 100644 --- a/.github/workflows/build-linux-hosts.yml +++ b/.github/workflows/build-linux-hosts.yml @@ -52,7 +52,7 @@ jobs: - name: Initialize CodeQL if: inputs.arch == 'amd64' - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: c @@ -64,7 +64,7 @@ jobs: - name: perform CodeQL analysis if: inputs.arch == 'amd64' - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 - name: list features run: ./fastfetch --list-features diff --git a/.github/workflows/build-solaris-amd64.yml b/.github/workflows/build-solaris-amd64.yml index c8e6256201..eb964065a8 100644 --- a/.github/workflows/build-solaris-amd64.yml +++ b/.github/workflows/build-solaris-amd64.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: run VM - uses: vmactions/solaris-vm@315163f088b66e55bbcc45928bd224d4973b2312 # v1.3.8 + uses: vmactions/solaris-vm@96d8d976f9e67d82ec6c7e8ce9c1060731f9e21c # v1.3.9 with: usesh: true envs: 'CMAKE_BUILD_TYPE' From 20c155e5b4d4b039e56267a80a2ca6fa67d04b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sun, 13 Sep 2026 01:06:05 +0800 Subject: [PATCH 25/76] TerminalFont (Windows): honors JSON fragment Fixes #2573 --- .../terminalfont/terminalfont_windows.c | 227 +++++++++++++++--- 1 file changed, 187 insertions(+), 40 deletions(-) diff --git a/src/detection/terminalfont/terminalfont_windows.c b/src/detection/terminalfont/terminalfont_windows.c index c03fea1b61..4fde0b6c23 100644 --- a/src/detection/terminalfont/terminalfont_windows.c +++ b/src/detection/terminalfont/terminalfont_windows.c @@ -13,26 +13,90 @@ #include #include -static const char* detectWTProfile(yyjson_val* profile, FFstrbuf* name, double* size) { +// Windows Terminal resolves every setting through an inheritance chain. A single property, +// such as the font face or the font size, is resolved from the most to the least important +// source: +// 4. the active profile itself (settings.json -> profiles.list) +// 3. profiles.defaults (settings.json) +// 2. a JSON fragment updating the profile with "updates" +// 1. a JSON fragment defining the profile with "guid" +// 0. not set -> the builtin default reported below +// https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalSettingsModel/IInheritable.h +// https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +enum { + WT_FONT_PRIORITY_FRAGMENT = 1, + WT_FONT_PRIORITY_FRAGMENT_UPDATES = 2, + WT_FONT_PRIORITY_DEFAULTS = 3, + WT_FONT_PRIORITY_PROFILE = 4, +}; + +typedef struct FFTerminalFontWT { + FFstrbuf name; + double size; + uint8_t namePriority; // 0 if unset + uint8_t sizePriority; // 0 if unset +} FFTerminalFontWT; + +static inline void wrapWTFontFree(FFTerminalFontWT* result) { + assert(result); + ffStrbufDestroy(&result->name); +} + +static FFTerminalFontWT ffTerminalFontWTCreate(void) { + FFTerminalFontWT result = { + .name = ffStrbufCreate(), + .size = -1, + }; + return result; +} + +static void applyWTProfile(yyjson_val* profile, uint8_t priority, FFTerminalFontWT* result) { yyjson_val* font = yyjson_obj_get(profile, "font"); - if (!font) { - return "yyjson_obj_get(profile, \"font\"); failed"; + if (!yyjson_is_obj(font)) { + return; } - if (!yyjson_is_obj(font)) { - return "yyjson_is_obj(font) returns false"; + if (result->namePriority < priority) { + yyjson_val* face = yyjson_obj_get(font, "face"); + if (yyjson_is_str(face)) { + ffStrbufClear(&result->name); + ffStrbufAppendJsonVal(&result->name, face); + if (result->name.length > 0) { // an empty face is treated as unset + result->namePriority = priority; + } + } + } + + if (result->sizePriority < priority) { + yyjson_val* size = yyjson_obj_get(font, "size"); + if (yyjson_is_num(size)) { + result->size = unsafe_yyjson_get_num(size); + result->sizePriority = priority; + } } +} - if (name->length == 0) { - ffStrbufAppendJsonVal(name, yyjson_obj_get(font, "face")); +// Finds the profile matching `wtProfileId` in a `profiles` array. +// A fragment either defines a profile with "guid" or updates an existing one with "updates", +// the latter being more important, so it is looked up first. +// Note that "guid" and "updates" may be missing: yyjson_get_str() returns nullptr then. +static yyjson_val* findWTProfileInArray(yyjson_val* profiles, const FFstrbuf* wtProfileId, bool* fromUpdates) { + if (!yyjson_is_arr(profiles)) { + return nullptr; } - if (*size < 0) { - yyjson_val* psize = yyjson_obj_get(font, "size"); - if (yyjson_is_num(psize)) { - *size = unsafe_yyjson_get_num(psize); + for (uint8_t pass = 0; pass < 2; ++pass) { + yyjson_val* profile; + size_t idx, max; + yyjson_arr_foreach (profiles, idx, max, profile) { + const char* id = yyjson_get_str(yyjson_obj_get(profile, pass == 0 ? "updates" : "guid")); + if (id && ffStrbufEqualS(wtProfileId, id)) { + *fromUpdates = pass == 0; + return profile; + } } } + return nullptr; } @@ -43,7 +107,7 @@ static inline void wrapYyjsonFree(yyjson_doc** doc) { } } -static const char* detectFromWTImpl(FFstrbuf* content, FFstrbuf* name, double* size) { +static const char* detectFromWTSettings(FFstrbuf* content, const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(content->chars, content->length, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, nullptr, nullptr); if (!doc) { return "Failed to parse WT JSON config file"; @@ -57,36 +121,104 @@ static const char* detectFromWTImpl(FFstrbuf* content, FFstrbuf* name, double* s return "yyjson_obj_get(root, \"profiles\") failed"; } - FF_STRBUF_AUTO_DESTROY wtProfileId = ffStrbufCreateS(getenv("WT_PROFILE_ID")); - ffStrbufTrim(&wtProfileId, '\''); - if (wtProfileId.length > 0) { - yyjson_val* list = yyjson_obj_get(profiles, "list"); - if (yyjson_is_arr(list)) { - yyjson_val* profile; - size_t idx, max; - yyjson_arr_foreach (list, idx, max, profile) { - yyjson_val* guid = yyjson_obj_get(profile, "guid"); - - if (ffStrbufEqualS(&wtProfileId, yyjson_get_str(guid))) { - detectWTProfile(profile, name, size); - break; - } - } + if (wtProfileId->length > 0) { + bool fromUpdates = false; + yyjson_val* profile = findWTProfileInArray(yyjson_obj_get(profiles, "list"), wtProfileId, &fromUpdates); + if (profile) { + applyWTProfile(profile, WT_FONT_PRIORITY_PROFILE, result); } } yyjson_val* defaults = yyjson_obj_get(profiles, "defaults"); if (defaults) { - detectWTProfile(defaults, name, size); + applyWTProfile(defaults, WT_FONT_PRIORITY_DEFAULTS, result); } - if (name->length == 0) { - ffStrbufSetS(name, "Cascadia Mono"); + return nullptr; +} + +// Windows Terminal reads fragment files from a two level directory layout and doesn't recurse: +// \Microsoft\Windows Terminal\Fragments\\.json +// https://learn.microsoft.com/en-us/windows/terminal/json-fragment-extensions#where-to-place-the-json-fragment-files +static void applyWTFragmentFile(const char* path, const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffReadFileBuffer(path, &content)) { + return; } - if (*size < 0) { - *size = 12; + + // Fragments are optional: an unreadable or malformed one must not fail font detection + [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(content.chars, content.length, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, nullptr, nullptr); + if (!doc) { + return; + } + + yyjson_val* const root = yyjson_doc_get_root(doc); + assert(root); + + bool fromUpdates = false; + yyjson_val* profile = findWTProfileInArray(yyjson_obj_get(root, "profiles"), wtProfileId, &fromUpdates); + if (profile) { + applyWTProfile(profile, fromUpdates ? WT_FONT_PRIORITY_FRAGMENT_UPDATES : WT_FONT_PRIORITY_FRAGMENT, result); + } +} + +static void detectWTProfileFromFragmentsIn(const FFstrbuf* fragmentDir, const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(fragmentDir); + const uint32_t baseLength = path.length; + + ffStrbufAppendC(&path, '*'); + WIN32_FIND_DATAA entry; + FF_AUTO_CLOSE_DIR HANDLE hFind = FindFirstFileA(path.chars, &entry); + if (hFind == INVALID_HANDLE_VALUE) { + return; + } + + do { + if (!(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || entry.cFileName[0] == '.') { + continue; + } + + ffStrbufSubstrBefore(&path, baseLength); + ffStrbufAppendS(&path, entry.cFileName); + ffStrbufAppendC(&path, '\\'); + const uint32_t appLength = path.length; + + ffStrbufAppendS(&path, "*.json"); + WIN32_FIND_DATAA fileEntry; + FF_AUTO_CLOSE_DIR HANDLE hFile = FindFirstFileA(path.chars, &fileEntry); + if (hFile != INVALID_HANDLE_VALUE) { + do { + if (fileEntry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + continue; + } + + ffStrbufSubstrBefore(&path, appLength); + ffStrbufAppendS(&path, fileEntry.cFileName); + applyWTFragmentFile(path.chars, wtProfileId, result); + } while (FindNextFileA(hFile, &fileEntry)); + } + + ffStrbufSubstrBefore(&path, baseLength); + } while (FindNextFileA(hFind, &entry)); +} + +static void detectFromWTFragments(const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { + // Windows Terminal merges the user scoped fragments before the machine scoped ones, + // so the user scoped fragments are more important + static const KNOWNFOLDERID* const fragmentFolderIds[] = { &FOLDERID_LocalAppData, &FOLDERID_ProgramData }; + + for (uint32_t i = 0; i < ARRAY_SIZE(fragmentFolderIds); ++i) { + PWSTR folderW = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(fragmentFolderIds[i], KF_FLAG_DEFAULT, nullptr, &folderW))) { + FF_STRBUF_AUTO_DESTROY fragmentDir = ffStrbufCreateWS(folderW); + CoTaskMemFree(folderW); + ffStrbufAppendS(&fragmentDir, "\\Microsoft\\Windows Terminal\\Fragments\\"); + + if (ffPathExists(fragmentDir.chars, FF_PATHTYPE_DIRECTORY)) { + detectWTProfileFromFragmentsIn(&fragmentDir, wtProfileId, result); + } + } } - return nullptr; } static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFontResult* terminalFont) { @@ -159,17 +291,32 @@ static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFon return; } - FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); - double size = -1; - error = detectFromWTImpl(&json, &name, &size); + FF_STRBUF_AUTO_DESTROY wtProfileId = ffStrbufCreateS(getenv("WT_PROFILE_ID")); + ffStrbufTrim(&wtProfileId, '\''); + [[gnu::cleanup(wrapWTFontFree)]] FFTerminalFontWT result = ffTerminalFontWTCreate(); + + error = detectFromWTSettings(&json, &wtProfileId, &result); if (error) { ffStrbufAppendS(&terminalFont->error, error); - } else { - char sizeStr[16]; - snprintf(sizeStr, ARRAY_SIZE(sizeStr), "%g", size); - ffFontInitValues(&terminalFont->font, name.chars, sizeStr); + return; + } + + // JSON fragments are only read when settings.json doesn't fully specify the font + if (wtProfileId.length > 0 && (result.name.length == 0 || result.size < 0)) { + detectFromWTFragments(&wtProfileId, &result); } + + if (result.name.length == 0) { + ffStrbufAppendS(&result.name, "Cascadia Mono"); + } + if (result.size < 0) { + result.size = 12; + } + + char sizeStr[16]; + snprintf(sizeStr, ARRAY_SIZE(sizeStr), "%g", result.size); + ffFontInitValues(&terminalFont->font, result.name.chars, sizeStr); } static void detectMintty(FFTerminalFontResult* terminalFont) { From 19725cd2f74e972fcf0485b0df83d09664f64f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sun, 13 Sep 2026 08:34:39 +0800 Subject: [PATCH 26/76] Packages (Windows): adds `--source winget` to improve performance of winget package detection --- src/detection/packages/packages_windows.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/detection/packages/packages_windows.c b/src/detection/packages/packages_windows.c index b4c3883a52..52100f8fe0 100644 --- a/src/detection/packages/packages_windows.c +++ b/src/detection/packages/packages_windows.c @@ -157,10 +157,25 @@ static void detectPacman(FFPackagesResult* result) { } static void detectWinget(FFPackagesResult* result) { + // Why not read winget's own database instead of shelling out? + // `%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\\installed.db` + // is a `PackageTrackingCatalog`: it records the install / uninstall actions winget performed for that + // source, not a snapshot of what is currently installed. Uninstalling with anything but winget leaves + // the record behind forever, so counting it also counts packages that are long gone. Its schema is + // private and versioned (1.3, while the source index is 2.0), winget opens it ReadWrite while running, + // and reading it would drag in a SQLite dependency. + // The authoritative set of installed packages comes from the ARP registry and MSIX, which is exactly + // what `winget list` enumerates before correlating it against the read-only source index. FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); if (ffProcessAppendStdOut(&buffer, (char*[]) { "winget.exe", "list", + // Without `--source winget`, winget also lists every package installed by + // other means (ARP / MSIX), which are not winget packages at all. + // It also skips the msstore HTTP round-trips, which are the main reason + // why `winget list` is slow and its latency unpredictable. + "--source", + "winget", "--disable-interactivity", nullptr, })) { From ec8b9be354a3ba63b5531fa32f63236b2a0edb38 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sun, 13 Sep 2026 10:57:04 +0800 Subject: [PATCH 27/76] Packages: opens SQLite DB in immutable mode .. to avoid `SQLITE_READONLY` errors --- src/common/impl/settings.c | 98 ++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/src/common/impl/settings.c b/src/common/impl/settings.c index dcbd8893e2..0f45e51b3b 100644 --- a/src/common/impl/settings.c +++ b/src/common/impl/settings.c @@ -3,6 +3,7 @@ #include "common/library.h" #include "common/thread.h" #include "common/io.h" +#include "common/debug.h" #include @@ -380,72 +381,85 @@ static const SQLiteData* getSQLiteData(void) { return &data; } -int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) { - if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) { - return 0; - } - - const SQLiteData* data = getSQLiteData(); - if (data == nullptr) { - return 0; - } - +typedef struct FFSQLite3Bundle { + const SQLiteData* data; sqlite3* db; - if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX, nullptr) != SQLITE_OK) { - return 0; - } - sqlite3_stmt* stmt; - if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, nullptr) != SQLITE_OK) { - data->ffsqlite3_close(db); - return 0; +} FFSQLite3Bundle; +static void destroySQLite3Bundle(FFSQLite3Bundle* bundle) { + if (!bundle->data) { + return; } - - if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) { - data->ffsqlite3_finalize(stmt); - data->ffsqlite3_close(db); - return 0; + if (bundle->stmt) { + bundle->data->ffsqlite3_finalize(bundle->stmt); + bundle->stmt = nullptr; + } + if (bundle->db) { + bundle->data->ffsqlite3_close(bundle->db); + bundle->db = nullptr; } - - int result = data->ffsqlite3_column_int(stmt, 0); - - data->ffsqlite3_finalize(stmt); - data->ffsqlite3_close(db); - - return result; } -bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) { +static bool prepareSQLite3Statement(const char* dbPath, const char* query, FFSQLite3Bundle* bundle) { if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) { return false; } - const SQLiteData* data = getSQLiteData(); + const SQLiteData* data = bundle->data = getSQLiteData(); if (data == nullptr) { + FF_DEBUG("Failed to load SQLite3 library"); return false; } - sqlite3* db; - if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { + // Read-only WAL databases may require a -shm or lock file during prepare, which is not + // possible when the database directory is not writable. immutable=1 avoids those files. + FF_STRBUF_AUTO_DESTROY pathImmutable = ffStrbufCreateStatic("file:"); + ffStrbufAppendS(&pathImmutable, dbPath); + ffStrbufAppendS(&pathImmutable, "?immutable=1"); + + int ret = data->ffsqlite3_open_v2(pathImmutable.chars, &bundle->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_URI, nullptr); + if (ret != SQLITE_OK) { + FF_DEBUG("Failed to open SQLite3 database: %s (%d)", dbPath, ret); return false; } - sqlite3_stmt* stmt; - if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, nullptr) != SQLITE_OK) { - data->ffsqlite3_close(db); + ret = data->ffsqlite3_prepare_v2(bundle->db, query, (int) strlen(query), &bundle->stmt, nullptr); + if (ret != SQLITE_OK) { + FF_DEBUG("Failed to prepare SQLite3 statement: %s (%d)", query, ret); + return false; + } + + ret = data->ffsqlite3_step(bundle->stmt); + if (ret != SQLITE_ROW) { + FF_DEBUG("Failed to step SQLite3 statement: %s (%d)", query, ret); return false; } - if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) { - data->ffsqlite3_finalize(stmt); - data->ffsqlite3_close(db); + ret = data->ffsqlite3_data_count(bundle->stmt); + if (ret < 1) { + FF_DEBUG("No data returned for SQLite3 statement: %s", query); return false; } - ffStrbufSetS(result, (const char*) data->ffsqlite3_column_text(stmt, 0)); + return true; +} + +int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) { + [[gnu::cleanup(destroySQLite3Bundle)]] FFSQLite3Bundle bundle = {}; + if (!prepareSQLite3Statement(dbPath, query, &bundle)) { + return 0; + } + + return bundle.data->ffsqlite3_column_int(bundle.stmt, 0); +} + +bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) { + [[gnu::cleanup(destroySQLite3Bundle)]] FFSQLite3Bundle bundle = {}; + if (!prepareSQLite3Statement(dbPath, query, &bundle)) { + return false; + } - data->ffsqlite3_finalize(stmt); - data->ffsqlite3_close(db); + ffStrbufSetS(result, (const char*) bundle.data->ffsqlite3_column_text(bundle.stmt, 0)); return true; } From ced49ab2a4928edb1ece47bd0cff7493377a5ff7 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sun, 13 Sep 2026 12:05:07 +0800 Subject: [PATCH 28/76] Chore: dont let clang-format break my code style --- src/common/impl/option.c | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/common/impl/option.c b/src/common/impl/option.c index 87b004aea2..1db0438148 100644 --- a/src/common/impl/option.c +++ b/src/common/impl/option.c @@ -131,11 +131,43 @@ void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer) { } if (ffCharIsEnglishAlphabet(value[0])) { + // clang-format off FF_APPEND_COLOR_CODE_COND(reset_, FF_COLOR_MODE_RESET) - else FF_APPEND_COLOR_CODE_COND(bold_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(bright_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(dim_, FF_COLOR_MODE_DIM) else FF_APPEND_COLOR_CODE_COND(italic_, FF_COLOR_MODE_ITALIC) else FF_APPEND_COLOR_CODE_COND(underline_, FF_COLOR_MODE_UNDERLINE) else FF_APPEND_COLOR_CODE_COND(blink_, FF_COLOR_MODE_BLINK) else FF_APPEND_COLOR_CODE_COND(inverse_, FF_COLOR_MODE_INVERSE) else FF_APPEND_COLOR_CODE_COND(hidden_, FF_COLOR_MODE_HIDDEN) else FF_APPEND_COLOR_CODE_COND(strike_, FF_COLOR_MODE_STRIKETHROUGH) else FF_APPEND_COLOR_CODE_COND(black, FF_COLOR_FG_BLACK) else FF_APPEND_COLOR_CODE_COND(red, FF_COLOR_FG_RED) else FF_APPEND_COLOR_CODE_COND(green, FF_COLOR_FG_GREEN) else FF_APPEND_COLOR_CODE_COND(yellow, FF_COLOR_FG_YELLOW) else FF_APPEND_COLOR_CODE_COND(blue, FF_COLOR_FG_BLUE) else FF_APPEND_COLOR_CODE_COND(magenta, FF_COLOR_FG_MAGENTA) else FF_APPEND_COLOR_CODE_COND(cyan, FF_COLOR_FG_CYAN) else FF_APPEND_COLOR_CODE_COND(white, FF_COLOR_FG_WHITE) else FF_APPEND_COLOR_CODE_COND(default, FF_COLOR_FG_DEFAULT) else FF_APPEND_COLOR_CODE_COND(light_black, FF_COLOR_FG_LIGHT_BLACK) else FF_APPEND_COLOR_CODE_COND(light_red, FF_COLOR_FG_LIGHT_RED) else FF_APPEND_COLOR_CODE_COND(light_green, FF_COLOR_FG_LIGHT_GREEN) else FF_APPEND_COLOR_CODE_COND(light_yellow, FF_COLOR_FG_LIGHT_YELLOW) else FF_APPEND_COLOR_CODE_COND(light_blue, FF_COLOR_FG_LIGHT_BLUE) else FF_APPEND_COLOR_CODE_COND(light_magenta, FF_COLOR_FG_LIGHT_MAGENTA) else FF_APPEND_COLOR_CODE_COND(light_cyan, FF_COLOR_FG_LIGHT_CYAN) else FF_APPEND_COLOR_CODE_COND(light_white, FF_COLOR_FG_LIGHT_WHITE) else FF_APPEND_COLOR_PROP_COND(keys, colorKeys) else FF_APPEND_COLOR_PROP_COND(title, colorTitle) else FF_APPEND_COLOR_PROP_COND(output, colorOutput) else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator) else { + else FF_APPEND_COLOR_CODE_COND(bold_, FF_COLOR_MODE_BOLD) + else FF_APPEND_COLOR_CODE_COND(bright_, FF_COLOR_MODE_BOLD) + else FF_APPEND_COLOR_CODE_COND(dim_, FF_COLOR_MODE_DIM) + else FF_APPEND_COLOR_CODE_COND(italic_, FF_COLOR_MODE_ITALIC) + else FF_APPEND_COLOR_CODE_COND(underline_, FF_COLOR_MODE_UNDERLINE) + else FF_APPEND_COLOR_CODE_COND(blink_, FF_COLOR_MODE_BLINK) + else FF_APPEND_COLOR_CODE_COND(inverse_, FF_COLOR_MODE_INVERSE) + else FF_APPEND_COLOR_CODE_COND(hidden_, FF_COLOR_MODE_HIDDEN) + else FF_APPEND_COLOR_CODE_COND(strike_, FF_COLOR_MODE_STRIKETHROUGH) + else FF_APPEND_COLOR_CODE_COND(black, FF_COLOR_FG_BLACK) + else FF_APPEND_COLOR_CODE_COND(red, FF_COLOR_FG_RED) + else FF_APPEND_COLOR_CODE_COND(green, FF_COLOR_FG_GREEN) + else FF_APPEND_COLOR_CODE_COND(yellow, FF_COLOR_FG_YELLOW) + else FF_APPEND_COLOR_CODE_COND(blue, FF_COLOR_FG_BLUE) + else FF_APPEND_COLOR_CODE_COND(magenta, FF_COLOR_FG_MAGENTA) + else FF_APPEND_COLOR_CODE_COND(cyan, FF_COLOR_FG_CYAN) + else FF_APPEND_COLOR_CODE_COND(white, FF_COLOR_FG_WHITE) + else FF_APPEND_COLOR_CODE_COND(default, FF_COLOR_FG_DEFAULT) + else FF_APPEND_COLOR_CODE_COND(light_black, FF_COLOR_FG_LIGHT_BLACK) + else FF_APPEND_COLOR_CODE_COND(light_red, FF_COLOR_FG_LIGHT_RED) + else FF_APPEND_COLOR_CODE_COND(light_green, FF_COLOR_FG_LIGHT_GREEN) + else FF_APPEND_COLOR_CODE_COND(light_yellow, FF_COLOR_FG_LIGHT_YELLOW) + else FF_APPEND_COLOR_CODE_COND(light_blue, FF_COLOR_FG_LIGHT_BLUE) + else FF_APPEND_COLOR_CODE_COND(light_magenta, FF_COLOR_FG_LIGHT_MAGENTA) + else FF_APPEND_COLOR_CODE_COND(light_cyan, FF_COLOR_FG_LIGHT_CYAN) + else FF_APPEND_COLOR_CODE_COND(light_white, FF_COLOR_FG_LIGHT_WHITE) + else FF_APPEND_COLOR_PROP_COND(keys, colorKeys) + else FF_APPEND_COLOR_PROP_COND(title, colorTitle) + else FF_APPEND_COLOR_PROP_COND(output, colorOutput) + else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator) + else { fprintf(stderr, "Error: invalid color code found: %s\n", value); exit(479); } + // clang-format on } else if (value[0] == '@') { // Xterm 256 color ++value; From 1ee8528f10ae0cfc0876be3b06707def20a14f22 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sun, 13 Sep 2026 12:26:57 +0800 Subject: [PATCH 29/76] Format: makes `{#title}` and `{#key}` honors `brightColor` config --- src/common/impl/option.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common/impl/option.c b/src/common/impl/option.c index 1db0438148..24b4c7acca 100644 --- a/src/common/impl/option.c +++ b/src/common/impl/option.c @@ -122,8 +122,9 @@ void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer) { value += strlen(#prefix); \ continue; \ } -#define FF_APPEND_COLOR_PROP_COND(prefix, prop) \ +#define FF_APPEND_COLOR_PROP_COND(prefix, prop, honorBrightColor) \ if (ffStrStartsWithIgnCase(value, #prefix)) { \ + if (honorBrightColor && instance.config.display.brightColor) ffStrbufAppendS(buffer, "1;"); \ if (instance.config.display.prop.length) ffStrbufAppend(buffer, &instance.config.display.prop); \ else ffStrbufAppendS(buffer, FF_COLOR_FG_DEFAULT); \ value += strlen(#prefix); \ @@ -159,10 +160,10 @@ void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer) { else FF_APPEND_COLOR_CODE_COND(light_magenta, FF_COLOR_FG_LIGHT_MAGENTA) else FF_APPEND_COLOR_CODE_COND(light_cyan, FF_COLOR_FG_LIGHT_CYAN) else FF_APPEND_COLOR_CODE_COND(light_white, FF_COLOR_FG_LIGHT_WHITE) - else FF_APPEND_COLOR_PROP_COND(keys, colorKeys) - else FF_APPEND_COLOR_PROP_COND(title, colorTitle) - else FF_APPEND_COLOR_PROP_COND(output, colorOutput) - else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator) + else FF_APPEND_COLOR_PROP_COND(keys, colorKeys, true) + else FF_APPEND_COLOR_PROP_COND(title, colorTitle, true) + else FF_APPEND_COLOR_PROP_COND(output, colorOutput, false) + else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator, false) else { fprintf(stderr, "Error: invalid color code found: %s\n", value); exit(479); From 833403592735f9fceff6c0dbe3d01dadbe421982 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sun, 13 Sep 2026 12:27:17 +0800 Subject: [PATCH 30/76] Logo: small optimizations --- src/logo/logo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/logo/logo.c b/src/logo/logo.c index 1cc3211dc4..12af2251ac 100644 --- a/src/logo/logo.c +++ b/src/logo/logo.c @@ -287,11 +287,11 @@ void ffLogoPrintChars(const char* data, bool doColorReplacement) { static void logoApplyColors(const FFlogo* logo, bool replacement) { if (instance.config.display.colorTitle.length == 0) { - ffStrbufAppendS(&instance.config.display.colorTitle, logo->colorTitle ?: logo->colors[0]); + ffStrbufSetStatic(&instance.config.display.colorTitle, logo->colorTitle ?: logo->colors[0]); } if (instance.config.display.colorKeys.length == 0) { - ffStrbufAppendS(&instance.config.display.colorKeys, logo->colorKeys ?: logo->colors[1]); + ffStrbufSetStatic(&instance.config.display.colorKeys, logo->colorKeys ?: logo->colors[1]); } if (replacement) { @@ -300,7 +300,7 @@ static void logoApplyColors(const FFlogo* logo, bool replacement) { const char* const* colors = logo->colors; for (int i = 0; *colors != nullptr && i < FASTFETCH_LOGO_MAX_COLORS; i++, colors++) { if (options->colors[i].length == 0) { - ffStrbufAppendS(&options->colors[i], *colors); + ffStrbufSetStatic(&options->colors[i], *colors); } } } From cd5a2def9a90090b241757b6dce341d69c5db66d Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sun, 13 Sep 2026 17:33:15 +0800 Subject: [PATCH 31/76] DE (Linux): uses `COSMIC_VERSION` if available https://github.com/pop-os/cosmic-comp/issues/2425#issuecomment-5652401962 --- src/detection/de/de_linux.c | 8 +++++++- src/detection/displayserver/displayserver.h | 1 + src/detection/displayserver/linux/wmde.c | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/detection/de/de_linux.c b/src/detection/de/de_linux.c index 719d81fe83..c6cebbae5c 100644 --- a/src/detection/de/de_linux.c +++ b/src/detection/de/de_linux.c @@ -184,6 +184,12 @@ static const char* getTrinity(FFstrbuf* result, [[maybe_unused]] FFDEOptions* op } static const char* getCosmic(FFstrbuf* result, [[maybe_unused]] FFDEOptions* options) { + const char* env = getenv("COSMIC_VERSION"); + if (env) { + ffStrbufSetS(result, env); + return nullptr; + } + if (ffProcessAppendStdOut(result, (char* const[]) { "cosmic-comp", "--version", nullptr }) == nullptr) { // cosmic-comp 0.1.0 (git commit fa88002ba41d2edec25dd7ffdee9719fbb928fc0) ffStrbufSubstrAfterFirstC(result, ' '); @@ -256,7 +262,7 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti getUnity(result, options); } else if (ffStrbufEqualS(deName, "trinity")) { getTrinity(result, options); - } else if (ffStrbufEqualS(deName, "COSMIC")) { + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_COSMIC)) { getCosmic(result, options); } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_ENLIGHTENMENT)) { getEnlightenment(result, options); diff --git a/src/detection/displayserver/displayserver.h b/src/detection/displayserver/displayserver.h index 715a2de439..5ce08c386d 100644 --- a/src/detection/displayserver/displayserver.h +++ b/src/detection/displayserver/displayserver.h @@ -17,6 +17,7 @@ #define FF_DE_PRETTY_UKUI "UKUI" #define FF_DE_PRETTY_NEBIDE "NebiDE" #define FF_DE_PRETTY_ENLIGHTENMENT "Enlightenment" +#define FF_DE_PRETTY_COSMIC "COSMIC" #define FF_WM_PRETTY_KWIN "KWin" #define FF_WM_PRETTY_MUTTER "Mutter" diff --git a/src/detection/displayserver/linux/wmde.c b/src/detection/displayserver/linux/wmde.c index 984bdbeda4..ed994fc7f6 100644 --- a/src/detection/displayserver/linux/wmde.c +++ b/src/detection/displayserver/linux/wmde.c @@ -75,6 +75,10 @@ static const char* parseEnv(void) { return "Sway"; } + if (getenv("COSMIC_VERSION") != nullptr) { + return "COSMIC"; + } + #if __linux__ && !__ANDROID__ if ( getenv("WAYLAND_DISPLAY") != nullptr && From d79694560410732c2577577d6f0968c37ba8b1ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:49:44 +0800 Subject: [PATCH 32/76] CI: Bump docker/setup-qemu-action in the github-actions group (#2583) Bumps the github-actions group with 1 update: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action). Updates `docker/setup-qemu-action` from 4.2.0 to 4.3.0 - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/96fe6ef7f33517b61c61be40b68a1882f3264fb8...1f40c72289eff860ee54a304f1438e3cff362e0a) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-linux-loong64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux-loong64.yml b/.github/workflows/build-linux-loong64.yml index 453f43f08c..2a69a2c2de 100644 --- a/.github/workflows/build-linux-loong64.yml +++ b/.github/workflows/build-linux-loong64.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: set up QEMU for loong64 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: build in loong64 container run: | From 98e9b6eef1adf2c2e8dbc4785afd5198462fb13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sun, 13 Sep 2026 21:45:58 +0800 Subject: [PATCH 33/76] 3rdparty: embeds libsixel --- CMakeLists.txt | 22 + src/3rdparty/sixel/LICENSE | 18 + src/3rdparty/sixel/LICENSE.pnmcolormap | 13 + src/3rdparty/sixel/LICENSE.sixel | 49 + src/3rdparty/sixel/README.md | 147 +++ src/3rdparty/sixel/allocator.c | 379 ++++++ src/3rdparty/sixel/allocator.h | 49 + src/3rdparty/sixel/config.h | 58 + src/3rdparty/sixel/dither.c | 918 +++++++++++++ src/3rdparty/sixel/dither.h | 78 ++ src/3rdparty/sixel/malloc_stub.h | 50 + src/3rdparty/sixel/output.c | 210 +++ src/3rdparty/sixel/output.h | 91 ++ src/3rdparty/sixel/pixelformat.c | 725 +++++++++++ src/3rdparty/sixel/quant.c | 1549 ++++++++++++++++++++++ src/3rdparty/sixel/quant.h | 90 ++ src/3rdparty/sixel/repo.json | 6 + src/3rdparty/sixel/sixel.h | 1173 +++++++++++++++++ src/3rdparty/sixel/status.c | 355 ++++++ src/3rdparty/sixel/status.h | 47 + src/3rdparty/sixel/tosixel.c | 1627 ++++++++++++++++++++++++ 21 files changed, 7654 insertions(+) create mode 100644 src/3rdparty/sixel/LICENSE create mode 100644 src/3rdparty/sixel/LICENSE.pnmcolormap create mode 100644 src/3rdparty/sixel/LICENSE.sixel create mode 100644 src/3rdparty/sixel/README.md create mode 100644 src/3rdparty/sixel/allocator.c create mode 100644 src/3rdparty/sixel/allocator.h create mode 100644 src/3rdparty/sixel/config.h create mode 100644 src/3rdparty/sixel/dither.c create mode 100644 src/3rdparty/sixel/dither.h create mode 100644 src/3rdparty/sixel/malloc_stub.h create mode 100644 src/3rdparty/sixel/output.c create mode 100644 src/3rdparty/sixel/output.h create mode 100644 src/3rdparty/sixel/pixelformat.c create mode 100644 src/3rdparty/sixel/quant.c create mode 100644 src/3rdparty/sixel/quant.h create mode 100644 src/3rdparty/sixel/repo.json create mode 100644 src/3rdparty/sixel/sixel.h create mode 100644 src/3rdparty/sixel/status.c create mode 100644 src/3rdparty/sixel/status.h create mode 100644 src/3rdparty/sixel/tosixel.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 685f2f6e62..cdd0f29412 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR Dragon cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF) option(ENABLE_ZLIB "Enable zlib" ON) +option(ENABLE_SIXEL "Enable sixel image logo output (vendored libsixel encoder)" 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) option(ENABLE_TRACER "Build fastfetch with function tracing" OFF) @@ -1418,6 +1419,21 @@ 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} ) @@ -1993,6 +2009,12 @@ target_include_directories(libfastfetch PUBLIC ${PROJECT_SOURCE_DIR}/src ) +if(ENABLE_SIXEL) + # src/3rdparty/sixel/*.c include (angle brackets, so the directory itself + # has to be on the include path) + target_include_directories(libfastfetch PRIVATE ${PROJECT_SOURCE_DIR}/src/3rdparty/sixel) +endif() + target_link_libraries(libfastfetch PRIVATE ${CMAKE_DL_LIBS} ) diff --git a/src/3rdparty/sixel/LICENSE b/src/3rdparty/sixel/LICENSE new file mode 100644 index 0000000000..3da7b40442 --- /dev/null +++ b/src/3rdparty/sixel/LICENSE @@ -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. diff --git a/src/3rdparty/sixel/LICENSE.pnmcolormap b/src/3rdparty/sixel/LICENSE.pnmcolormap new file mode 100644 index 0000000000..0b8ab89f7a --- /dev/null +++ b/src/3rdparty/sixel/LICENSE.pnmcolormap @@ -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. + diff --git a/src/3rdparty/sixel/LICENSE.sixel b/src/3rdparty/sixel/LICENSE.sixel new file mode 100644 index 0000000000..862a32914c --- /dev/null +++ b/src/3rdparty/sixel/LICENSE.sixel @@ -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. + diff --git a/src/3rdparty/sixel/README.md b/src/3rdparty/sixel/README.md new file mode 100644 index 0000000000..7550359a95 --- /dev/null +++ b/src/3rdparty/sixel/README.md @@ -0,0 +1,147 @@ +# 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 only**. + +## Why this is vendored + +Neither MSYS2 nor vcpkg packages libsixel, so 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. + +## Why Windows only + +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. + +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`, and these sources are **not compiled at all** on Linux / macOS / +BSD. + +Consequence: sixel bytes differ between Windows (libsixel) and the rest +(ImageMagick). That is deliberate and accepted — see +`doc/windows-image-backend.md` decision 9. + +## 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, 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 `config.h` below. +- **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 / 255 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 / 255 KB. + +## Local modifications + +Two deliberate divergences from upstream. Re-apply both 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 `` / + `` include in `pixelformat.c` / `status.c` is a hard error. + +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 two local modifications listed above +# 3. verify +``` + +Verification (must be 0 diagnostics, and the encoder must emit a DCS-wrapped stream): + +```sh +CC=/c/msys64/clang64/bin/cc.exe +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. diff --git a/src/3rdparty/sixel/allocator.c b/src/3rdparty/sixel/allocator.c new file mode 100644 index 0000000000..2264d83d49 --- /dev/null +++ b/src/3rdparty/sixel/allocator.c @@ -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 + +#if HAVE_ASSERT_H +# include +#endif /* HAVE_ASSERT_H */ +#if HAVE_SYS_TYPES_H +# include +#endif /* HAVE_SYS_TYPES_H */ +#if HAVE_ERRNO_H +# include +#endif /* HAVE_ERRNO_H */ +#if HAVE_MEMORY_H +# include +#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 */ diff --git a/src/3rdparty/sixel/allocator.h b/src/3rdparty/sixel/allocator.h new file mode 100644 index 0000000000..c18da4b366 --- /dev/null +++ b/src/3rdparty/sixel/allocator.h @@ -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 + +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 */ diff --git a/src/3rdparty/sixel/config.h b/src/3rdparty/sixel/config.h new file mode 100644 index 0000000000..4e34eb0347 --- /dev/null +++ b/src/3rdparty/sixel/config.h @@ -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 */ diff --git a/src/3rdparty/sixel/dither.c b/src/3rdparty/sixel/dither.c new file mode 100644 index 0000000000..e5353e8e1d --- /dev/null +++ b/src/3rdparty/sixel/dither.c @@ -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 +#include + +#if HAVE_MATH_H +# include +#endif /* HAVE_MATH_H */ +#if HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#if HAVE_LIMITS_H +# include +#endif /* HAVE_LIMITS_H */ +#if HAVE_INTTYPES_H +# include +#endif /* HAVE_INTTYPES_H */ + +#include "dither.h" +#include "quant.h" +#include + + +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 */ diff --git a/src/3rdparty/sixel/dither.h b/src/3rdparty/sixel/dither.h new file mode 100644 index 0000000000..1c6d203a65 --- /dev/null +++ b/src/3rdparty/sixel/dither.h @@ -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 + +/* 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 */ diff --git a/src/3rdparty/sixel/malloc_stub.h b/src/3rdparty/sixel/malloc_stub.h new file mode 100644 index 0000000000..2f2621e894 --- /dev/null +++ b/src/3rdparty/sixel/malloc_stub.h @@ -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 +#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 */ diff --git a/src/3rdparty/sixel/output.c b/src/3rdparty/sixel/output.c new file mode 100644 index 0000000000..9efd97d781 --- /dev/null +++ b/src/3rdparty/sixel/output.c @@ -0,0 +1,210 @@ +/* + * 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 +# include + +#if HAVE_ASSERT_H +# include +#endif /* HAVE_ASSERT_H */ + +#include +#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; + 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 */ diff --git a/src/3rdparty/sixel/output.h b/src/3rdparty/sixel/output.h new file mode 100644 index 0000000000..197d09781d --- /dev/null +++ b/src/3rdparty/sixel/output.h @@ -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 */ diff --git a/src/3rdparty/sixel/pixelformat.c b/src/3rdparty/sixel/pixelformat.c new file mode 100644 index 0000000000..60b22f9199 --- /dev/null +++ b/src/3rdparty/sixel/pixelformat.c @@ -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 +#include + +#if HAVE_MEMORY_H +# include +#endif /* HAVE_MEMORY_H */ + +#include + +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 */ diff --git a/src/3rdparty/sixel/quant.c b/src/3rdparty/sixel/quant.c new file mode 100644 index 0000000000..f730b0028e --- /dev/null +++ b/src/3rdparty/sixel/quant.c @@ -0,0 +1,1549 @@ +/* + * + * mediancut algorithm implementation is imported from pnmcolormap.c + * in netpbm library. + * http://netpbm.sourceforge.net/ + * + * ******************************************************************************* + * original license block of pnmcolormap.c + * ******************************************************************************* + * + * 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. + * + * ****************************************************************************** + * + * 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 +#include + +#if HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#if HAVE_MATH_H +#include +#endif /* HAVE_MATH_H */ +#if HAVE_LIMITS_H +# include +#endif /* HAVE_MATH_H */ +#if HAVE_INTTYPES_H +# include +#endif /* HAVE_MATH_H */ + +#include "quant.h" + +#if HAVE_DEBUG +#define quant_trace fprintf +#else +static inline void quant_trace(FILE *f, ...) { (void) f; } +#endif + +/***************************************************************************** + * + * quantization + * + *****************************************************************************/ + +typedef struct box* boxVector; +struct box { + unsigned int ind; + unsigned int colors; + unsigned int sum; +}; + +typedef unsigned long sample; +typedef sample * tuple; + +struct tupleint { + /* An ordered pair of a tuple value and an integer, such as you + would find in a tuple table or tuple hash. + Note that this is a variable length structure. + */ + unsigned int value; + sample tuple[1]; + /* This is actually a variable size array -- its size is the + depth of the tuple in question. Some compilers do not let us + declare a variable length array. + */ +}; +typedef struct tupleint ** tupletable; + +typedef struct { + unsigned int size; + tupletable table; +} tupletable2; + +static unsigned int compareplanePlane; + /* This is a parameter to compareplane(). We use this global variable + so that compareplane() can be called by qsort(), to compare two + tuples. qsort() doesn't pass any arguments except the two tuples. + */ +static int +compareplane(const void * const arg1, + const void * const arg2) +{ + int lhs, rhs; + + typedef const struct tupleint * const * const sortarg; + sortarg comparandPP = (sortarg) arg1; + sortarg comparatorPP = (sortarg) arg2; + lhs = (int)(*comparandPP)->tuple[compareplanePlane]; + rhs = (int)(*comparatorPP)->tuple[compareplanePlane]; + + return lhs - rhs; +} + + +static int +sumcompare(const void * const b1, const void * const b2) +{ + return (int)((boxVector)b2)->sum - (int)((boxVector)b1)->sum; +} + + +static SIXELSTATUS +alloctupletable( + tupletable /* out */ *result, + unsigned int const /* in */ depth, + unsigned int const /* in */ size, + sixel_allocator_t /* in */ *allocator) +{ + SIXELSTATUS status = SIXEL_FALSE; + enum { message_buffer_size = 256 }; + char message[message_buffer_size]; + int nwrite; + unsigned int mainTableSize; + unsigned int tupleIntSize; + unsigned int allocSize; + void * pool; + tupletable tbl; + unsigned int i; + + if (UINT_MAX / sizeof(struct tupleint) < size) { + nwrite = sprintf(message, + "size %u is too big for arithmetic", + size); + if (nwrite > 0) { + sixel_helper_set_additional_message(message); + } + status = SIXEL_RUNTIME_ERROR; + goto end; + } + + mainTableSize = size * sizeof(struct tupleint *); + tupleIntSize = sizeof(struct tupleint) - sizeof(sample) + + depth * sizeof(sample); + + /* To save the enormous amount of time it could take to allocate + each individual tuple, we do a trick here and allocate everything + as a single malloc block and suballocate internally. + */ + if ((UINT_MAX - mainTableSize) / tupleIntSize < size) { + nwrite = sprintf(message, + "size %u is too big for arithmetic", + size); + if (nwrite > 0) { + sixel_helper_set_additional_message(message); + } + status = SIXEL_RUNTIME_ERROR; + goto end; + } + + allocSize = mainTableSize + size * tupleIntSize; + + pool = sixel_allocator_malloc(allocator, allocSize); + if (pool == NULL) { + sprintf(message, + "unable to allocate %u bytes for a %u-entry " + "tuple table", + allocSize, size); + sixel_helper_set_additional_message(message); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + tbl = (tupletable) pool; + + for (i = 0; i < size; ++i) + tbl[i] = (struct tupleint *) + ((char*)pool + mainTableSize + i * tupleIntSize); + + *result = tbl; + + status = SIXEL_OK; + +end: + return status; +} + + +/* +** Here is the fun part, the median-cut colormap generator. This is based +** on Paul Heckbert's paper "Color Image Quantization for Frame Buffer +** Display", SIGGRAPH '82 Proceedings, page 297. +*/ + +static tupletable2 +newColorMap(unsigned int const newcolors, unsigned int const depth, sixel_allocator_t *allocator) +{ + SIXELSTATUS status = SIXEL_FALSE; + tupletable2 colormap; + unsigned int i; + + colormap.size = 0; + status = alloctupletable(&colormap.table, depth, newcolors, allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + if (colormap.table) { + for (i = 0; i < newcolors; ++i) { + unsigned int plane; + for (plane = 0; plane < depth; ++plane) + colormap.table[i]->tuple[plane] = 0; + } + colormap.size = newcolors; + } + +end: + return colormap; +} + + +static boxVector +newBoxVector( + unsigned int const /* in */ colors, + unsigned int const /* in */ sum, + unsigned int const /* in */ newcolors, + sixel_allocator_t /* in */ *allocator) +{ + boxVector bv; + + bv = (boxVector)sixel_allocator_malloc(allocator, + sizeof(struct box) * (size_t)newcolors); + if (bv == NULL) { + quant_trace(stderr, "out of memory allocating box vector table\n"); + return NULL; + } + + /* Set up the initial box. */ + bv[0].ind = 0; + bv[0].colors = colors; + bv[0].sum = sum; + + return bv; +} + + +static void +findBoxBoundaries(tupletable2 const colorfreqtable, + unsigned int const depth, + unsigned int const boxStart, + unsigned int const boxSize, + sample minval[], + sample maxval[]) +{ +/*---------------------------------------------------------------------------- + Go through the box finding the minimum and maximum of each + component - the boundaries of the box. +-----------------------------------------------------------------------------*/ + unsigned int plane; + unsigned int i; + + for (plane = 0; plane < depth; ++plane) { + minval[plane] = colorfreqtable.table[boxStart]->tuple[plane]; + maxval[plane] = minval[plane]; + } + + for (i = 1; i < boxSize; ++i) { + for (plane = 0; plane < depth; ++plane) { + sample const v = colorfreqtable.table[boxStart + i]->tuple[plane]; + if (v < minval[plane]) minval[plane] = v; + if (v > maxval[plane]) maxval[plane] = v; + } + } +} + + + +static unsigned int +largestByNorm(sample minval[], sample maxval[], unsigned int const depth) +{ + + unsigned int largestDimension; + unsigned int plane; + sample largestSpreadSoFar; + + largestSpreadSoFar = 0; + largestDimension = 0; + for (plane = 0; plane < depth; ++plane) { + sample const spread = maxval[plane]-minval[plane]; + if (spread > largestSpreadSoFar) { + largestDimension = plane; + largestSpreadSoFar = spread; + } + } + return largestDimension; +} + + + +static unsigned int +largestByLuminosity(sample minval[], sample maxval[], unsigned int const depth) +{ +/*---------------------------------------------------------------------------- + This subroutine presumes that the tuple type is either + BLACKANDWHITE, GRAYSCALE, or RGB (which implies pamP->depth is 1 or 3). + To save time, we don't actually check it. +-----------------------------------------------------------------------------*/ + unsigned int retval; + + double lumin_factor[3] = {0.2989, 0.5866, 0.1145}; + + if (depth == 1) { + retval = 0; + } else { + /* An RGB tuple */ + unsigned int largestDimension; + unsigned int plane; + double largestSpreadSoFar; + + largestSpreadSoFar = 0.0; + largestDimension = 0; + + for (plane = 0; plane < 3; ++plane) { + double const spread = + lumin_factor[plane] * (maxval[plane]-minval[plane]); + if (spread > largestSpreadSoFar) { + largestDimension = plane; + largestSpreadSoFar = spread; + } + } + retval = largestDimension; + } + return retval; +} + + + +static void +centerBox(unsigned int const boxStart, + unsigned int const boxSize, + tupletable2 const colorfreqtable, + unsigned int const depth, + tuple const newTuple) +{ + + unsigned int plane; + sample minval, maxval; + unsigned int i; + + for (plane = 0; plane < depth; ++plane) { + minval = maxval = colorfreqtable.table[boxStart]->tuple[plane]; + + for (i = 1; i < boxSize; ++i) { + sample v = colorfreqtable.table[boxStart + i]->tuple[plane]; + minval = minval < v ? minval: v; + maxval = maxval > v ? maxval: v; + } + newTuple[plane] = (minval + maxval) / 2; + } +} + + + +static void +averageColors(unsigned int const boxStart, + unsigned int const boxSize, + tupletable2 const colorfreqtable, + unsigned int const depth, + tuple const newTuple) +{ + unsigned int plane; + sample sum; + unsigned int i; + + for (plane = 0; plane < depth; ++plane) { + sum = 0; + + for (i = 0; i < boxSize; ++i) { + sum += colorfreqtable.table[boxStart + i]->tuple[plane]; + } + + newTuple[plane] = sum / boxSize; + } +} + + + +static void +averagePixels(unsigned int const boxStart, + unsigned int const boxSize, + tupletable2 const colorfreqtable, + unsigned int const depth, + tuple const newTuple) +{ + + unsigned int n; + /* Number of tuples represented by the box */ + unsigned int plane; + unsigned int i; + + /* Count the tuples in question */ + n = 0; /* initial value */ + for (i = 0; i < boxSize; ++i) { + n += (unsigned int)colorfreqtable.table[boxStart + i]->value; + } + + for (plane = 0; plane < depth; ++plane) { + sample sum; + + sum = 0; + + for (i = 0; i < boxSize; ++i) { + sum += colorfreqtable.table[boxStart + i]->tuple[plane] + * (unsigned int)colorfreqtable.table[boxStart + i]->value; + } + + newTuple[plane] = sum / n; + } +} + + + +static tupletable2 +colormapFromBv(unsigned int const newcolors, + boxVector const bv, + unsigned int const boxes, + tupletable2 const colorfreqtable, + unsigned int const depth, + int const methodForRep, + sixel_allocator_t *allocator) +{ + /* + ** Ok, we've got enough boxes. Now choose a representative color for + ** each box. There are a number of possible ways to make this choice. + ** One would be to choose the center of the box; this ignores any structure + ** within the boxes. Another method would be to average all the colors in + ** the box - this is the method specified in Heckbert's paper. A third + ** method is to average all the pixels in the box. + */ + tupletable2 colormap; + unsigned int bi; + + colormap = newColorMap(newcolors, depth, allocator); + if (!colormap.size) { + return colormap; + } + + for (bi = 0; bi < boxes; ++bi) { + switch (methodForRep) { + case SIXEL_REP_CENTER_BOX: + centerBox(bv[bi].ind, bv[bi].colors, + colorfreqtable, depth, + colormap.table[bi]->tuple); + break; + case SIXEL_REP_AVERAGE_COLORS: + averageColors(bv[bi].ind, bv[bi].colors, + colorfreqtable, depth, + colormap.table[bi]->tuple); + break; + case SIXEL_REP_AVERAGE_PIXELS: + averagePixels(bv[bi].ind, bv[bi].colors, + colorfreqtable, depth, + colormap.table[bi]->tuple); + break; + default: + quant_trace(stderr, "Internal error: " + "invalid value of methodForRep: %d\n", + methodForRep); + } + } + return colormap; +} + + +static SIXELSTATUS +splitBox(boxVector const bv, + unsigned int *const boxesP, + unsigned int const bi, + tupletable2 const colorfreqtable, + unsigned int const depth, + int const methodForLargest) +{ +/*---------------------------------------------------------------------------- + Split Box 'bi' in the box vector bv (so that bv contains one more box + than it did as input). Split it so that each new box represents about + half of the pixels in the distribution given by 'colorfreqtable' for + the colors in the original box, but with distinct colors in each of the + two new boxes. + + Assume the box contains at least two colors. +-----------------------------------------------------------------------------*/ + SIXELSTATUS status = SIXEL_FALSE; + unsigned int const boxStart = bv[bi].ind; + unsigned int const boxSize = bv[bi].colors; + unsigned int const sm = bv[bi].sum; + + enum { max_depth= 16 }; + sample minval[max_depth]; + sample maxval[max_depth]; + + /* assert(max_depth >= depth); */ + + unsigned int largestDimension; + /* number of the plane with the largest spread */ + unsigned int medianIndex; + unsigned int lowersum; + /* Number of pixels whose value is "less than" the median */ + + findBoxBoundaries(colorfreqtable, depth, boxStart, boxSize, + minval, maxval); + + /* Find the largest dimension, and sort by that component. I have + included two methods for determining the "largest" dimension; + first by simply comparing the range in RGB space, and second by + transforming into luminosities before the comparison. + */ + switch (methodForLargest) { + case SIXEL_LARGE_NORM: + largestDimension = largestByNorm(minval, maxval, depth); + break; + case SIXEL_LARGE_LUM: + largestDimension = largestByLuminosity(minval, maxval, depth); + break; + default: + sixel_helper_set_additional_message( + "Internal error: invalid value of methodForLargest."); + status = SIXEL_LOGIC_ERROR; + goto end; + } + + /* TODO: I think this sort should go after creating a box, + not before splitting. Because you need the sort to use + the SIXEL_REP_CENTER_BOX method of choosing a color to + represent the final boxes + */ + + /* Set the gross global variable 'compareplanePlane' as a + parameter to compareplane(), which is called by qsort(). + */ + compareplanePlane = largestDimension; + qsort((char*) &colorfreqtable.table[boxStart], boxSize, + sizeof(colorfreqtable.table[boxStart]), + compareplane); + + { + /* Now find the median based on the counts, so that about half + the pixels (not colors, pixels) are in each subdivision. */ + + unsigned int i; + + lowersum = colorfreqtable.table[boxStart]->value; /* initial value */ + for (i = 1; i < boxSize - 1 && lowersum < sm / 2; ++i) { + lowersum += colorfreqtable.table[boxStart + i]->value; + } + medianIndex = i; + } + /* Split the box, and sort to bring the biggest boxes to the top. */ + + bv[bi].colors = medianIndex; + bv[bi].sum = lowersum; + bv[*boxesP].ind = boxStart + medianIndex; + bv[*boxesP].colors = boxSize - medianIndex; + bv[*boxesP].sum = sm - lowersum; + ++(*boxesP); + qsort((char*) bv, *boxesP, sizeof(struct box), sumcompare); + + status = SIXEL_OK; + +end: + return status; +} + + + +static SIXELSTATUS +mediancut(tupletable2 const colorfreqtable, + unsigned int const depth, + unsigned int const newcolors, + int const methodForLargest, + int const methodForRep, + tupletable2 *const colormapP, + sixel_allocator_t *allocator) +{ +/*---------------------------------------------------------------------------- + Compute a set of only 'newcolors' colors that best represent an + image whose pixels are summarized by the histogram + 'colorfreqtable'. Each tuple in that table has depth 'depth'. + colorfreqtable.table[i] tells the number of pixels in the subject image + have a particular color. + + As a side effect, sort 'colorfreqtable'. +-----------------------------------------------------------------------------*/ + boxVector bv; + unsigned int bi; + unsigned int boxes; + int multicolorBoxesExist; + unsigned int i; + unsigned int sum; + SIXELSTATUS status = SIXEL_FALSE; + + sum = 0; + + for (i = 0; i < colorfreqtable.size; ++i) { + sum += colorfreqtable.table[i]->value; + } + + /* There is at least one box that contains at least 2 colors; ergo, + there is more splitting we can do. */ + bv = newBoxVector(colorfreqtable.size, sum, newcolors, allocator); + if (bv == NULL) { + goto end; + } + boxes = 1; + multicolorBoxesExist = (colorfreqtable.size > 1); + + /* Main loop: split boxes until we have enough. */ + while (boxes < newcolors && multicolorBoxesExist) { + /* Find the first splittable box. */ + for (bi = 0; bi < boxes && bv[bi].colors < 2; ++bi) + ; + if (bi >= boxes) { + multicolorBoxesExist = 0; + } else { + status = splitBox(bv, &boxes, bi, + colorfreqtable, depth, + methodForLargest); + if (SIXEL_FAILED(status)) { + goto end; + } + } + } + *colormapP = colormapFromBv(newcolors, bv, boxes, + colorfreqtable, depth, + methodForRep, allocator); + + sixel_allocator_free(allocator, bv); + + status = SIXEL_OK; + +end: + return status; +} + + +static unsigned int +computeHash(unsigned char const *data, unsigned int const depth) +{ + unsigned int hash = 0; + unsigned int n; + + for (n = 0; n < depth; n++) { + hash |= (unsigned int)(data[depth - 1 - n] >> 3) << n * 5; + } + + return hash; +} + + +static SIXELSTATUS +computeHistogram(unsigned char const /* in */ *data, + unsigned int /* in */ length, + unsigned long const /* in */ depth, + tupletable2 * const /* out */ colorfreqtableP, + int const /* in */ qualityMode, + sixel_allocator_t /* in */ *allocator) +{ + SIXELSTATUS status = SIXEL_FALSE; + typedef unsigned short unit_t; + unsigned int i, n; + unit_t *histogram = NULL; + unit_t *refmap = NULL; + unit_t *ref; + unit_t *it; + unsigned int bucket_index; + unsigned int step; + unsigned int max_sample; + + switch (qualityMode) { + case SIXEL_QUALITY_LOW: + max_sample = 18383; + break; + case SIXEL_QUALITY_HIGH: + max_sample = 1118383; + break; + case SIXEL_QUALITY_FULL: + default: + max_sample = 4003079; + break; + } + + step = length / depth / max_sample * depth; + if (step <= 0) { + step = depth; + } + + quant_trace(stderr, "making histogram...\n"); + + histogram = (unit_t *)sixel_allocator_calloc(allocator, + (size_t)(1 << depth * 5), + sizeof(unit_t)); + if (histogram == NULL) { + sixel_helper_set_additional_message( + "unable to allocate memory for histogram."); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + it = ref = refmap + = (unsigned short *)sixel_allocator_malloc(allocator, + (size_t)(1 << depth * 5) * sizeof(unit_t)); + if (!it) { + sixel_helper_set_additional_message( + "unable to allocate memory for lookup table."); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + + for (i = 0; i < length; i += step) { + bucket_index = computeHash(data + i, 3); + if (histogram[bucket_index] == 0) { + *ref++ = bucket_index; + } + if (histogram[bucket_index] < (unsigned int)(1 << sizeof(unsigned short) * 8) - 1) { + histogram[bucket_index]++; + } + } + + colorfreqtableP->size = (unsigned int)(ref - refmap); + + status = alloctupletable(&colorfreqtableP->table, depth, (unsigned int)(ref - refmap), allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + for (i = 0; i < colorfreqtableP->size; ++i) { + if (histogram[refmap[i]] > 0) { + colorfreqtableP->table[i]->value = histogram[refmap[i]]; + for (n = 0; n < depth; n++) { + colorfreqtableP->table[i]->tuple[depth - 1 - n] + = (sample)((*it >> n * 5 & 0x1f) << 3); + } + } + it++; + } + + quant_trace(stderr, "%u colors found\n", colorfreqtableP->size); + + status = SIXEL_OK; + +end: + sixel_allocator_free(allocator, refmap); + sixel_allocator_free(allocator, histogram); + + return status; +} + + +static int +computeColorMapFromInput(unsigned char const *data, + unsigned int const length, + unsigned int const depth, + unsigned int const reqColors, + int const methodForLargest, + int const methodForRep, + int const qualityMode, + tupletable2 * const colormapP, + unsigned int *origcolors, + sixel_allocator_t *allocator) +{ +/*---------------------------------------------------------------------------- + Produce a colormap containing the best colors to represent the + image stream in file 'ifP'. Figure it out using the median cut + technique. + + The colormap will have 'reqcolors' or fewer colors in it, unless + 'allcolors' is true, in which case it will have all the colors that + are in the input. + + The colormap has the same maxval as the input. + + Put the colormap in newly allocated storage as a tupletable2 + and return its address as *colormapP. Return the number of colors in + it as *colorsP and its maxval as *colormapMaxvalP. + + Return the characteristics of the input file as + *formatP and *freqPamP. (This information is not really + relevant to our colormap mission; just a fringe benefit). +-----------------------------------------------------------------------------*/ + SIXELSTATUS status = SIXEL_FALSE; + tupletable2 colorfreqtable = {0, NULL}; + unsigned int i; + unsigned int n; + + status = computeHistogram(data, length, depth, + &colorfreqtable, qualityMode, allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + if (origcolors) { + *origcolors = colorfreqtable.size; + } + + if (colorfreqtable.size <= reqColors) { + quant_trace(stderr, + "Image already has few enough colors (<=%d). " + "Keeping same colors.\n", reqColors); + /* *colormapP = colorfreqtable; */ + colormapP->size = colorfreqtable.size; + status = alloctupletable(&colormapP->table, depth, colorfreqtable.size, allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + for (i = 0; i < colorfreqtable.size; ++i) { + colormapP->table[i]->value = colorfreqtable.table[i]->value; + for (n = 0; n < depth; ++n) { + colormapP->table[i]->tuple[n] = colorfreqtable.table[i]->tuple[n]; + } + } + } else { + quant_trace(stderr, "choosing %d colors...\n", reqColors); + status = mediancut(colorfreqtable, depth, reqColors, + methodForLargest, methodForRep, colormapP, allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + quant_trace(stderr, "%d colors are choosed.\n", colorfreqtable.size); + } + + status = SIXEL_OK; + +end: + sixel_allocator_free(allocator, colorfreqtable.table); + return status; +} + + +/* diffuse error energy to surround pixels */ +static void +error_diffuse(unsigned char /* in */ *data, /* base address of pixel buffer */ + int /* in */ pos, /* address of the destination pixel */ + int /* in */ depth, /* color depth in bytes */ + int /* in */ error, /* error energy */ + int /* in */ numerator, /* numerator of diffusion coefficient */ + int /* in */ denominator /* denominator of diffusion coefficient */) +{ + int c; + + data += pos * depth; + + c = *data + error * numerator / denominator; + if (c < 0) { + c = 0; + } + if (c >= 1 << 8) { + c = (1 << 8) - 1; + } + *data = (unsigned char)c; +} + + +static void +diffuse_none(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + /* unused */ (void) data; + /* unused */ (void) width; + /* unused */ (void) height; + /* unused */ (void) x; + /* unused */ (void) y; + /* unused */ (void) depth; + /* unused */ (void) error; +} + + +static void +diffuse_fs(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + int pos; + + pos = y * width + x; + + /* Floyd Steinberg Method + * curr 7/16 + * 3/16 5/48 1/16 + */ + if (x < width - 1 && y < height - 1) { + /* add error to the right cell */ + error_diffuse(data, pos + width * 0 + 1, depth, error, 7, 16); + /* add error to the left-bottom cell */ + error_diffuse(data, pos + width * 1 - 1, depth, error, 3, 16); + /* add error to the bottom cell */ + error_diffuse(data, pos + width * 1 + 0, depth, error, 5, 16); + /* add error to the right-bottom cell */ + error_diffuse(data, pos + width * 1 + 1, depth, error, 1, 16); + } +} + + +static void +diffuse_atkinson(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + int pos; + + pos = y * width + x; + + /* Atkinson's Method + * curr 1/8 1/8 + * 1/8 1/8 1/8 + * 1/8 + */ + if (y < height - 2) { + /* add error to the right cell */ + error_diffuse(data, pos + width * 0 + 1, depth, error, 1, 8); + /* add error to the 2th right cell */ + error_diffuse(data, pos + width * 0 + 2, depth, error, 1, 8); + /* add error to the left-bottom cell */ + error_diffuse(data, pos + width * 1 - 1, depth, error, 1, 8); + /* add error to the bottom cell */ + error_diffuse(data, pos + width * 1 + 0, depth, error, 1, 8); + /* add error to the right-bottom cell */ + error_diffuse(data, pos + width * 1 + 1, depth, error, 1, 8); + /* add error to the 2th bottom cell */ + error_diffuse(data, pos + width * 2 + 0, depth, error, 1, 8); + } +} + + +static void +diffuse_jajuni(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + int pos; + + pos = y * width + x; + + /* Jarvis, Judice & Ninke Method + * curr 7/48 5/48 + * 3/48 5/48 7/48 5/48 3/48 + * 1/48 3/48 5/48 3/48 1/48 + */ + if (pos < (height - 2) * width - 2) { + error_diffuse(data, pos + width * 0 + 1, depth, error, 7, 48); + error_diffuse(data, pos + width * 0 + 2, depth, error, 5, 48); + error_diffuse(data, pos + width * 1 - 2, depth, error, 3, 48); + error_diffuse(data, pos + width * 1 - 1, depth, error, 5, 48); + error_diffuse(data, pos + width * 1 + 0, depth, error, 7, 48); + error_diffuse(data, pos + width * 1 + 1, depth, error, 5, 48); + error_diffuse(data, pos + width * 1 + 2, depth, error, 3, 48); + error_diffuse(data, pos + width * 2 - 2, depth, error, 1, 48); + error_diffuse(data, pos + width * 2 - 1, depth, error, 3, 48); + error_diffuse(data, pos + width * 2 + 0, depth, error, 5, 48); + error_diffuse(data, pos + width * 2 + 1, depth, error, 3, 48); + error_diffuse(data, pos + width * 2 + 2, depth, error, 1, 48); + } +} + + +static void +diffuse_stucki(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + int pos; + + pos = y * width + x; + + /* Stucki's Method + * curr 8/48 4/48 + * 2/48 4/48 8/48 4/48 2/48 + * 1/48 2/48 4/48 2/48 1/48 + */ + if (pos < (height - 2) * width - 2) { + error_diffuse(data, pos + width * 0 + 1, depth, error, 1, 6); + error_diffuse(data, pos + width * 0 + 2, depth, error, 1, 12); + error_diffuse(data, pos + width * 1 - 2, depth, error, 1, 24); + error_diffuse(data, pos + width * 1 - 1, depth, error, 1, 12); + error_diffuse(data, pos + width * 1 + 0, depth, error, 1, 6); + error_diffuse(data, pos + width * 1 + 1, depth, error, 1, 12); + error_diffuse(data, pos + width * 1 + 2, depth, error, 1, 24); + error_diffuse(data, pos + width * 2 - 2, depth, error, 1, 48); + error_diffuse(data, pos + width * 2 - 1, depth, error, 1, 24); + error_diffuse(data, pos + width * 2 + 0, depth, error, 1, 12); + error_diffuse(data, pos + width * 2 + 1, depth, error, 1, 24); + error_diffuse(data, pos + width * 2 + 2, depth, error, 1, 48); + } +} + + +static void +diffuse_burkes(unsigned char *data, int width, int height, + int x, int y, int depth, int error) +{ + int pos; + + pos = y * width + x; + + /* Burkes' Method + * curr 4/16 2/16 + * 1/16 2/16 4/16 2/16 1/16 + */ + if (pos < (height - 1) * width - 2) { + error_diffuse(data, pos + width * 0 + 1, depth, error, 1, 4); + error_diffuse(data, pos + width * 0 + 2, depth, error, 1, 8); + error_diffuse(data, pos + width * 1 - 2, depth, error, 1, 16); + error_diffuse(data, pos + width * 1 - 1, depth, error, 1, 8); + error_diffuse(data, pos + width * 1 + 0, depth, error, 1, 4); + error_diffuse(data, pos + width * 1 + 1, depth, error, 1, 8); + error_diffuse(data, pos + width * 1 + 2, depth, error, 1, 16); + } +} + +static float +mask_a (int x, int y, int c) +{ + return ((((x + c * 67) + y * 236) * 119) & 255 ) / 128.0 - 1.0; +} + +static float +mask_x (int x, int y, int c) +{ + return ((((x + c * 29) ^ y* 149) * 1234) & 511 ) / 256.0 - 1.0; +} + +/* lookup closest color from palette with "normal" strategy */ +static int +lookup_normal(unsigned char const * const pixel, + int const depth, + unsigned char const * const palette, + int const reqcolor, + unsigned short * const cachetable, + int const complexion) +{ + int result; + int diff; + int r; + int i; + int n; + int distant; + + result = (-1); + diff = INT_MAX; + + /* don't use cachetable in 'normal' strategy */ + (void) cachetable; + + for (i = 0; i < reqcolor; i++) { + distant = 0; + r = pixel[0] - palette[i * depth + 0]; + distant += r * r * complexion; + for (n = 1; n < depth; ++n) { + r = pixel[n] - palette[i * depth + n]; + distant += r * r; + } + if (distant < diff) { + diff = distant; + result = i; + } + } + + return result; +} + + +/* lookup closest color from palette with "fast" strategy */ +static int +lookup_fast(unsigned char const * const pixel, + int const depth, + unsigned char const * const palette, + int const reqcolor, + unsigned short * const cachetable, + int const complexion) +{ + int result; + unsigned int hash; + int diff; + int cache; + int i; + int distant; + + /* don't use depth in 'fast' strategy because it's always 3 */ + (void) depth; + + result = (-1); + diff = INT_MAX; + hash = computeHash(pixel, 3); + + cache = cachetable[hash]; + if (cache) { /* fast lookup */ + return cache - 1; + } + /* collision */ + for (i = 0; i < reqcolor; i++) { + distant = 0; +#if 0 + for (n = 0; n < 3; ++n) { + r = pixel[n] - palette[i * 3 + n]; + distant += r * r; + } +#elif 1 /* complexion correction */ + distant = (pixel[0] - palette[i * 3 + 0]) * (pixel[0] - palette[i * 3 + 0]) * complexion + + (pixel[1] - palette[i * 3 + 1]) * (pixel[1] - palette[i * 3 + 1]) + + (pixel[2] - palette[i * 3 + 2]) * (pixel[2] - palette[i * 3 + 2]) + ; +#endif + if (distant < diff) { + diff = distant; + result = i; + } + } + cachetable[hash] = result + 1; + + return result; +} + + +static int +lookup_mono_darkbg(unsigned char const * const pixel, + int const depth, + unsigned char const * const palette, + int const reqcolor, + unsigned short * const cachetable, + int const complexion) +{ + int n; + int distant; + + /* unused */ (void) palette; + /* unused */ (void) cachetable; + /* unused */ (void) complexion; + + distant = 0; + for (n = 0; n < depth; ++n) { + distant += pixel[n]; + } + return distant >= 128 * reqcolor ? 1: 0; +} + + +static int +lookup_mono_lightbg(unsigned char const * const pixel, + int const depth, + unsigned char const * const palette, + int const reqcolor, + unsigned short * const cachetable, + int const complexion) +{ + int n; + int distant; + + /* unused */ (void) palette; + /* unused */ (void) cachetable; + /* unused */ (void) complexion; + + distant = 0; + for (n = 0; n < depth; ++n) { + distant += pixel[n]; + } + return distant < 128 * reqcolor ? 1: 0; +} + + +/* choose colors using median-cut method */ +SIXELSTATUS +sixel_quant_make_palette( + unsigned char /* out */ **result, + unsigned char const /* in */ *data, + unsigned int /* in */ length, + 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) +{ + SIXELSTATUS status = SIXEL_FALSE; + unsigned int i; + unsigned int n; + int ret; + tupletable2 colormap; + unsigned int depth; + int result_depth; + + result_depth = sixel_helper_compute_depth(pixelformat); + if (result_depth <= 0) { + *result = NULL; + goto end; + } + + depth = (unsigned int)result_depth; + + ret = computeColorMapFromInput(data, length, depth, + reqcolors, methodForLargest, + methodForRep, qualityMode, + &colormap, origcolors, allocator); + if (ret != 0) { + *result = NULL; + goto end; + } + *ncolors = colormap.size; + quant_trace(stderr, "tupletable size: %d\n", *ncolors); + *result = (unsigned char *)sixel_allocator_malloc(allocator, *ncolors * depth); + for (i = 0; i < *ncolors; i++) { + for (n = 0; n < depth; ++n) { + (*result)[i * depth + n] = colormap.table[i]->tuple[n]; + } + } + + sixel_allocator_free(allocator, colormap.table); + + status = SIXEL_OK; + +end: + return status; +} + + +/* 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 */ depth, + unsigned char /* in */ *palette, + int /* in */ reqcolor, + int /* in */ methodForDiffuse, + int /* in */ foptimize, + int /* in */ foptimize_palette, + int /* in */ complexion, + unsigned short /* in */ *cachetable, + int /* in */ *ncolors, + sixel_allocator_t /* in */ *allocator) +{ + typedef int component_t; + enum { max_depth = 4 }; + enum { max_channel_diff_sq = 255 * 255 }; + SIXELSTATUS status = SIXEL_FALSE; + int pos, n, x, y, sum1, sum2; + int non_weighted_components; + component_t offset; + int color_index; + long long max_complexion; + unsigned short *indextable; + unsigned char new_palette[SIXEL_PALETTE_MAX * 4]; + unsigned short migration_map[SIXEL_PALETTE_MAX]; + float (*f_mask) (int x, int y, int c) = NULL; + void (*f_diffuse)(unsigned char *data, int width, int height, + int x, int y, int depth, int offset); + int (*f_lookup)(unsigned char const * const pixel, + int const depth, + unsigned char const * const palette, + int const reqcolor, + unsigned short * const cachetable, + int const complexion); + + /* check bad reqcolor */ + if (reqcolor < 1) { + status = SIXEL_BAD_ARGUMENT; + sixel_helper_set_additional_message( + "sixel_quant_apply_palette: " + "a bad argument is detected, reqcolor < 0."); + goto end; + } + + /* NOTE: diffuse_jajuni, diffuse_stucki, and diffuse_burkes reference at + * minimum the position pos + width * 1 - 2, so width must be at least 2 + * to avoid underflow. + * On the other hand, diffuse_fs and diffuse_atkinson + * reference pos + width * 1 - 1, but since these functions are only called + * when width >= 1, they do not cause underflow. + */ + if (depth != 3) { + f_diffuse = diffuse_none; + } else { + switch (methodForDiffuse) { + case SIXEL_DIFFUSE_NONE: + f_diffuse = diffuse_none; + break; + case SIXEL_DIFFUSE_ATKINSON: + f_diffuse = diffuse_atkinson; + break; + case SIXEL_DIFFUSE_FS: + f_diffuse = diffuse_fs; + break; + case SIXEL_DIFFUSE_JAJUNI: + /* fallback to diffuse_none if width < 2 */ + f_diffuse = width >= 2 ? diffuse_jajuni: diffuse_none; + break; + case SIXEL_DIFFUSE_STUCKI: + /* fallback to diffuse_none if width < 2 */ + f_diffuse = width >= 2 ? diffuse_stucki: diffuse_none; + break; + case SIXEL_DIFFUSE_BURKES: + /* fallback to diffuse_none if width < 2 */ + f_diffuse = width >= 2 ? diffuse_burkes: diffuse_none; + break; + case SIXEL_DIFFUSE_A_DITHER: + f_diffuse = diffuse_none; + f_mask = mask_a; + break; + case SIXEL_DIFFUSE_X_DITHER: + f_diffuse = diffuse_none; + f_mask = mask_x; + break; + default: + quant_trace(stderr, "Internal error: invalid value of" + " methodForDiffuse: %d\n", + methodForDiffuse); + f_diffuse = diffuse_none; + break; + } + } + + f_lookup = NULL; + if (reqcolor == 2) { + sum1 = 0; + sum2 = 0; + for (n = 0; n < depth; ++n) { + sum1 += palette[n]; + } + for (n = depth; n < depth + depth; ++n) { + sum2 += palette[n]; + } + if (sum1 == 0 && sum2 == 255 * 3) { + f_lookup = lookup_mono_darkbg; + } else if (sum1 == 255 * 3 && sum2 == 0) { + f_lookup = lookup_mono_lightbg; + } + } + if (f_lookup == NULL) { + if (foptimize && depth == 3) { + f_lookup = lookup_fast; + } else { + f_lookup = lookup_normal; + } + } + + if ((f_lookup == lookup_fast || f_lookup == lookup_normal) && complexion > 1) { + non_weighted_components = depth > 1 ? depth - 1 : 0; + max_complexion = (INT_MAX - (long long)max_channel_diff_sq + * (long long)non_weighted_components) + / (long long)max_channel_diff_sq; + if ((long long)complexion > max_complexion) { + status = SIXEL_BAD_ARGUMENT; + sixel_helper_set_additional_message( + "sixel_quant_apply_palette: complexion parameter is too large."); + goto end; + } + } + + indextable = cachetable; + if (cachetable == NULL && f_lookup == lookup_fast) { + indextable = (unsigned short *)sixel_allocator_calloc(allocator, + (size_t)(1 << depth * 5), + sizeof(unsigned short)); + if (!indextable) { + quant_trace(stderr, "Unable to allocate memory for indextable.\n"); + goto end; + } + } + + if (foptimize_palette) { + *ncolors = 0; + + memset(new_palette, 0x00, sizeof(SIXEL_PALETTE_MAX * depth)); + memset(migration_map, 0x00, sizeof(migration_map)); + + if (f_mask) { + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) { + unsigned char copy[max_depth]; + int d; + int val; + + pos = y * width + x; + for (d = 0; d < depth; d ++) { + val = data[pos * depth + d] + f_mask(x, y, d) * 32; + copy[d] = val < 0 ? 0 : val > 255 ? 255 : val; + } + color_index = f_lookup(copy, depth, + palette, reqcolor, indextable, complexion); + if (migration_map[color_index] == 0) { + result[pos] = *ncolors; + for (n = 0; n < depth; ++n) { + new_palette[*ncolors * depth + n] = palette[color_index * depth + n]; + } + ++*ncolors; + migration_map[color_index] = *ncolors; + } else { + result[pos] = migration_map[color_index] - 1; + } + } + } + memcpy(palette, new_palette, (size_t)(*ncolors * depth)); + } else { + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) { + pos = y * width + x; + color_index = f_lookup(data + (pos * depth), depth, + palette, reqcolor, indextable, complexion); + if (migration_map[color_index] == 0) { + result[pos] = *ncolors; + for (n = 0; n < depth; ++n) { + new_palette[*ncolors * depth + n] = palette[color_index * depth + n]; + } + ++*ncolors; + migration_map[color_index] = *ncolors; + } else { + result[pos] = migration_map[color_index] - 1; + } + for (n = 0; n < depth; ++n) { + offset = data[pos * depth + n] - palette[color_index * depth + n]; + f_diffuse(data + n, width, height, x, y, depth, offset); + } + } + } + memcpy(palette, new_palette, (size_t)(*ncolors * depth)); + } + } else { + if (f_mask) { + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) { + unsigned char copy[max_depth]; + int d; + int val; + + pos = y * width + x; + for (d = 0; d < depth; d ++) { + val = data[pos * depth + d] + f_mask(x, y, d) * 32; + copy[d] = val < 0 ? 0 : val > 255 ? 255 : val; + } + result[pos] = f_lookup(copy, depth, + palette, reqcolor, indextable, complexion); + } + } + } else { + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) { + pos = y * width + x; + color_index = f_lookup(data + (pos * depth), depth, + palette, reqcolor, indextable, complexion); + result[pos] = color_index; + for (n = 0; n < depth; ++n) { + offset = data[pos * depth + n] - palette[color_index * depth + n]; + f_diffuse(data + n, width, height, x, y, depth, offset); + } + } + } + } + *ncolors = reqcolor; + } + + if (cachetable == NULL) { + sixel_allocator_free(allocator, indextable); + } + + status = SIXEL_OK; + +end: + return status; +} + + +void +sixel_quant_free_palette( + unsigned char /* in */ *data, + sixel_allocator_t /* in */ *allocator) +{ + sixel_allocator_free(allocator, data); +} + + +#if HAVE_TESTS +static int +test1(void) +{ + int nret = EXIT_FAILURE; + sample minval[1] = { 1 }; + sample maxval[1] = { 2 }; + unsigned int retval; + + retval = largestByLuminosity(minval, maxval, 1); + if (retval != 0) { + goto error; + } + nret = EXIT_SUCCESS; + +error: + return nret; +} + + +SIXELAPI int +sixel_quant_tests_main(void) +{ + int nret = EXIT_FAILURE; + size_t i; + typedef int (* testcase)(void); + + static testcase const testcases[] = { + test1, + }; + + 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 */ diff --git a/src/3rdparty/sixel/quant.h b/src/3rdparty/sixel/quant.h new file mode 100644 index 0000000000..5ecf28b47e --- /dev/null +++ b/src/3rdparty/sixel/quant.h @@ -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 + +/* 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 */ diff --git a/src/3rdparty/sixel/repo.json b/src/3rdparty/sixel/repo.json new file mode 100644 index 0000000000..481ed232f9 --- /dev/null +++ b/src/3rdparty/sixel/repo.json @@ -0,0 +1,6 @@ +{ + "home": "https://github.com/saitoha/libsixel", + "license": "MIT ( embed in source )", + "version": "1.8.7-r2", + "author": "Hayaki Saito" +} diff --git a/src/3rdparty/sixel/sixel.h b/src/3rdparty/sixel/sixel.h new file mode 100644 index 0000000000..73afc971ab --- /dev/null +++ b/src/3rdparty/sixel/sixel.h @@ -0,0 +1,1173 @@ +/* + * Copyright (c) 2014-2020 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 /* for size_t */ + +#ifndef LIBSIXEL_SIXEL_H +#define LIBSIXEL_SIXEL_H + +/* + * fastfetch: upstream defines this as `__declspec(dllexport)` on Windows, but + * this vendored subset is compiled into libfastfetch statically rather than + * into a standalone DLL. Keeping the attribute would (a) add every libsixel + * symbol to fastfetch's export table and (b) make these declarations disagree + * with the ones in dither.h / output.h / quant.h / allocator.h, which do not + * carry it (clang: -Wdll-attribute-on-redeclaration). + * Re-apply this change when re-syncing with upstream. + */ +#define SIXELAPI + +#define LIBSIXEL_VERSION "1.8.7-r2" +#define LIBSIXEL_ABI_VERSION "1:6:0" + +typedef unsigned char sixel_index_t; + +/* limitations */ +#define SIXEL_OUTPUT_PACKET_SIZE 16384 +#define SIXEL_PALETTE_MIN 2 +#define SIXEL_PALETTE_MAX 256 +#define SIXEL_USE_DEPRECATED_SYMBOLS 1 +#define SIXEL_ALLOCATE_BYTES_MAX 10248UL * 1024UL * 128UL /* up to 128M */ +#define SIXEL_WIDTH_LIMIT 1000000 +#define SIXEL_HEIGHT_LIMIT 1000000 + +/* loader settings */ +#define SIXEL_DEFALUT_GIF_DELAY 1 + +/* return value */ +typedef int SIXELSTATUS; +#define SIXEL_OK 0x0000 /* succeeded */ +#define SIXEL_FALSE 0x1000 /* failed */ + +#define SIXEL_RUNTIME_ERROR (SIXEL_FALSE | 0x0100) /* runtime error */ +#define SIXEL_LOGIC_ERROR (SIXEL_FALSE | 0x0200) /* logic error */ +#define SIXEL_FEATURE_ERROR (SIXEL_FALSE | 0x0300) /* feature not enabled */ +#define SIXEL_LIBC_ERROR (SIXEL_FALSE | 0x0400) /* errors caused by curl */ +#define SIXEL_CURL_ERROR (SIXEL_FALSE | 0x0500) /* errors occures in libc functions */ +#define SIXEL_JPEG_ERROR (SIXEL_FALSE | 0x0600) /* errors occures in libjpeg functions */ +#define SIXEL_PNG_ERROR (SIXEL_FALSE | 0x0700) /* errors occures in libpng functions */ +#define SIXEL_GDK_ERROR (SIXEL_FALSE | 0x0800) /* errors occures in gdk functions */ +#define SIXEL_GD_ERROR (SIXEL_FALSE | 0x0900) /* errors occures in gd functions */ +#define SIXEL_STBI_ERROR (SIXEL_FALSE | 0x0a00) /* errors occures in stb_image functions */ +#define SIXEL_STBIW_ERROR (SIXEL_FALSE | 0x0b00) /* errors occures in stb_image_write functions */ + +#define SIXEL_INTERRUPTED (SIXEL_OK | 0x0001) /* interrupted by a signal */ + +#define SIXEL_BAD_ALLOCATION (SIXEL_RUNTIME_ERROR | 0x0001) /* malloc() failed */ +#define SIXEL_BAD_ARGUMENT (SIXEL_RUNTIME_ERROR | 0x0002) /* bad argument detected */ +#define SIXEL_BAD_INPUT (SIXEL_RUNTIME_ERROR | 0x0003) /* bad input detected */ +#define SIXEL_BAD_INTEGER_OVERFLOW (SIXEL_RUNTIME_ERROR | 0x0004) /* integer overflow */ + +#define SIXEL_NOT_IMPLEMENTED (SIXEL_FEATURE_ERROR | 0x0001) /* feature not implemented */ + +#define SIXEL_SUCCEEDED(status) (((status) & 0x1000) == 0) +#define SIXEL_FAILED(status) (((status) & 0x1000) != 0) + +/* method for finding the largest dimension for splitting, + * and sorting by that component */ +#define SIXEL_LARGE_AUTO 0x0 /* choose automatically the method for finding the largest + dimension */ +#define SIXEL_LARGE_NORM 0x1 /* simply comparing the range in RGB space */ +#define SIXEL_LARGE_LUM 0x2 /* transforming into luminosities before the comparison */ + +/* method for choosing a color from the box */ +#define SIXEL_REP_AUTO 0x0 /* choose automatically the method for selecting + representative color from each box */ +#define SIXEL_REP_CENTER_BOX 0x1 /* choose the center of the box */ +#define SIXEL_REP_AVERAGE_COLORS 0x2 /* choose the average all the color + in the box (specified in Heckbert's paper) */ +#define SIXEL_REP_AVERAGE_PIXELS 0x3 /* choose the average all the pixels in the box */ + +/* method for diffusing */ +#define SIXEL_DIFFUSE_AUTO 0x0 /* choose diffusion type automatically */ +#define SIXEL_DIFFUSE_NONE 0x1 /* don't diffuse */ +#define SIXEL_DIFFUSE_ATKINSON 0x2 /* diffuse with Bill Atkinson's method */ +#define SIXEL_DIFFUSE_FS 0x3 /* diffuse with Floyd-Steinberg method */ +#define SIXEL_DIFFUSE_JAJUNI 0x4 /* diffuse with Jarvis, Judice & Ninke method */ +#define SIXEL_DIFFUSE_STUCKI 0x5 /* diffuse with Stucki's method */ +#define SIXEL_DIFFUSE_BURKES 0x6 /* diffuse with Burkes' method */ +#define SIXEL_DIFFUSE_A_DITHER 0x7 /* positionally stable arithmetic dither */ +#define SIXEL_DIFFUSE_X_DITHER 0x8 /* positionally stable arithmetic xor based dither */ + +/* quality modes */ +#define SIXEL_QUALITY_AUTO 0x0 /* choose quality mode automatically */ +#define SIXEL_QUALITY_HIGH 0x1 /* high quality palette construction */ +#define SIXEL_QUALITY_LOW 0x2 /* low quality palette construction */ +#define SIXEL_QUALITY_FULL 0x3 /* full quality palette construction */ +#define SIXEL_QUALITY_HIGHCOLOR 0x4 /* high color */ + +/* built-in dither */ +#define SIXEL_BUILTIN_MONO_DARK 0x0 /* monochrome terminal with dark background */ +#define SIXEL_BUILTIN_MONO_LIGHT 0x1 /* monochrome terminal with light background */ +#define SIXEL_BUILTIN_XTERM16 0x2 /* xterm 16color */ +#define SIXEL_BUILTIN_XTERM256 0x3 /* xterm 256color */ +#define SIXEL_BUILTIN_VT340_MONO 0x4 /* vt340 monochrome */ +#define SIXEL_BUILTIN_VT340_COLOR 0x5 /* vt340 color */ +#define SIXEL_BUILTIN_G1 0x6 /* 1bit grayscale */ +#define SIXEL_BUILTIN_G2 0x7 /* 2bit grayscale */ +#define SIXEL_BUILTIN_G4 0x8 /* 4bit grayscale */ +#define SIXEL_BUILTIN_G8 0x9 /* 8bit grayscale */ + +/* offset value of pixelFormat */ +#define SIXEL_FORMATTYPE_COLOR (0) +#define SIXEL_FORMATTYPE_GRAYSCALE (1 << 6) +#define SIXEL_FORMATTYPE_PALETTE (1 << 7) + +/* pixelformat type of input image + NOTE: for compatibility, the value of PIXELFORAMT_COLOR_RGB888 must be 3 */ +#define SIXEL_PIXELFORMAT_RGB555 (SIXEL_FORMATTYPE_COLOR | 0x01) /* 15bpp */ +#define SIXEL_PIXELFORMAT_RGB565 (SIXEL_FORMATTYPE_COLOR | 0x02) /* 16bpp */ +#define SIXEL_PIXELFORMAT_RGB888 (SIXEL_FORMATTYPE_COLOR | 0x03) /* 24bpp */ +#define SIXEL_PIXELFORMAT_BGR555 (SIXEL_FORMATTYPE_COLOR | 0x04) /* 15bpp */ +#define SIXEL_PIXELFORMAT_BGR565 (SIXEL_FORMATTYPE_COLOR | 0x05) /* 16bpp */ +#define SIXEL_PIXELFORMAT_BGR888 (SIXEL_FORMATTYPE_COLOR | 0x06) /* 24bpp */ +#define SIXEL_PIXELFORMAT_ARGB8888 (SIXEL_FORMATTYPE_COLOR | 0x10) /* 32bpp */ +#define SIXEL_PIXELFORMAT_RGBA8888 (SIXEL_FORMATTYPE_COLOR | 0x11) /* 32bpp */ +#define SIXEL_PIXELFORMAT_ABGR8888 (SIXEL_FORMATTYPE_COLOR | 0x12) /* 32bpp */ +#define SIXEL_PIXELFORMAT_BGRA8888 (SIXEL_FORMATTYPE_COLOR | 0x13) /* 32bpp */ +#define SIXEL_PIXELFORMAT_G1 (SIXEL_FORMATTYPE_GRAYSCALE | 0x00) /* 1bpp grayscale */ +#define SIXEL_PIXELFORMAT_G2 (SIXEL_FORMATTYPE_GRAYSCALE | 0x01) /* 2bpp grayscale */ +#define SIXEL_PIXELFORMAT_G4 (SIXEL_FORMATTYPE_GRAYSCALE | 0x02) /* 4bpp grayscale */ +#define SIXEL_PIXELFORMAT_G8 (SIXEL_FORMATTYPE_GRAYSCALE | 0x03) /* 8bpp grayscale */ +#define SIXEL_PIXELFORMAT_AG88 (SIXEL_FORMATTYPE_GRAYSCALE | 0x13) /* 16bpp gray+alpha */ +#define SIXEL_PIXELFORMAT_GA88 (SIXEL_FORMATTYPE_GRAYSCALE | 0x23) /* 16bpp gray+alpha */ +#define SIXEL_PIXELFORMAT_PAL1 (SIXEL_FORMATTYPE_PALETTE | 0x00) /* 1bpp palette */ +#define SIXEL_PIXELFORMAT_PAL2 (SIXEL_FORMATTYPE_PALETTE | 0x01) /* 2bpp palette */ +#define SIXEL_PIXELFORMAT_PAL4 (SIXEL_FORMATTYPE_PALETTE | 0x02) /* 4bpp palette */ +#define SIXEL_PIXELFORMAT_PAL8 (SIXEL_FORMATTYPE_PALETTE | 0x03) /* 8bpp palette */ + +/* palette type */ +#define SIXEL_PALETTETYPE_AUTO 0 /* choose palette type automatically */ +#define SIXEL_PALETTETYPE_HLS 1 /* HLS colorspace */ +#define SIXEL_PALETTETYPE_RGB 2 /* RGB colorspace */ + +/* policies of SIXEL encoding */ +#define SIXEL_ENCODEPOLICY_AUTO 0 /* choose encoding policy automatically */ +#define SIXEL_ENCODEPOLICY_FAST 1 /* encode as fast as possible */ +#define SIXEL_ENCODEPOLICY_SIZE 2 /* encode to as small sixel sequence as possible */ + +/* method for re-sampling */ +#define SIXEL_RES_NEAREST 0 /* Use nearest neighbor method */ +#define SIXEL_RES_GAUSSIAN 1 /* Use guaussian filter */ +#define SIXEL_RES_HANNING 2 /* Use hanning filter */ +#define SIXEL_RES_HAMMING 3 /* Use hamming filter */ +#define SIXEL_RES_BILINEAR 4 /* Use bilinear filter */ +#define SIXEL_RES_WELSH 5 /* Use welsh filter */ +#define SIXEL_RES_BICUBIC 6 /* Use bicubic filter */ +#define SIXEL_RES_LANCZOS2 7 /* Use lanczos-2 filter */ +#define SIXEL_RES_LANCZOS3 8 /* Use lanczos-3 filter */ +#define SIXEL_RES_LANCZOS4 9 /* Use lanczos-4 filter */ + +/* image format */ +#define SIXEL_FORMAT_GIF 0x0 /* read only */ +#define SIXEL_FORMAT_PNG 0x1 /* read/write */ +#define SIXEL_FORMAT_BMP 0x2 /* read only */ +#define SIXEL_FORMAT_JPG 0x3 /* read only */ +#define SIXEL_FORMAT_TGA 0x4 /* read only */ +#define SIXEL_FORMAT_WBMP 0x5 /* read only with --with-gd configure option */ +#define SIXEL_FORMAT_TIFF 0x6 /* read only */ +#define SIXEL_FORMAT_SIXEL 0x7 /* read only */ +#define SIXEL_FORMAT_PNM 0x8 /* read only */ +#define SIXEL_FORMAT_GD2 0x9 /* read only with --with-gd configure option */ +#define SIXEL_FORMAT_PSD 0xa /* read only */ +#define SIXEL_FORMAT_HDR 0xb /* read only */ + +/* loop mode */ +#define SIXEL_LOOP_AUTO 0 /* honer the setting of GIF header */ +#define SIXEL_LOOP_FORCE 1 /* always enable loop */ +#define SIXEL_LOOP_DISABLE 2 /* always disable loop */ + +/* setopt flags */ +#define SIXEL_OPTFLAG_INPUT ('i') /* -i, --input: specify input file name. */ +#define SIXEL_OPTFLAG_OUTPUT ('o') /* -o, --output: specify output file name. */ +#define SIXEL_OPTFLAG_OUTFILE ('o') /* -o, --outfile: specify output file name. */ +#define SIXEL_OPTFLAG_7BIT_MODE ('7') /* -7, --7bit-mode: for 7bit terminals or printers (default) */ +#define SIXEL_OPTFLAG_8BIT_MODE ('8') /* -8, --8bit-mode: for 8bit terminals or printers */ +#define SIXEL_OPTFLAG_HAS_GRI_ARG_LIMIT ('R') /* -R, --gri-limit: limit arguments of DECGRI('!') to 255 */ +#define SIXEL_OPTFLAG_COLORS ('p') /* -p COLORS, --colors=COLORS: specify number of colors */ +#define SIXEL_OPTFLAG_MAPFILE ('m') /* -m FILE, --mapfile=FILE: specify set of colors */ +#define SIXEL_OPTFLAG_MONOCHROME ('e') /* -e, --monochrome: output monochrome sixel image */ +#define SIXEL_OPTFLAG_INSECURE ('k') /* -k, --insecure: allow to connect to SSL sites without certs */ +#define SIXEL_OPTFLAG_INVERT ('i') /* -i, --invert: assume the terminal background color */ +#define SIXEL_OPTFLAG_HIGH_COLOR ('I') /* -I, --high-color: output 15bpp sixel image */ +#define SIXEL_OPTFLAG_USE_MACRO ('u') /* -u, --use-macro: use DECDMAC and DEVINVM sequences */ +#define SIXEL_OPTFLAG_MACRO_NUMBER ('n') /* -n MACRONO, --macro-number=MACRONO: + specify macro register number */ +#define SIXEL_OPTFLAG_COMPLEXION_SCORE ('C') /* -C COMPLEXIONSCORE, --complexion-score=COMPLEXIONSCORE: + specify an number argument for the score of + complexion correction. */ +#define SIXEL_OPTFLAG_IGNORE_DELAY ('g') /* -g, --ignore-delay: render GIF animation without delay */ +#define SIXEL_OPTFLAG_STATIC ('S') /* -S, --static: render animated GIF as a static image */ +#define SIXEL_OPTFLAG_DIFFUSION ('d') /* -d DIFFUSIONTYPE, --diffusion=DIFFUSIONTYPE: + choose diffusion method which used with -p option. + DIFFUSIONTYPE is one of them: + auto -> choose diffusion type + automatically (default) + none -> do not diffuse + fs -> Floyd-Steinberg method + atkinson -> Bill Atkinson's method + jajuni -> Jarvis, Judice & Ninke + stucki -> Stucki's method + burkes -> Burkes' method + a_dither -> positionally stable + arithmetic dither + a_dither -> positionally stable + arithmetic xor based dither + */ +#define SIXEL_OPTFLAG_FIND_LARGEST ('f') /* -f FINDTYPE, --find-largest=FINDTYPE: + choose method for finding the largest + dimension of median cut boxes for + splitting, make sense only when -p + option (color reduction) is + specified + FINDTYPE is one of them: + auto -> choose finding method + automatically (default) + norm -> simply comparing the + range in RGB space + lum -> transforming into + luminosities before the + comparison + */ +#define SIXEL_OPTFLAG_SELECT_COLOR ('s') /* -s SELECTTYPE, --select-color=SELECTTYPE + choose the method for selecting + representative color from each + median-cut box, make sense only + when -p option (color reduction) is + specified + SELECTTYPE is one of them: + auto -> choose selecting + method automatically + (default) + center -> choose the center of + the box + average -> calculate the color + average into the box + histogram -> similar with average + but considers color + histogram + */ +#define SIXEL_OPTFLAG_CROP ('c') /* -c REGION, --crop=REGION: + crop source image to fit the + specified geometry. REGION should + be formatted as '%dx%d+%d+%d' + */ +#define SIXEL_OPTFLAG_WIDTH ('w') /* -w WIDTH, --width=WIDTH: + resize image to specified width + WIDTH is represented by the + following syntax + auto -> preserving aspect + ratio (default) + % -> scale width with + given percentage + -> scale width with + pixel counts + px -> scale width with + pixel counts + */ +#define SIXEL_OPTFLAG_HEIGHT ('h') /* -h HEIGHT, --height=HEIGHT: + resize image to specified height + HEIGHT is represented by the + following syntax + auto -> preserving aspect + ratio (default) + % -> scale height with + given percentage + -> scale height with + pixel counts + px -> scale height with + pixel counts + */ +#define SIXEL_OPTFLAG_RESAMPLING ('r') /* -r RESAMPLINGTYPE, --resampling=RESAMPLINGTYPE: + choose resampling filter used + with -w or -h option (scaling) + RESAMPLINGTYPE is one of them: + nearest -> Nearest-Neighbor + method + gaussian -> Gaussian filter + hanning -> Hanning filter + hamming -> Hamming filter + bilinear -> Bilinear filter + (default) + welsh -> Welsh filter + bicubic -> Bicubic filter + lanczos2 -> Lanczos-2 filter + lanczos3 -> Lanczos-3 filter + lanczos4 -> Lanczos-4 filter + */ +#define SIXEL_OPTFLAG_QUALITY ('q') /* -q QUALITYMODE, --quality=QUALITYMODE: + select quality of color + quanlization. + auto -> decide quality mode + automatically (default) + low -> low quality and high + speed mode + high -> high quality and low + speed mode + full -> full quality and careful + speed mode + */ +#define SIXEL_OPTFLAG_LOOPMODE ('l') /* -l LOOPMODE, --loop-control=LOOPMODE: + select loop control mode for GIF + animation. + auto -> honor the setting of + GIF header (default) + force -> always enable loop + disable -> always disable loop + */ +#define SIXEL_OPTFLAG_PALETTE_TYPE ('t') /* -t PALETTETYPE, --palette-type=PALETTETYPE: + select palette color space type + auto -> choose palette type + automatically (default) + hls -> use HLS color space + rgb -> use RGB color space + */ +#define SIXEL_OPTFLAG_BUILTIN_PALETTE ('b') /* -b BUILTINPALETTE, --builtin-palette=BUILTINPALETTE: + select built-in palette type + xterm16 -> X default 16 color map + xterm256 -> X default 256 color map + vt340mono -> VT340 monochrome map + vt340color -> VT340 color map + gray1 -> 1bit grayscale map + gray2 -> 2bit grayscale map + gray4 -> 4bit grayscale map + gray8 -> 8bit grayscale map + */ +#define SIXEL_OPTFLAG_ENCODE_POLICY ('E') /* -E ENCODEPOLICY, --encode-policy=ENCODEPOLICY: + select encoding policy + auto -> choose encoding policy + automatically (default) + fast -> encode as fast as possible + size -> encode to as small sixel + sequence as possible + */ +#define SIXEL_OPTFLAG_BGCOLOR ('B') /* -B BGCOLOR, --bgcolor=BGCOLOR: + specify background color + BGCOLOR is represented by the + following syntax + #rgb + #rrggbb + #rrrgggbbb + #rrrrggggbbbb + rgb:r/g/b + rgb:rr/gg/bb + rgb:rrr/ggg/bbb + rgb:rrrr/gggg/bbbb + */ +#define SIXEL_OPTFLAG_PENETRATE ('P') /* -P, --penetrate: + penetrate GNU Screen using DCS + pass-through sequence */ +#define SIXEL_OPTFLAG_PIPE_MODE ('D') /* -D, --pipe-mode: (deprecated) + read source images from stdin continuously */ +#define SIXEL_OPTFLAG_VERBOSE ('v') /* -v, --verbose: show debugging info */ +#define SIXEL_OPTFLAG_VERSION ('V') /* -V, --version: show version and license info */ +#define SIXEL_OPTFLAG_HELP ('H') /* -H, --help: show this help */ + +#if SIXEL_USE_DEPRECATED_SYMBOLS +/* output character size */ +enum characterSize { + CSIZE_7BIT = 0, /* 7bit character */ + CSIZE_8BIT = 1 /* 8bit character */ +}; + +/* method for finding the largest dimension for splitting, + * and sorting by that component */ +enum methodForLargest { + LARGE_AUTO = 0, /* choose automatically the method for finding the largest + dimension */ + LARGE_NORM = 1, /* simply comparing the range in RGB space */ + LARGE_LUM = 2 /* transforming into luminosities before the comparison */ +}; + +/* method for choosing a color from the box */ +enum methodForRep { + REP_AUTO = 0, /* choose automatically the method for selecting + representative color from each box */ + REP_CENTER_BOX = 1, /* choose the center of the box */ + REP_AVERAGE_COLORS = 2, /* choose the average all the color + in the box (specified in Heckbert's paper) */ + REP_AVERAGE_PIXELS = 3 /* choose the average all the pixels in the box */ +}; + +/* method for diffusing */ +enum methodForDiffuse { + DIFFUSE_AUTO = 0, /* choose diffusion type automatically */ + DIFFUSE_NONE = 1, /* don't diffuse */ + DIFFUSE_ATKINSON = 2, /* diffuse with Bill Atkinson's method */ + DIFFUSE_FS = 3, /* diffuse with Floyd-Steinberg method */ + DIFFUSE_JAJUNI = 4, /* diffuse with Jarvis, Judice & Ninke method */ + DIFFUSE_STUCKI = 5, /* diffuse with Stucki's method */ + DIFFUSE_BURKES = 6, /* diffuse with Burkes' method */ + DIFFUSE_A_DITHER = 7, /* positionally stable arithmetic dither */ + DIFFUSE_X_DITHER = 8 /* positionally stable arithmetic xor based dither */ +}; + +/* quality modes */ +enum qualityMode { + QUALITY_AUTO = 0, /* choose quality mode automatically */ + QUALITY_HIGH = 1, /* high quality palette construction */ + QUALITY_LOW = 2, /* low quality palette construction */ + QUALITY_FULL = 3, /* full quality palette construction */ + QUALITY_HIGHCOLOR = 4 /* high color */ +}; + +/* built-in dither */ +enum builtinDither { + BUILTIN_MONO_DARK = 0, /* monochrome terminal with dark background */ + BUILTIN_MONO_LIGHT = 1, /* monochrome terminal with dark background */ + BUILTIN_XTERM16 = 2, /* xterm 16color */ + BUILTIN_XTERM256 = 3, /* xterm 256color */ + BUILTIN_VT340_MONO = 4, /* vt340 monochrome */ + BUILTIN_VT340_COLOR = 5 /* vt340 color */ +}; + +/* offset value of enum pixelFormat */ +enum formatType { + FORMATTYPE_COLOR = 0, + FORMATTYPE_GRAYSCALE = 1 << 6, + FORMATTYPE_PALETTE = 1 << 7 +}; + +/* pixelformat type of input image + NOTE: for compatibility, the value of PIXELFORAMT_COLOR_RGB888 must be 3 */ +enum pixelFormat { + PIXELFORMAT_RGB555 = FORMATTYPE_COLOR | 0x01, /* 15bpp */ + PIXELFORMAT_RGB565 = FORMATTYPE_COLOR | 0x02, /* 16bpp */ + PIXELFORMAT_RGB888 = FORMATTYPE_COLOR | 0x03, /* 24bpp */ + PIXELFORMAT_BGR555 = FORMATTYPE_COLOR | 0x04, /* 15bpp */ + PIXELFORMAT_BGR565 = FORMATTYPE_COLOR | 0x05, /* 16bpp */ + PIXELFORMAT_BGR888 = FORMATTYPE_COLOR | 0x06, /* 24bpp */ + PIXELFORMAT_ARGB8888 = FORMATTYPE_COLOR | 0x10, /* 32bpp */ + PIXELFORMAT_RGBA8888 = FORMATTYPE_COLOR | 0x11, /* 32bpp */ + PIXELFORMAT_G1 = FORMATTYPE_GRAYSCALE | 0x00, /* 1bpp grayscale */ + PIXELFORMAT_G2 = FORMATTYPE_GRAYSCALE | 0x01, /* 2bpp grayscale */ + PIXELFORMAT_G4 = FORMATTYPE_GRAYSCALE | 0x02, /* 4bpp grayscale */ + PIXELFORMAT_G8 = FORMATTYPE_GRAYSCALE | 0x03, /* 8bpp grayscale */ + PIXELFORMAT_AG88 = FORMATTYPE_GRAYSCALE | 0x13, /* 16bpp gray+alpha */ + PIXELFORMAT_GA88 = FORMATTYPE_GRAYSCALE | 0x23, /* 16bpp gray+alpha */ + PIXELFORMAT_PAL1 = FORMATTYPE_PALETTE | 0x00, /* 1bpp palette */ + PIXELFORMAT_PAL2 = FORMATTYPE_PALETTE | 0x01, /* 2bpp palette */ + PIXELFORMAT_PAL4 = FORMATTYPE_PALETTE | 0x02, /* 4bpp palette */ + PIXELFORMAT_PAL8 = FORMATTYPE_PALETTE | 0x03 /* 8bpp palette */ +}; + +/* palette type */ +enum paletteType { + PALETTETYPE_AUTO = 0, /* choose palette type automatically */ + PALETTETYPE_HLS = 1, /* HLS colorspace */ + PALETTETYPE_RGB = 2 /* RGB colorspace */ +}; + +/* policies of SIXEL encoding */ +enum encodePolicy { + ENCODEPOLICY_AUTO = 0, /* choose encoding policy automatically */ + ENCODEPOLICY_FAST = 1, /* encode as fast as possible */ + ENCODEPOLICY_SIZE = 2 /* encode to as small sixel sequence as possible */ +}; + +/* method for re-sampling */ +enum methodForResampling { + RES_NEAREST = 0, /* Use nearest neighbor method */ + RES_GAUSSIAN = 1, /* Use guaussian filter */ + RES_HANNING = 2, /* Use hanning filter */ + RES_HAMMING = 3, /* Use hamming filter */ + RES_BILINEAR = 4, /* Use bilinear filter */ + RES_WELSH = 5, /* Use welsh filter */ + RES_BICUBIC = 6, /* Use bicubic filter */ + RES_LANCZOS2 = 7, /* Use lanczos-2 filter */ + RES_LANCZOS3 = 8, /* Use lanczos-3 filter */ + RES_LANCZOS4 = 9 /* Use lanczos-4 filter */ +}; +#endif + +typedef void *(* sixel_malloc_t)(size_t); +typedef void *(* sixel_calloc_t)(size_t, size_t); +typedef void *(* sixel_realloc_t)(void *, size_t); +typedef void (* sixel_free_t)(void *); + +struct sixel_allocator; +typedef struct sixel_allocator sixel_allocator_t; + +#ifdef __cplusplus +extern "C" { +#endif + +/* create allocator object */ +SIXELAPI 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 */ + +/* increase reference count of allocator object (thread-unsafe) */ +SIXELAPI void +sixel_allocator_ref( + sixel_allocator_t /* in */ *allocator); /* allocator object to be + increment reference counter */ + +/* decrease reference count of output context object (thread-unsafe) */ +SIXELAPI void +sixel_allocator_unref(sixel_allocator_t *allocator); + +/* call custom malloc() */ +SIXELAPI void * +sixel_allocator_malloc( + sixel_allocator_t /* in */ *allocator, /* allocator object */ + size_t /* in */ n); /* allocation size */ + +/* call custom calloc() */ +SIXELAPI void * +sixel_allocator_calloc( + sixel_allocator_t /* in */ *allocator, /* allocator object */ + size_t /* in */ nelm, /* allocation size */ + size_t /* in */ elsize); /* allocation size */ + +/* 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 */ + +/* call custom free() */ +SIXELAPI void +sixel_allocator_free( + sixel_allocator_t /* in */ *allocator, /* allocator object */ + void /* in */ *p); /* existing buffer to be freed */ + +#ifdef HAVE_TESTS +extern volatile int sixel_debug_malloc_counter; + +void * +sixel_bad_malloc(size_t size); + +void * +sixel_bad_calloc(size_t count, size_t size); + +void * +sixel_bad_realloc(void *ptr, size_t size); +#endif /* HAVE_TESTS */ + +#ifdef __cplusplus +} +#endif + +/* output context manipulation API */ + +struct sixel_output; +typedef struct sixel_output sixel_output_t; +typedef int (* sixel_write_function)(char *data, int size, void *priv); + +#ifdef __cplusplus +extern "C" { +#endif + +/* create new output context object */ +SIXELAPI SIXELSTATUS +sixel_output_new( + sixel_output_t /* out */ **output, /* output object to be created */ + sixel_write_function /* in */ fn_write, /* callback for output sixel */ + void /* in */ *priv, /* private data given as + 3rd argument of fn_write */ + sixel_allocator_t /* in */ *allocator); /* allocator, null if you use + default allocator */ + +/* deprecated: create an output object */ +SIXELAPI __attribute__((deprecated)) sixel_output_t * +sixel_output_create( + sixel_write_function /* in */ fn_write, /* callback for output sixel */ + void /* in */ *priv); /* private data given as + 3rd argument of fn_write */ +/* destroy output context object */ +SIXELAPI void +sixel_output_destroy(sixel_output_t /* in */ *output); /* output context */ + +/* increase reference count of output context object (thread-unsafe) */ +SIXELAPI void +sixel_output_ref(sixel_output_t /* in */ *output); /* output context */ + +/* decrease reference count of output context object (thread-unsafe) */ +SIXELAPI void +sixel_output_unref(sixel_output_t /* in */ *output); /* output context */ + +/* get 8bit output mode which indicates whether it uses C1 control characters */ +SIXELAPI int +sixel_output_get_8bit_availability( + sixel_output_t /* in */ *output); /* output context */ + +/* set 8bit output mode state */ +SIXELAPI void +sixel_output_set_8bit_availability( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ availability); /* 0: do not use 8bit characters + 1: use 8bit characters */ + +/* 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 */ + +/* set GNU Screen penetration feature enable or disable */ +SIXELAPI void +sixel_output_set_penetrate_multiplexer( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ penetrate); /* 0: penetrate GNU Screen + 1: do not penetrate GNU Screen */ + +/* set whether we skip DCS envelope */ +SIXELAPI void +sixel_output_set_skip_dcs_envelope( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ skip); /* 0: output DCS envelope + 1: do not output DCS envelope */ + +/* set palette type: RGB or HLS */ +SIXELAPI void +sixel_output_set_palette_type( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ palettetype); /* PALETTETYPE_RGB: RGB palette + PALETTETYPE_HLS: HLS palette */ + +/* set encodeing policy: auto, fast or size */ +SIXELAPI void +sixel_output_set_encode_policy( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ encode_policy); + + +#ifdef __cplusplus +} +#endif + + +/* color quantization API */ + +/* handle type of dither context object */ +struct sixel_dither; +typedef struct sixel_dither sixel_dither_t; + +#ifdef __cplusplus +extern "C" { +#endif + +/* 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 */ + +/* create dither context object */ +SIXELAPI __attribute__((deprecated)) sixel_dither_t * +sixel_dither_create(int /* in */ ncolors); /* number of colors */ + +/* get built-in dither context object */ +SIXELAPI sixel_dither_t * +sixel_dither_get(int builtin_dither); /* ID of built-in dither object */ + +/* destroy dither context object */ +SIXELAPI void +sixel_dither_destroy(sixel_dither_t *dither); /* dither context object */ + +/* increase reference count of dither context object (thread-unsafe) */ +SIXELAPI void +sixel_dither_ref(sixel_dither_t *dither); /* dither context object */ + +/* decrease reference count of dither context object (thread-unsafe) */ +SIXELAPI void +sixel_dither_unref(sixel_dither_t *dither); /* dither context object */ + +/* initialize internal palette from specified pixel buffer */ +SIXELAPI SIXELSTATUS +sixel_dither_initialize( + sixel_dither_t *dither, /* dither context object */ + unsigned char /* in */ *data, /* sample image */ + int /* in */ width, /* image width */ + int /* in */ height, /* image height */ + int /* in */ pixelformat, /* one of enum pixelFormat */ + int /* in */ method_for_largest, /* method for finding the largest dimension */ + int /* in */ method_for_rep, /* method for choosing a color from the box */ + int /* in */ quality_mode); /* quality of histogram processing */ + +/* set diffusion type, choose from enum methodForDiffuse */ +SIXELAPI void +sixel_dither_set_diffusion_type( + sixel_dither_t /* in */ *dither, /* dither context object */ + int /* in */ method_for_diffuse); /* one of enum methodForDiffuse */ + +/* get number of palette colors */ +SIXELAPI int +sixel_dither_get_num_of_palette_colors( + sixel_dither_t /* in */ *dither); /* dither context object */ + +/* get number of histogram colors */ +SIXELAPI int +sixel_dither_get_num_of_histogram_colors( + sixel_dither_t /* in */ *dither); /* dither context object */ + +SIXELAPI __attribute__((deprecated)) int /* typoed! remains for compatibility. */ +sixel_dither_get_num_of_histgram_colors( + sixel_dither_t /* in */ *dither); /* dither context object */ + +/* get palette */ +SIXELAPI unsigned char * +sixel_dither_get_palette( + sixel_dither_t /* in */ *dither); /* dither context object */ + +/* set palette */ +SIXELAPI void +sixel_dither_set_palette( + sixel_dither_t /* in */ *dither, /* dither context object */ + unsigned char /* in */ *palette); + +/* 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) */ + +/* 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(default) + 1: do not output palette section */ +/* 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 */ +/* set pixelformat */ +SIXELAPI void +sixel_dither_set_pixelformat( + sixel_dither_t /* in */ *dither, /* dither context object */ + int /* in */ pixelformat); /* one of enum pixelFormat */ + +/* set transparent */ +SIXELAPI void +sixel_dither_set_transparent( + sixel_dither_t /* in */ *dither, /* dither context object */ + int /* in */ transparent); /* transparent color index */ + +#ifdef __cplusplus +} +#endif + +/* converter API */ + +typedef void * (* sixel_allocator_function)(size_t size); + +#ifdef __cplusplus +extern "C" { +#endif + +/* convert pixels into sixel format and write it to output context */ +SIXELAPI SIXELSTATUS +sixel_encode( + unsigned char /* in */ *pixels, /* pixel bytes */ + int /* in */ width, /* image width */ + int /* in */ height, /* image height */ + int /* in */ depth, /* color depth: now unused */ + sixel_dither_t /* in */ *dither, /* dither context */ + sixel_output_t /* in */ *context); /* output context */ + +/* convert sixel data into indexed pixel bytes and palette data */ +SIXELAPI SIXELSTATUS +sixel_decode_raw( + unsigned char /* in */ *p, /* sixel bytes */ + int /* in */ len, /* size of sixel bytes */ + unsigned char /* out */ **pixels, /* decoded pixels */ + int /* out */ *pwidth, /* image width */ + int /* out */ *pheight, /* image height */ + unsigned char /* out */ **palette, /* ARGB palette */ + int /* out */ *ncolors, /* palette size (<= 256) */ + sixel_allocator_t /* in */ *allocator); /* allocator object or null */ + +SIXELAPI __attribute__((deprecated)) SIXELSTATUS +sixel_decode( + unsigned char /* in */ *sixels, /* sixel bytes */ + int /* in */ size, /* size of sixel bytes */ + unsigned char /* out */ **pixels, /* decoded pixels */ + int /* out */ *pwidth, /* image width */ + int /* out */ *pheight, /* image height */ + unsigned char /* out */ **palette, /* RGBA palette */ + int /* out */ *ncolors, /* palette size (<= 256) */ + sixel_allocator_function /* in */ fn_malloc); /* malloc function */ + +#ifdef __cplusplus +} +#endif + +/* helper API */ + +#ifdef __cplusplus +extern "C" { +#endif + +SIXELAPI void +sixel_helper_set_additional_message( + const char /* in */ *message /* error message */ +); + +SIXELAPI char const * +sixel_helper_get_additional_message(void); + +/* convert error status code int formatted string */ +SIXELAPI char const * +sixel_helper_format_error( + SIXELSTATUS /* in */ status /* status code */ +); + +/* compute pixel depth from pixelformat */ +SIXELAPI int +sixel_helper_compute_depth( + int /* in */ pixelformat /* one of enum pixelFormat */ +); + +/* convert pixelFormat into PIXELFORMAT_RGB888 */ +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 */ +); + +/* scale image to specified size */ +SIXELAPI SIXELSTATUS +sixel_helper_scale_image( + unsigned char /* out */ *dst, /* destination buffer */ + unsigned char const /* in */ *src, /* source image data */ + int /* in */ srcw, /* source image width */ + int /* in */ srch, /* source image height */ + int /* in */ pixelformat, /* one of enum pixelFormat */ + int /* in */ dstw, /* destination image width */ + int /* in */ dsth, /* destination image height */ + int /* in */ method_for_resampling, /* one of methodForResampling */ + sixel_allocator_t /* in */ *allocator /* allocator object */ +); + +#ifdef __cplusplus +} +#endif + + +/* image loader/writer API */ + +#if SIXEL_USE_DEPRECATED_SYMBOLS +enum imageFormat { + FORMAT_GIF = 0, /* read only */ + FORMAT_PNG = 1, /* read/write */ + FORMAT_BMP = 2, /* read only */ + FORMAT_JPG = 3, /* read only */ + FORMAT_TGA = 4, /* read only */ + FORMAT_WBMP = 5, /* read only with --with-gd configure option */ + FORMAT_TIFF = 6, /* read only */ + FORMAT_SIXEL = 7, /* read only */ + FORMAT_PNM = 8, /* read only */ + FORMAT_GD2 = 9, /* read only with --with-gd configure option */ + FORMAT_PSD = 10, /* read only */ + FORMAT_HDR = 11 /* read only */ +}; + +/* loop mode */ +enum loopControl { + LOOP_AUTO = 0, /* honer the setting of GIF header */ + LOOP_FORCE = 1, /* always enable loop */ + LOOP_DISABLE = 2 /* always disable loop */ +}; +#endif + +/* handle type of dither context object */ +struct sixel_frame; +typedef struct sixel_frame sixel_frame_t; + +#ifdef __cplusplus +extern "C" { +#endif + +/* constructor of frame object */ +SIXELAPI SIXELSTATUS +sixel_frame_new( + sixel_frame_t /* out */ **ppframe, /* frame object to be created */ + sixel_allocator_t /* in */ *allocator); /* allocator, null if you use + default allocator */ +/* deprecated version of sixel_frame_new() */ +SIXELAPI __attribute__((deprecated)) sixel_frame_t * +sixel_frame_create(void); + +/* increase reference count of frame object (thread-unsafe) */ +SIXELAPI void +sixel_frame_ref(sixel_frame_t /* in */ *frame); + +/* decrease reference count of frame object (thread-unsafe) */ +SIXELAPI void +sixel_frame_unref(sixel_frame_t /* in */ *frame); + +/* initialize frame object with a pixel buffer */ +SIXELAPI SIXELSTATUS +sixel_frame_init( + sixel_frame_t /* in */ *frame, /* frame object to be initialize */ + unsigned char /* in */ *pixels, /* pixel buffer */ + int /* in */ width, /* pixel width of buffer */ + int /* in */ height, /* pixel height of buffer */ + int /* in */ pixelformat, /* pixelformat of buffer */ + unsigned char /* in */ *palette, /* palette for buffer or NULL */ + int /* in */ ncolors /* number of palette colors or (-1) */ +); + +/* get pixels */ +SIXELAPI unsigned char * +sixel_frame_get_pixels(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get palette */ +SIXELAPI unsigned char * +sixel_frame_get_palette(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get width */ +SIXELAPI int +sixel_frame_get_width(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get height */ +SIXELAPI int +sixel_frame_get_height(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get ncolors */ +SIXELAPI int +sixel_frame_get_ncolors(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get pixelformat */ +SIXELAPI int +sixel_frame_get_pixelformat(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get transparent */ +SIXELAPI int +sixel_frame_get_transparent(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get transparent */ +SIXELAPI int +sixel_frame_get_multiframe(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get delay */ +SIXELAPI int +sixel_frame_get_delay(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get frame no */ +SIXELAPI int +sixel_frame_get_frame_no(sixel_frame_t /* in */ *frame); /* frame object */ + +/* get loop no */ +SIXELAPI int +sixel_frame_get_loop_no(sixel_frame_t /* in */ *frame); /* frame object */ + +/* strip alpha from RGBA/ARGB formatted pixbuf */ +SIXELAPI int +sixel_frame_strip_alpha( + sixel_frame_t /* in */ *frame, + unsigned char /* in */ *bgcolor); + +/* resize a frame to given size with specified resampling filter */ +SIXELAPI SIXELSTATUS +sixel_frame_resize( + sixel_frame_t /* in */ *frame, + int /* in */ width, + int /* in */ height, + int /* in */ method_for_resampling); + +/* clip frame */ +SIXELAPI SIXELSTATUS +sixel_frame_clip( + sixel_frame_t /* in */ *frame, + int /* in */ x, + int /* in */ y, + int /* in */ width, + int /* in */ height); + +typedef SIXELSTATUS (* sixel_load_image_function)( + sixel_frame_t /* in */ *frame, + void /* in/out */ *context); + +/* Note: this function returns SIXEL_OK without calling FN_LOAD when the file + content is empty or 1-byte LF. This implies an assumption that CONTEXT is + initialized to be the default value for the empty file before calling this + function. If it is not the case, the caller needs to properly detect it and + handle this, or otherwise, CONTEXT can be used uninitialized in subsequent + codes. */ +SIXELAPI SIXELSTATUS +sixel_helper_load_image_file( + char const /* in */ *filename, /* source file name */ + int /* in */ fstatic, /* whether to extract static image */ + int /* in */ fuse_palette, /* whether to use paletted image */ + int /* in */ reqcolors, /* requested number of colors */ + unsigned char /* in */ *bgcolor, /* background color */ + int /* in */ loop_control, /* one of enum loopControl */ + sixel_load_image_function /* in */ fn_load, /* callback */ + int /* in */ finsecure, /* true if do not verify SSL */ + int const /* in */ *cancel_flag, /* cancel flag */ + void /* in/out */ *context, /* private data for callback */ + sixel_allocator_t /* in */ *allocator); /* allocator object */ + +/* write image to file */ +SIXELAPI SIXELSTATUS +sixel_helper_write_image_file( + unsigned char /* in */ *data, /* source pixel data */ + int /* in */ width, /* source data width */ + int /* in */ height, /* source data height */ + unsigned char /* in */ *palette, /* palette of source data */ + int /* in */ pixelformat, /* source pixelFormat */ + char const /* in */ *filename, /* destination filename */ + int /* in */ imageformat, /* one of enum imageformat */ + sixel_allocator_t /* in */ *allocator); /* allocator object */ + +#ifdef __cplusplus +} +#endif + + +/* easy encoder API */ + +/* handle type of dither context object */ +struct sixel_encoder; +typedef struct sixel_encoder sixel_encoder_t; + +#ifdef __cplusplus +extern "C" { +#endif + +/* create encoder object */ +SIXELAPI SIXELSTATUS +sixel_encoder_new( + sixel_encoder_t /* out */ **ppencoder, /* encoder object to be created */ + sixel_allocator_t /* in */ *allocator); /* allocator, null if you use + default allocator */ + +/* deprecated version of sixel_decoder_new() */ +SIXELAPI __attribute__((deprecated)) sixel_encoder_t * +sixel_encoder_create(void); + +/* increase reference count of encoder object (thread-unsafe) */ +SIXELAPI void +sixel_encoder_ref(sixel_encoder_t /* in */ *encoder); + +/* decrease reference count of encoder object (thread-unsafe) */ +SIXELAPI void +sixel_encoder_unref(sixel_encoder_t /* in */ *encoder); + +/* set cancel state flag to encoder object */ +SIXELAPI SIXELSTATUS +sixel_encoder_set_cancel_flag( + sixel_encoder_t /* in */ *encoder, + int /* in */ *cancel_flag); + +/* set an option flag to encoder object */ +SIXELAPI SIXELSTATUS +sixel_encoder_setopt( + sixel_encoder_t /* in */ *encoder, + int /* in */ arg, + char const /* in */ *optarg); + +/* load source data from specified file and encode it to SIXEL format */ +SIXELAPI SIXELSTATUS +sixel_encoder_encode( + sixel_encoder_t /* in */ *encoder, + char const /* in */ *filename); + +/* encode specified pixel data to SIXEL format + * output to encoder->outfd */ +SIXELAPI SIXELSTATUS +sixel_encoder_encode_bytes( + sixel_encoder_t /* in */ *encoder, + unsigned char /* in */ *bytes, + int /* in */ width, + int /* in */ height, + int /* in */ pixelformat, + unsigned char /* in */ *palette, + int /* in */ ncolors); + +#ifdef __cplusplus +} +#endif + + +/* easy encoder API */ + +/* handle type of dither context object */ +struct sixel_decoder; +typedef struct sixel_decoder sixel_decoder_t; + +#ifdef __cplusplus +extern "C" { +#endif + +/* create decoder object */ +SIXELAPI SIXELSTATUS +sixel_decoder_new( + sixel_decoder_t /* out */ **ppdecoder, /* decoder object to be created */ + sixel_allocator_t /* in */ *allocator); /* allocator, null if you use + default allocator */ + +/* deprecated version of sixel_decoder_new() */ +SIXELAPI __attribute__((deprecated)) sixel_decoder_t * +sixel_decoder_create(void); + +/* increase reference count of decoder object (thread-unsafe) */ +SIXELAPI void +sixel_decoder_ref(sixel_decoder_t *decoder); + +/* decrease reference count of decoder object (thread-unsafe) */ +SIXELAPI void +sixel_decoder_unref(sixel_decoder_t *decoder); + +/* set an option flag to decoder object */ +SIXELAPI SIXELSTATUS +sixel_decoder_setopt( + sixel_decoder_t /* in */ *decoder, /* decoder object */ + int /* in */ arg, /* one of SIXEL_OPTFLAG_*** */ + char const /* in */ *optarg); /* null or an argument of optflag */ + +/* load source data from stdin or the file specified with + SIXEL_OPTFLAG_INPUT flag, and decode it */ +SIXELAPI SIXELSTATUS +sixel_decoder_decode( + sixel_decoder_t /* in */ *decoder); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBSIXEL_SIXEL_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 */ diff --git a/src/3rdparty/sixel/status.c b/src/3rdparty/sixel/status.c new file mode 100644 index 0000000000..509e627a52 --- /dev/null +++ b/src/3rdparty/sixel/status.c @@ -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 +#include + +#if HAVE_MEMORY_H +# include +#endif /* HAVE_MEMORY_H */ +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_ERRNO_H +# include +#endif /* HAVE_ERRNO_H */ +#ifdef HAVE_LIBCURL +# include +#endif /* HAVE_LIBCURL */ + +#include +#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 */ diff --git a/src/3rdparty/sixel/status.h b/src/3rdparty/sixel/status.h new file mode 100644 index 0000000000..ea23584037 --- /dev/null +++ b/src/3rdparty/sixel/status.h @@ -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 */ diff --git a/src/3rdparty/sixel/tosixel.c b/src/3rdparty/sixel/tosixel.c new file mode 100644 index 0000000000..f3a23731a9 --- /dev/null +++ b/src/3rdparty/sixel/tosixel.c @@ -0,0 +1,1627 @@ +/* + * this file is derived from "sixel" original version (2014-3-2) + * http://nanno.dip.jp/softlib/man/rlogin/sixel.tar.gz + * + * Initial developer of this file is kmiya@culti. + * + * He distributes it under very permissive license which permits + * useing, copying, modification, redistribution, and all other + * public activities without any restrictions. + * + * He declares this is compatible with MIT/BSD/GPL. + * + * Hayaki Saito (saitoha@me.com) modified this and re-licensed + * it under the MIT license. + * + * Araki Ken added high-color encoding mode(sixel_encode_highcolor) + * extension. + * + */ +#include "config.h" + +/* STDC_HEADERS */ +#include +#include + +#if HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#if HAVE_LIMITS_H +# include +#endif /* HAVE_LIMITS_H */ +#if HAVE_INTTYPES_H +# include +#endif /* HAVE_INTTYPES_H */ + +#include +#include "output.h" +#include "dither.h" + +#define DCS_START_7BIT "\033P" +#define DCS_START_7BIT_SIZE (sizeof(DCS_START_7BIT) - 1) +#define DCS_START_8BIT "\220" +#define DCS_START_8BIT_SIZE (sizeof(DCS_START_8BIT) - 1) +#define DCS_END_7BIT "\033\\" +#define DCS_END_7BIT_SIZE (sizeof(DCS_END_7BIT) - 1) +#define DCS_END_8BIT "\234" +#define DCS_END_8BIT_SIZE (sizeof(DCS_END_8BIT) - 1) +#define DCS_7BIT(x) DCS_START_7BIT x DCS_END_7BIT +#define DCS_8BIT(x) DCS_START_8BIT x DCS_END_8BIT +#define SCREEN_PACKET_SIZE 256 + +enum { + PALETTE_HIT = 1, + PALETTE_CHANGE = 2 +}; + +/* implementation */ + +/* GNU Screen penetration */ +static void +sixel_penetrate( + sixel_output_t /* in */ *output, /* output context */ + int /* in */ nwrite, /* output size */ + char const /* in */ *dcs_start, /* DCS introducer */ + char const /* in */ *dcs_end, /* DCS terminator */ + int const /* in */ dcs_start_size, /* size of DCS introducer */ + int const /* in */ dcs_end_size) /* size of DCS terminator */ +{ + int pos; + int const splitsize = SCREEN_PACKET_SIZE + - dcs_start_size - dcs_end_size; + + for (pos = 0; pos < nwrite; pos += splitsize) { + output->fn_write((char *)dcs_start, dcs_start_size, output->priv); + output->fn_write(((char *)output->buffer) + pos, + nwrite - pos < splitsize ? nwrite - pos: splitsize, + output->priv); + output->fn_write((char *)dcs_end, dcs_end_size, output->priv); + } +} + + +static void +sixel_advance(sixel_output_t *output, int nwrite) +{ + if ((output->pos += nwrite) >= SIXEL_OUTPUT_PACKET_SIZE) { + if (output->penetrate_multiplexer) { + sixel_penetrate(output, + SIXEL_OUTPUT_PACKET_SIZE, + DCS_START_7BIT, + DCS_END_7BIT, + DCS_START_7BIT_SIZE, + DCS_END_7BIT_SIZE); + } else { + output->fn_write((char *)output->buffer, + SIXEL_OUTPUT_PACKET_SIZE, output->priv); + } + memcpy(output->buffer, + output->buffer + SIXEL_OUTPUT_PACKET_SIZE, + (size_t)(output->pos -= SIXEL_OUTPUT_PACKET_SIZE)); + } +} + + +static void +sixel_putc(unsigned char *buffer, unsigned char value) +{ + *buffer = value; +} + + +static void +sixel_puts(unsigned char *buffer, char const *value, int size) +{ + memcpy(buffer, (void *)value, (size_t)size); +} + + +#if HAVE_LDIV +static int +sixel_putnum_impl(char *buffer, long value, int pos) +{ + ldiv_t r; + + r = ldiv(value, 10); + if (r.quot > 0) { + pos = sixel_putnum_impl(buffer, r.quot, pos); + } + *(buffer + pos) = '0' + r.rem; + return pos + 1; +} +#endif /* HAVE_LDIV */ + + +static int +sixel_putnum(char *buffer, int value) +{ + int pos; + +#if HAVE_LDIV + pos = sixel_putnum_impl(buffer, value, 0); +#else + pos = sprintf(buffer, "%d", value); +#endif /* HAVE_LDIV */ + + return pos; +} + + +static SIXELSTATUS +sixel_put_flash(sixel_output_t *const output) +{ + int n; + int nwrite; + + if (output->has_gri_arg_limit) { /* VT240 Max 255 ? */ + while (output->save_count > 255) { + /* argument of DECGRI('!') is limitted to 255 in real VT */ + sixel_puts(output->buffer + output->pos, "!255", 4); + sixel_advance(output, 4); + sixel_putc(output->buffer + output->pos, output->save_pixel); + sixel_advance(output, 1); + output->save_count -= 255; + } + } + + if (output->save_count > 3) { + /* DECGRI Graphics Repeat Introducer ! Pn Ch */ + sixel_putc(output->buffer + output->pos, '!'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, output->save_count); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, output->save_pixel); + sixel_advance(output, 1); + } else { + for (n = 0; n < output->save_count; n++) { + output->buffer[output->pos] = output->save_pixel; + sixel_advance(output, 1); + } + } + + output->save_pixel = 0; + output->save_count = 0; + + return 0; +} + + +static SIXELSTATUS +sixel_put_pixel(sixel_output_t *const output, int pix) +{ + SIXELSTATUS status = SIXEL_FALSE; + + if (pix < 0 || pix > '?') { + pix = 0; + } + + pix += '?'; + + if (pix == output->save_pixel) { + output->save_count++; + } else { + status = sixel_put_flash(output); + if (SIXEL_FAILED(status)) { + goto end; + } + output->save_pixel = pix; + output->save_count = 1; + } + + status = SIXEL_OK; + +end: + return status; +} + +static SIXELSTATUS +sixel_node_new(sixel_node_t **np, sixel_allocator_t *allocator) +{ + SIXELSTATUS status = SIXEL_FALSE; + + *np = (sixel_node_t *)sixel_allocator_malloc(allocator, + sizeof(sixel_node_t)); + if (np == NULL) { + sixel_helper_set_additional_message( + "sixel_node_new: sixel_allocator_malloc() failed."); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + + status = SIXEL_OK; + +end: + return status; +} + +static void +sixel_node_del(sixel_output_t *output, sixel_node_t *np) +{ + sixel_node_t *tp; + + if ((tp = output->node_top) == np) { + output->node_top = np->next; + } else { + while (tp->next != NULL) { + if (tp->next == np) { + tp->next = np->next; + break; + } + tp = tp->next; + } + } + + np->next = output->node_free; + output->node_free = np; +} + + +static SIXELSTATUS +sixel_put_node( + sixel_output_t /* in */ *output, /* output context */ + int /* in/out */ *x, /* header position */ + sixel_node_t /* in */ *np, /* node object */ + int /* in */ ncolors, /* number of palette colors */ + int /* in */ keycolor) /* transparent color number */ +{ + SIXELSTATUS status = SIXEL_FALSE; + int nwrite; + + if (ncolors != 2 || keycolor == (-1)) { + /* designate palette index */ + if (output->active_palette != np->pal) { + sixel_putc(output->buffer + output->pos, '#'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, np->pal); + sixel_advance(output, nwrite); + output->active_palette = np->pal; + } + } + + for (; *x < np->sx; ++*x) { + status = sixel_put_pixel(output, 0); + if (SIXEL_FAILED(status)) { + goto end; + } + } + + for (; *x < np->mx; ++*x) { + status = sixel_put_pixel(output, np->map[*x]); + if (SIXEL_FAILED(status)) { + goto end; + } + } + + status = sixel_put_flash(output); + if (SIXEL_FAILED(status)) { + goto end; + } + +end: + return status; +} + + +static SIXELSTATUS +sixel_encode_header(int width, int height, sixel_output_t *output) +{ + SIXELSTATUS status = SIXEL_FALSE; + int nwrite; + int p[3] = {0, 0, 0}; + int pcount = 3; + int use_raster_attributes = 1; + + output->pos = 0; + + if (!output->skip_dcs_envelope) { + if (output->has_8bit_control) { + sixel_puts(output->buffer + output->pos, + DCS_START_8BIT, + DCS_START_8BIT_SIZE); + sixel_advance(output, DCS_START_8BIT_SIZE); + } else { + sixel_puts(output->buffer + output->pos, + DCS_START_7BIT, + DCS_START_7BIT_SIZE); + sixel_advance(output, DCS_START_7BIT_SIZE); + } + } + + if (p[2] == 0) { + pcount--; + if (p[1] == 0) { + pcount--; + if (p[0] == 0) { + pcount--; + } + } + } + + if (pcount > 0) { + nwrite = sixel_putnum((char *)output->buffer + output->pos, p[0]); + sixel_advance(output, nwrite); + if (pcount > 1) { + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, p[1]); + sixel_advance(output, nwrite); + if (pcount > 2) { + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, p[2]); + sixel_advance(output, nwrite); + } + } + } + + sixel_putc(output->buffer + output->pos, 'q'); + sixel_advance(output, 1); + + if (use_raster_attributes) { + sixel_puts(output->buffer + output->pos, "\"1;1;", 5); + sixel_advance(output, 5); + nwrite = sixel_putnum((char *)output->buffer + output->pos, width); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, height); + sixel_advance(output, nwrite); + } + + status = SIXEL_OK; + + return status; +} + + +static SIXELSTATUS +output_rgb_palette_definition( + sixel_output_t /* in */ *output, + unsigned char /* in */ *palette, + int /* in */ n, + int /* in */ keycolor +) +{ + SIXELSTATUS status = SIXEL_FALSE; + int nwrite; + + if (n != keycolor) { + /* DECGCI Graphics Color Introducer # Pc ; Pu; Px; Py; Pz */ + sixel_putc(output->buffer + output->pos, '#'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, n); + sixel_advance(output, nwrite); + sixel_puts(output->buffer + output->pos, ";2;", 3); + sixel_advance(output, 3); + nwrite = sixel_putnum((char *)output->buffer + output->pos, + (palette[n * 3 + 0] * 100 + 127) / 255); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, + (palette[n * 3 + 1] * 100 + 127) / 255); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, + (palette[n * 3 + 2] * 100 + 127) / 255); + sixel_advance(output, nwrite); + } + + status = SIXEL_OK; + + return status; +} + + +static SIXELSTATUS +output_hls_palette_definition( + sixel_output_t /* in */ *output, + unsigned char /* in */ *palette, + int /* in */ n, + int /* in */ keycolor +) +{ + SIXELSTATUS status = SIXEL_FALSE; + int h; + int l; + int s; + int r; + int g; + int b; + int max; + int min; + int nwrite; + + if (n != keycolor) { + r = palette[n * 3 + 0]; + g = palette[n * 3 + 1]; + b = palette[n * 3 + 2]; + max = r > g ? (r > b ? r: b): (g > b ? g: b); + min = r < g ? (r < b ? r: b): (g < b ? g: b); + l = ((max + min) * 100 + 255) / 510; + if (max == min) { + h = s = 0; + } else { + if (l < 50) { + s = ((max - min) * 100) / (max + min); + } else { + s = ((max - min) * 100) / ((255 - max) + (255 - min)); + } + if (r == max) { + h = 120 + (g - b) * 60 / (max - min); + } else if (g == max) { + h = 240 + (b - r) * 60 / (max - min); + } else if (r < g) /* if (b == max) */ { + h = 360 + (r - g) * 60 / (max - min); + } else { + h = 0 + (r - g) * 60 / (max - min); + } + } + /* DECGCI Graphics Color Introducer # Pc ; Pu; Px; Py; Pz */ + sixel_putc(output->buffer + output->pos, '#'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, n); + sixel_advance(output, nwrite); + sixel_puts(output->buffer + output->pos, ";1;", 3); + sixel_advance(output, 3); + nwrite = sixel_putnum((char *)output->buffer + output->pos, h); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, l); + sixel_advance(output, nwrite); + sixel_putc(output->buffer + output->pos, ';'); + sixel_advance(output, 1); + nwrite = sixel_putnum((char *)output->buffer + output->pos, s); + sixel_advance(output, nwrite); + } + + status = SIXEL_OK; + return status; +} + + +static SIXELSTATUS +sixel_encode_body( + sixel_index_t /* in */ *pixels, + int /* in */ width, + int /* in */ height, + unsigned char /* in */ *palette, + int /* in */ ncolors, + int /* in */ keycolor, + int /* in */ bodyonly, + sixel_output_t /* in */ *output, + unsigned char /* in */ *palstate, + sixel_allocator_t /* in */ *allocator) +{ + SIXELSTATUS status = SIXEL_FALSE; + int x; + int y; + int i; + int n; + int c; + int sx; + int mx; + int len; + int pix; + char *map = NULL; + int check_integer_overflow; + sixel_node_t *np, *tp, top; + int fillable; + + if (ncolors < 1) { + status = SIXEL_BAD_ARGUMENT; + goto end; + } + len = ncolors * width; + output->active_palette = (-1); + + map = (char *)sixel_allocator_calloc(allocator, + (size_t)len, + sizeof(char)); + if (map == NULL) { + sixel_helper_set_additional_message( + "sixel_encode_body: sixel_allocator_calloc() failed."); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + + if (!bodyonly && (ncolors != 2 || keycolor == (-1))) { + if (output->palette_type == SIXEL_PALETTETYPE_HLS) { + for (n = 0; n < ncolors; n++) { + status = output_hls_palette_definition(output, palette, n, keycolor); + if (SIXEL_FAILED(status)) { + goto end; + } + } + } else { + for (n = 0; n < ncolors; n++) { + status = output_rgb_palette_definition(output, palette, n, keycolor); + if (SIXEL_FAILED(status)) { + goto end; + } + } + } + } + + for (y = i = 0; y < height; y++) { + if (output->encode_policy != SIXEL_ENCODEPOLICY_SIZE) { + fillable = 0; + } else if (palstate) { + /* high color sixel */ + pix = pixels[(y - i) * width]; + if (pix >= ncolors) { + fillable = 0; + } else { + fillable = 1; + } + } else { + /* normal sixel */ + fillable = 1; + } + for (x = 0; x < width; x++) { + if (y > INT_MAX / width) { + /* integer overflow */ + sixel_helper_set_additional_message( + "sixel_encode_body: integer overflow detected." + " (y > INT_MAX)"); + status = SIXEL_BAD_INTEGER_OVERFLOW; + goto end; + } + check_integer_overflow = y * width; + if (check_integer_overflow > INT_MAX - x) { + /* integer overflow */ + sixel_helper_set_additional_message( + "sixel_encode_body: integer overflow detected." + " (y * width > INT_MAX - x)"); + status = SIXEL_BAD_INTEGER_OVERFLOW; + goto end; + } + pix = pixels[check_integer_overflow + x]; /* color index */ + if (pix >= 0 && pix < ncolors && pix != keycolor) { + if (pix > INT_MAX / width) { + /* integer overflow */ + sixel_helper_set_additional_message( + "sixel_encode_body: integer overflow detected." + " (pix > INT_MAX / width)"); + status = SIXEL_BAD_INTEGER_OVERFLOW; + goto end; + } + check_integer_overflow = pix * width; + if (check_integer_overflow > INT_MAX - x) { + /* integer overflow */ + sixel_helper_set_additional_message( + "sixel_encode_body: integer overflow detected." + " (pix * width > INT_MAX - x)"); + status = SIXEL_BAD_INTEGER_OVERFLOW; + goto end; + } + map[pix * width + x] |= (1 << i); + } + else if (!palstate) { + fillable = 0; + } + } + + if (++i < 6 && (y + 1) < height) { + continue; + } + + for (c = 0; c < ncolors; c++) { + for (sx = 0; sx < width; sx++) { + if (*(map + c * width + sx) == 0) { + continue; + } + + for (mx = sx + 1; mx < width; mx++) { + if (*(map + c * width + mx) != 0) { + continue; + } + + for (n = 1; (mx + n) < width; n++) { + if (*(map + c * width + mx + n) != 0) { + break; + } + } + + if (n >= 10 || (mx + n) >= width) { + break; + } + mx = mx + n - 1; + } + + if ((np = output->node_free) != NULL) { + output->node_free = np->next; + } else { + status = sixel_node_new(&np, allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + } + + np->pal = c; + np->sx = sx; + np->mx = mx; + np->map = map + c * width; + + top.next = output->node_top; + tp = ⊤ + + while (tp->next != NULL) { + if (np->sx < tp->next->sx) { + break; + } else if (np->sx == tp->next->sx && np->mx > tp->next->mx) { + break; + } + tp = tp->next; + } + + np->next = tp->next; + tp->next = np; + output->node_top = top.next; + + sx = mx - 1; + } + + } + + if (y != 5) { + /* DECGNL Graphics Next Line */ + output->buffer[output->pos] = '-'; + sixel_advance(output, 1); + } + + for (x = 0; (np = output->node_top) != NULL;) { + sixel_node_t *next; + if (x > np->sx) { + /* DECGCR Graphics Carriage Return */ + output->buffer[output->pos] = '$'; + sixel_advance(output, 1); + x = 0; + } + + if (fillable) { + memset(np->map + np->sx, (1 << i) - 1, (size_t)(np->mx - np->sx)); + } + status = sixel_put_node(output, &x, np, ncolors, keycolor); + if (SIXEL_FAILED(status)) { + goto end; + } + next = np->next; + sixel_node_del(output, np); + np = next; + + while (np != NULL) { + if (np->sx < x) { + np = np->next; + continue; + } + + if (fillable) { + memset(np->map + np->sx, (1 << i) - 1, (size_t)(np->mx - np->sx)); + } + status = sixel_put_node(output, &x, np, ncolors, keycolor); + if (SIXEL_FAILED(status)) { + goto end; + } + next = np->next; + sixel_node_del(output, np); + np = next; + } + + fillable = 0; + } + + i = 0; + memset(map, 0, (size_t)len); + } + + if (palstate) { + output->buffer[output->pos] = '$'; + sixel_advance(output, 1); + } + + status = SIXEL_OK; + +end: + /* free nodes */ + while ((np = output->node_free) != NULL) { + output->node_free = np->next; + sixel_allocator_free(allocator, np); + } + output->node_top = NULL; + + sixel_allocator_free(allocator, map); + + return status; +} + + +static SIXELSTATUS +sixel_encode_footer(sixel_output_t *output) +{ + SIXELSTATUS status = SIXEL_FALSE; + + if (!output->skip_dcs_envelope && !output->penetrate_multiplexer) { + if (output->has_8bit_control) { + sixel_puts(output->buffer + output->pos, + DCS_END_8BIT, DCS_END_8BIT_SIZE); + sixel_advance(output, DCS_END_8BIT_SIZE); + } else { + sixel_puts(output->buffer + output->pos, + DCS_END_7BIT, DCS_END_7BIT_SIZE); + sixel_advance(output, DCS_END_7BIT_SIZE); + } + } + + /* flush buffer */ + if (output->pos > 0) { + if (output->penetrate_multiplexer) { + sixel_penetrate(output, output->pos, + DCS_START_7BIT, + DCS_END_7BIT, + DCS_START_7BIT_SIZE, + DCS_END_7BIT_SIZE); + output->fn_write((char *)DCS_7BIT("\033") DCS_7BIT("\\"), + (DCS_START_7BIT_SIZE + 1 + DCS_END_7BIT_SIZE) * 2, + output->priv); + } else { + output->fn_write((char *)output->buffer, output->pos, output->priv); + } + } + + status = SIXEL_OK; + + return status; +} + + +static SIXELSTATUS +sixel_encode_dither( + unsigned char /* in */ *pixels, /* pixel bytes to be encoded */ + int /* in */ width, /* width of source image */ + int /* in */ height, /* height of source image */ + sixel_dither_t /* in */ *dither, /* dither context */ + sixel_output_t /* in */ *output) /* output context */ +{ + SIXELSTATUS status = SIXEL_FALSE; + sixel_index_t *paletted_pixels = NULL; + sixel_index_t *input_pixels; + size_t bufsize; + + switch (dither->pixelformat) { + case SIXEL_PIXELFORMAT_PAL1: + case SIXEL_PIXELFORMAT_PAL2: + case SIXEL_PIXELFORMAT_PAL4: + case SIXEL_PIXELFORMAT_G1: + case SIXEL_PIXELFORMAT_G2: + case SIXEL_PIXELFORMAT_G4: + bufsize = (sizeof(sixel_index_t) * (size_t)width * (size_t)height * 3UL); + paletted_pixels = (sixel_index_t *)sixel_allocator_malloc(dither->allocator, bufsize); + if (paletted_pixels == NULL) { + sixel_helper_set_additional_message( + "sixel_encode_dither: sixel_allocator_malloc() failed."); + status = SIXEL_BAD_ALLOCATION; + goto end; + } + status = sixel_helper_normalize_pixelformat(paletted_pixels, + &dither->pixelformat, + pixels, + dither->pixelformat, + width, height); + if (SIXEL_FAILED(status)) { + goto end; + } + input_pixels = paletted_pixels; + break; + case SIXEL_PIXELFORMAT_PAL8: + case SIXEL_PIXELFORMAT_G8: + case SIXEL_PIXELFORMAT_GA88: + case SIXEL_PIXELFORMAT_AG88: + input_pixels = pixels; + break; + default: + /* apply palette */ + paletted_pixels = sixel_dither_apply_palette(dither, pixels, + width, height); + if (paletted_pixels == NULL) { + status = SIXEL_RUNTIME_ERROR; + goto end; + } + input_pixels = paletted_pixels; + break; + } + + status = sixel_encode_header(width, height, output); + if (SIXEL_FAILED(status)) { + goto end; + } + + status = sixel_encode_body(input_pixels, + width, + height, + dither->palette, + dither->ncolors, + dither->keycolor, + dither->bodyonly, + output, + NULL, + dither->allocator); + if (SIXEL_FAILED(status)) { + goto end; + } + + status = sixel_encode_footer(output); + if (SIXEL_FAILED(status)) { + goto end; + } + +end: + sixel_allocator_free(dither->allocator, paletted_pixels); + + return status; +} + +static void +dither_func_none(unsigned char *data, int width) +{ + (void) data; /* unused */ + (void) width; /* unused */ +} + + +static void +dither_func_fs(unsigned char *data, int width) +{ + int r, g, b; + int error_r = data[0] & 0x7; + int error_g = data[1] & 0x7; + int error_b = data[2] & 0x7; + + /* Floyd Steinberg Method + * curr 7/16 + * 3/16 5/48 1/16 + */ + r = (data[3 + 0] + (error_r * 5 >> 4)); + g = (data[3 + 1] + (error_g * 5 >> 4)); + b = (data[3 + 2] + (error_b * 5 >> 4)); + data[3 + 0] = r > 0xff ? 0xff: r; + data[3 + 1] = g > 0xff ? 0xff: g; + data[3 + 2] = b > 0xff ? 0xff: b; + r = data[width * 3 - 3 + 0] + (error_r * 3 >> 4); + g = data[width * 3 - 3 + 1] + (error_g * 3 >> 4); + b = data[width * 3 - 3 + 2] + (error_b * 3 >> 4); + data[width * 3 - 3 + 0] = r > 0xff ? 0xff: r; + data[width * 3 - 3 + 1] = g > 0xff ? 0xff: g; + data[width * 3 - 3 + 2] = b > 0xff ? 0xff: b; + r = data[width * 3 + 0] + (error_r * 5 >> 4); + g = data[width * 3 + 1] + (error_g * 5 >> 4); + b = data[width * 3 + 2] + (error_b * 5 >> 4); + data[width * 3 + 0] = r > 0xff ? 0xff: r; + data[width * 3 + 1] = g > 0xff ? 0xff: g; + data[width * 3 + 2] = b > 0xff ? 0xff: b; +} + + +static void +dither_func_atkinson(unsigned char *data, int width) +{ + int r, g, b; + int error_r = data[0] & 0x7; + int error_g = data[1] & 0x7; + int error_b = data[2] & 0x7; + + error_r += 4; + error_g += 4; + error_b += 4; + + /* Atkinson's Method + * curr 1/8 1/8 + * 1/8 1/8 1/8 + * 1/8 + */ + r = data[(width * 0 + 1) * 3 + 0] + (error_r >> 3); + g = data[(width * 0 + 1) * 3 + 1] + (error_g >> 3); + b = data[(width * 0 + 1) * 3 + 2] + (error_b >> 3); + data[(width * 0 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 0 + 2) * 3 + 0] + (error_r >> 3); + g = data[(width * 0 + 2) * 3 + 1] + (error_g >> 3); + b = data[(width * 0 + 2) * 3 + 2] + (error_b >> 3); + data[(width * 0 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 1) * 3 + 0] + (error_r >> 3); + g = data[(width * 1 - 1) * 3 + 1] + (error_g >> 3); + b = data[(width * 1 - 1) * 3 + 2] + (error_b >> 3); + data[(width * 1 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 0) * 3 + 0] + (error_r >> 3); + g = data[(width * 1 + 0) * 3 + 1] + (error_g >> 3); + b = data[(width * 1 + 0) * 3 + 2] + (error_b >> 3); + data[(width * 1 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = (data[(width * 1 + 1) * 3 + 0] + (error_r >> 3)); + g = (data[(width * 1 + 1) * 3 + 1] + (error_g >> 3)); + b = (data[(width * 1 + 1) * 3 + 2] + (error_b >> 3)); + data[(width * 1 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = (data[(width * 2 + 0) * 3 + 0] + (error_r >> 3)); + g = (data[(width * 2 + 0) * 3 + 1] + (error_g >> 3)); + b = (data[(width * 2 + 0) * 3 + 2] + (error_b >> 3)); + data[(width * 2 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 0) * 3 + 2] = b > 0xff ? 0xff: b; +} + + +static void +dither_func_jajuni(unsigned char *data, int width) +{ + int r, g, b; + int error_r = data[0] & 0x7; + int error_g = data[1] & 0x7; + int error_b = data[2] & 0x7; + + error_r += 4; + error_g += 4; + error_b += 4; + + /* Jarvis, Judice & Ninke Method + * curr 7/48 5/48 + * 3/48 5/48 7/48 5/48 3/48 + * 1/48 3/48 5/48 3/48 1/48 + */ + r = data[(width * 0 + 1) * 3 + 0] + (error_r * 7 / 48); + g = data[(width * 0 + 1) * 3 + 1] + (error_g * 7 / 48); + b = data[(width * 0 + 1) * 3 + 2] + (error_b * 7 / 48); + data[(width * 0 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 0 + 2) * 3 + 0] + (error_r * 5 / 48); + g = data[(width * 0 + 2) * 3 + 1] + (error_g * 5 / 48); + b = data[(width * 0 + 2) * 3 + 2] + (error_b * 5 / 48); + data[(width * 0 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 2) * 3 + 0] + (error_r * 3 / 48); + g = data[(width * 1 - 2) * 3 + 1] + (error_g * 3 / 48); + b = data[(width * 1 - 2) * 3 + 2] + (error_b * 3 / 48); + data[(width * 1 - 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 1) * 3 + 0] + (error_r * 5 / 48); + g = data[(width * 1 - 1) * 3 + 1] + (error_g * 5 / 48); + b = data[(width * 1 - 1) * 3 + 2] + (error_b * 5 / 48); + data[(width * 1 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 0) * 3 + 0] + (error_r * 7 / 48); + g = data[(width * 1 + 0) * 3 + 1] + (error_g * 7 / 48); + b = data[(width * 1 + 0) * 3 + 2] + (error_b * 7 / 48); + data[(width * 1 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 1) * 3 + 0] + (error_r * 5 / 48); + g = data[(width * 1 + 1) * 3 + 1] + (error_g * 5 / 48); + b = data[(width * 1 + 1) * 3 + 2] + (error_b * 5 / 48); + data[(width * 1 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 2) * 3 + 0] + (error_r * 3 / 48); + g = data[(width * 1 + 2) * 3 + 1] + (error_g * 3 / 48); + b = data[(width * 1 + 2) * 3 + 2] + (error_b * 3 / 48); + data[(width * 1 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 - 2) * 3 + 0] + (error_r * 1 / 48); + g = data[(width * 2 - 2) * 3 + 1] + (error_g * 1 / 48); + b = data[(width * 2 - 2) * 3 + 2] + (error_b * 1 / 48); + data[(width * 2 - 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 - 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 - 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 - 1) * 3 + 0] + (error_r * 3 / 48); + g = data[(width * 2 - 1) * 3 + 1] + (error_g * 3 / 48); + b = data[(width * 2 - 1) * 3 + 2] + (error_b * 3 / 48); + data[(width * 2 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 0) * 3 + 0] + (error_r * 5 / 48); + g = data[(width * 2 + 0) * 3 + 1] + (error_g * 5 / 48); + b = data[(width * 2 + 0) * 3 + 2] + (error_b * 5 / 48); + data[(width * 2 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 1) * 3 + 0] + (error_r * 3 / 48); + g = data[(width * 2 + 1) * 3 + 1] + (error_g * 3 / 48); + b = data[(width * 2 + 1) * 3 + 2] + (error_b * 3 / 48); + data[(width * 2 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 2) * 3 + 0] + (error_r * 1 / 48); + g = data[(width * 2 + 2) * 3 + 1] + (error_g * 1 / 48); + b = data[(width * 2 + 2) * 3 + 2] + (error_b * 1 / 48); + data[(width * 2 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 2) * 3 + 2] = b > 0xff ? 0xff: b; +} + + +static void +dither_func_stucki(unsigned char *data, int width) +{ + int r, g, b; + int error_r = data[0] & 0x7; + int error_g = data[1] & 0x7; + int error_b = data[2] & 0x7; + + error_r += 4; + error_g += 4; + error_b += 4; + + /* Stucki's Method + * curr 8/48 4/48 + * 2/48 4/48 8/48 4/48 2/48 + * 1/48 2/48 4/48 2/48 1/48 + */ + r = data[(width * 0 + 1) * 3 + 0] + (error_r * 8 / 48); + g = data[(width * 0 + 1) * 3 + 1] + (error_g * 8 / 48); + b = data[(width * 0 + 1) * 3 + 2] + (error_b * 8 / 48); + data[(width * 0 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 0 + 2) * 3 + 0] + (error_r * 4 / 48); + g = data[(width * 0 + 2) * 3 + 1] + (error_g * 4 / 48); + b = data[(width * 0 + 2) * 3 + 2] + (error_b * 4 / 48); + data[(width * 0 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 2) * 3 + 0] + (error_r * 2 / 48); + g = data[(width * 1 - 2) * 3 + 1] + (error_g * 2 / 48); + b = data[(width * 1 - 2) * 3 + 2] + (error_b * 2 / 48); + data[(width * 1 - 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 1) * 3 + 0] + (error_r * 4 / 48); + g = data[(width * 1 - 1) * 3 + 1] + (error_g * 4 / 48); + b = data[(width * 1 - 1) * 3 + 2] + (error_b * 4 / 48); + data[(width * 1 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 0) * 3 + 0] + (error_r * 8 / 48); + g = data[(width * 1 + 0) * 3 + 1] + (error_g * 8 / 48); + b = data[(width * 1 + 0) * 3 + 2] + (error_b * 8 / 48); + data[(width * 1 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 1) * 3 + 0] + (error_r * 4 / 48); + g = data[(width * 1 + 1) * 3 + 1] + (error_g * 4 / 48); + b = data[(width * 1 + 1) * 3 + 2] + (error_b * 4 / 48); + data[(width * 1 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 2) * 3 + 0] + (error_r * 2 / 48); + g = data[(width * 1 + 2) * 3 + 1] + (error_g * 2 / 48); + b = data[(width * 1 + 2) * 3 + 2] + (error_b * 2 / 48); + data[(width * 1 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 - 2) * 3 + 0] + (error_r * 1 / 48); + g = data[(width * 2 - 2) * 3 + 1] + (error_g * 1 / 48); + b = data[(width * 2 - 2) * 3 + 2] + (error_b * 1 / 48); + data[(width * 2 - 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 - 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 - 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 - 1) * 3 + 0] + (error_r * 2 / 48); + g = data[(width * 2 - 1) * 3 + 1] + (error_g * 2 / 48); + b = data[(width * 2 - 1) * 3 + 2] + (error_b * 2 / 48); + data[(width * 2 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 0) * 3 + 0] + (error_r * 4 / 48); + g = data[(width * 2 + 0) * 3 + 1] + (error_g * 4 / 48); + b = data[(width * 2 + 0) * 3 + 2] + (error_b * 4 / 48); + data[(width * 2 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 1) * 3 + 0] + (error_r * 2 / 48); + g = data[(width * 2 + 1) * 3 + 1] + (error_g * 2 / 48); + b = data[(width * 2 + 1) * 3 + 2] + (error_b * 2 / 48); + data[(width * 2 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 2 + 2) * 3 + 0] + (error_r * 1 / 48); + g = data[(width * 2 + 2) * 3 + 1] + (error_g * 1 / 48); + b = data[(width * 2 + 2) * 3 + 2] + (error_b * 1 / 48); + data[(width * 2 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 2 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 2 + 2) * 3 + 2] = b > 0xff ? 0xff: b; +} + + +static void +dither_func_burkes(unsigned char *data, int width) +{ + int r, g, b; + int error_r = data[0] & 0x7; + int error_g = data[1] & 0x7; + int error_b = data[2] & 0x7; + + error_r += 2; + error_g += 2; + error_b += 2; + + /* Burkes' Method + * curr 4/16 2/16 + * 1/16 2/16 4/16 2/16 1/16 + */ + r = data[(width * 0 + 1) * 3 + 0] + (error_r * 4 / 16); + g = data[(width * 0 + 1) * 3 + 1] + (error_g * 4 / 16); + b = data[(width * 0 + 1) * 3 + 2] + (error_b * 4 / 16); + data[(width * 0 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 0 + 2) * 3 + 0] + (error_r * 2 / 16); + g = data[(width * 0 + 2) * 3 + 1] + (error_g * 2 / 16); + b = data[(width * 0 + 2) * 3 + 2] + (error_b * 2 / 16); + data[(width * 0 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 0 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 0 + 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 2) * 3 + 0] + (error_r * 1 / 16); + g = data[(width * 1 - 2) * 3 + 1] + (error_g * 1 / 16); + b = data[(width * 1 - 2) * 3 + 2] + (error_b * 1 / 16); + data[(width * 1 - 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 2) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 - 1) * 3 + 0] + (error_r * 2 / 16); + g = data[(width * 1 - 1) * 3 + 1] + (error_g * 2 / 16); + b = data[(width * 1 - 1) * 3 + 2] + (error_b * 2 / 16); + data[(width * 1 - 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 - 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 - 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 0) * 3 + 0] + (error_r * 4 / 16); + g = data[(width * 1 + 0) * 3 + 1] + (error_g * 4 / 16); + b = data[(width * 1 + 0) * 3 + 2] + (error_b * 4 / 16); + data[(width * 1 + 0) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 0) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 0) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 1) * 3 + 0] + (error_r * 2 / 16); + g = data[(width * 1 + 1) * 3 + 1] + (error_g * 2 / 16); + b = data[(width * 1 + 1) * 3 + 2] + (error_b * 2 / 16); + data[(width * 1 + 1) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 1) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 1) * 3 + 2] = b > 0xff ? 0xff: b; + r = data[(width * 1 + 2) * 3 + 0] + (error_r * 1 / 16); + g = data[(width * 1 + 2) * 3 + 1] + (error_g * 1 / 16); + b = data[(width * 1 + 2) * 3 + 2] + (error_b * 1 / 16); + data[(width * 1 + 2) * 3 + 0] = r > 0xff ? 0xff: r; + data[(width * 1 + 2) * 3 + 1] = g > 0xff ? 0xff: g; + data[(width * 1 + 2) * 3 + 2] = b > 0xff ? 0xff: b; +} + + +static void +dither_func_a_dither(unsigned char *data, int width, int x, int y) +{ + int c; + float value; + float mask; + + (void) width; /* unused */ + + for (c = 0; c < 3; c ++) { + mask = (((x + c * 17) + y * 236) * 119) & 255; + mask = ((mask - 128) / 256.0f) ; + value = data[c] + mask; + if (value < 0) { + value = 0; + } + value = value > 255 ? 255 : value; + data[c] = value; + } +} + + +static void +dither_func_x_dither(unsigned char *data, int width, int x, int y) +{ + int c; + float value; + float mask; + + (void) width; /* unused */ + + for (c = 0; c < 3; c ++) { + mask = (((x + c * 17) ^ y * 236) * 1234) & 511; + mask = ((mask - 128) / 512.0f) ; + value = data[c] + mask; + if (value < 0) { + value = 0; + } + value = value > 255 ? 255 : value; + data[c] = value; + } +} + + +static void +sixel_apply_15bpp_dither( + unsigned char *pixels, + int x, int y, int width, int height, + int method_for_diffuse) +{ + /* apply floyd steinberg dithering */ + switch (method_for_diffuse) { + case SIXEL_DIFFUSE_FS: + if (x < width - 1 && y < height - 1) { + dither_func_fs(pixels, width); + } + break; + case SIXEL_DIFFUSE_ATKINSON: + if (x < width - 2 && y < height - 2) { + dither_func_atkinson(pixels, width); + } + break; + case SIXEL_DIFFUSE_JAJUNI: + if (x < width - 2 && y < height - 2) { + dither_func_jajuni(pixels, width); + } + break; + case SIXEL_DIFFUSE_STUCKI: + if (x < width - 2 && y < height - 2) { + dither_func_stucki(pixels, width); + } + break; + case SIXEL_DIFFUSE_BURKES: + if (x < width - 2 && y < height - 1) { + dither_func_burkes(pixels, width); + } + break; + case SIXEL_DIFFUSE_A_DITHER: + dither_func_a_dither(pixels, width, x, y); + break; + case SIXEL_DIFFUSE_X_DITHER: + dither_func_x_dither(pixels, width, x, y); + break; + case SIXEL_DIFFUSE_NONE: + default: + dither_func_none(pixels, width); + break; + } +} + + +static SIXELSTATUS +sixel_encode_highcolor( + unsigned char *pixels, int width, int height, + sixel_dither_t *dither, sixel_output_t *output + ) +{ + SIXELSTATUS status = SIXEL_FALSE; + sixel_index_t *paletted_pixels = NULL; + unsigned char *normalized_pixels = NULL; + /* Mark sixel line pixels which have been already drawn. */ + unsigned char *marks; + unsigned char *rgbhit; + unsigned char *rgb2pal; + unsigned char palhitcount[SIXEL_PALETTE_MAX]; + unsigned char palstate[SIXEL_PALETTE_MAX]; + int output_count; + int const maxcolors = 1 << 15; + size_t image_size; + size_t marks_size; + size_t normalized_size; + size_t whole_size; + size_t maxcolors_size; + int x, y; + unsigned char *dst; + unsigned char *mptr; + int dirty; + int mod_y; + int nextpal; + int threshold; + int pix; + int orig_height; + unsigned char *pal; + + /* + * The high-color encoder keeps one palette index per input pixel + * followed by color lookup tables and six scanlines of mark bytes. + * Calculate all derived sizes in size_t and reject arithmetic + * overflow before a wrapped value can reach the allocator. + */ + maxcolors_size = (size_t)maxcolors; + if ((size_t)height > ((size_t)-1) / (size_t)width) { + sixel_helper_set_additional_message( + "sixel_encode_highcolor: image size overflow."); + status = SIXEL_BAD_INPUT; + goto error; + } + image_size = (size_t)width * (size_t)height; + + if (image_size > ((size_t)-1) / 3UL) { + sixel_helper_set_additional_message( + "sixel_encode_highcolor: normalized size overflow."); + status = SIXEL_BAD_INPUT; + goto error; + } + normalized_size = image_size * 3UL; + + if ((size_t)width > ((size_t)-1) / 6UL) { + sixel_helper_set_additional_message( + "sixel_encode_highcolor: marks size overflow."); + status = SIXEL_BAD_INPUT; + goto error; + } + marks_size = (size_t)width * 6UL; + + if (image_size > (size_t)-1 - maxcolors_size || + image_size + maxcolors_size > (size_t)-1 - maxcolors_size || + image_size + maxcolors_size + maxcolors_size + > (size_t)-1 - marks_size) { + sixel_helper_set_additional_message( + "sixel_encode_highcolor: whole size overflow."); + status = SIXEL_BAD_INPUT; + goto error; + } + whole_size = image_size /* for paletted_pixels */ + + maxcolors_size /* for rgbhit */ + + maxcolors_size /* for rgb2pal */ + + marks_size; /* for marks */ + + if (dither->pixelformat != SIXEL_PIXELFORMAT_RGB888) { + /* normalize pixelfromat */ + normalized_pixels = (unsigned char *)sixel_allocator_malloc( + dither->allocator, normalized_size); + if (normalized_pixels == NULL) { + goto error; + } + status = sixel_helper_normalize_pixelformat(normalized_pixels, + &dither->pixelformat, + pixels, + dither->pixelformat, + width, height); + if (SIXEL_FAILED(status)) { + goto error; + } + pixels = normalized_pixels; + } + paletted_pixels = (sixel_index_t *)sixel_allocator_malloc(dither->allocator, + whole_size); + if (paletted_pixels == NULL) { + goto error; + } + rgbhit = paletted_pixels + image_size; + memset(rgbhit, 0, maxcolors_size * 2UL + marks_size); + rgb2pal = rgbhit + maxcolors; + marks = rgb2pal + maxcolors; + output_count = 0; + +next: + dst = paletted_pixels; + nextpal = 0; + threshold = 1; + dirty = 0; + mptr = marks; + memset(palstate, 0, sizeof(palstate)); + y = mod_y = 0; + + while (1) { + for (x = 0; x < width; x++, mptr++, dst++, pixels += 3) { + if (*mptr) { + *dst = 255; + } else { + sixel_apply_15bpp_dither(pixels, + x, y, width, height, + dither->method_for_diffuse); + pix = ((pixels[0] & 0xf8) << 7) | + ((pixels[1] & 0xf8) << 2) | + ((pixels[2] >> 3) & 0x1f); + + if (!rgbhit[pix]) { + while (1) { + if (nextpal >= 255) { + if (threshold >= 255) { + break; + } else { + threshold = (threshold == 1) ? 9: 255; + nextpal = 0; + } + } else if (palstate[nextpal] || + palhitcount[nextpal] > threshold) { + nextpal++; + } else { + break; + } + } + + if (nextpal >= 255) { + dirty = 1; + *dst = 255; + } else { + pal = dither->palette + (nextpal * 3); + + rgbhit[pix] = 1; + if (output_count > 0) { + rgbhit[((pal[0] & 0xf8) << 7) | + ((pal[1] & 0xf8) << 2) | + ((pal[2] >> 3) & 0x1f)] = 0; + } + *dst = rgb2pal[pix] = nextpal++; + *mptr = 1; + palstate[*dst] = PALETTE_CHANGE; + palhitcount[*dst] = 1; + *(pal++) = pixels[0]; + *(pal++) = pixels[1]; + *(pal++) = pixels[2]; + } + } else { + *dst = rgb2pal[pix]; + *mptr = 1; + if (!palstate[*dst]) { + palstate[*dst] = PALETTE_HIT; + } + if (palhitcount[*dst] < 255) { + palhitcount[*dst]++; + } + } + } + } + + if (++y >= height) { + if (dirty) { + mod_y = 5; + } else { + goto end; + } + } + if (dirty && (mod_y == 5 || y >= height)) { + orig_height = height; + + if (output_count++ == 0) { + status = sixel_encode_header(width, height, output); + if (SIXEL_FAILED(status)) { + goto error; + } + } + height = y; + status = sixel_encode_body(paletted_pixels, + width, + height, + dither->palette, + 255, + 255, + dither->bodyonly, + output, + palstate, + dither->allocator); + if (SIXEL_FAILED(status)) { + goto error; + } + if (y >= orig_height) { + goto end; + } + pixels -= (6 * width * 3); + height = orig_height - height + 6; + goto next; + } + + if (++mod_y == 6) { + mptr = (unsigned char *)memset(marks, 0, marks_size); + mod_y = 0; + } + } + + goto next; + +end: + if (output_count == 0) { + status = sixel_encode_header(width, height, output); + if (SIXEL_FAILED(status)) { + goto error; + } + } + status = sixel_encode_body(paletted_pixels, + width, + height, + dither->palette, + 255, + 255, + dither->bodyonly, + output, + palstate, + dither->allocator); + if (SIXEL_FAILED(status)) { + goto error; + } + + status = sixel_encode_footer(output); + if (SIXEL_FAILED(status)) { + goto error; + } + +error: + sixel_allocator_free(dither->allocator, paletted_pixels); + sixel_allocator_free(dither->allocator, normalized_pixels); + + return status; +} + + +SIXELAPI SIXELSTATUS +sixel_encode( + unsigned char /* in */ *pixels, /* pixel bytes */ + int /* in */ width, /* image width */ + int /* in */ height, /* image height */ + int const /* in */ depth, /* color depth */ + sixel_dither_t /* in */ *dither, /* dither context */ + sixel_output_t /* in */ *output) /* output context */ +{ + SIXELSTATUS status = SIXEL_FALSE; + + (void) depth; + + /* TODO: reference counting should be thread-safe */ + sixel_dither_ref(dither); + sixel_output_ref(output); + + if (width < 1) { + sixel_helper_set_additional_message( + "sixel_encode: bad width parameter." + " (width < 1)"); + status = SIXEL_BAD_INPUT; + goto end; + } + + if (height < 1) { + sixel_helper_set_additional_message( + "sixel_encode: bad height parameter." + " (height < 1)"); + status = SIXEL_BAD_INPUT; + goto end; + } + + if (width > SIXEL_WIDTH_LIMIT) { + sixel_helper_set_additional_message( + "sixel_encode: bad width parameter." + " (width > SIXEL_WIDTH_LIMIT)"); + status = SIXEL_BAD_INPUT; + goto end; + } + + if (height > SIXEL_HEIGHT_LIMIT) { + sixel_helper_set_additional_message( + "sixel_encode: bad height parameter." + " (height > SIXEL_HEIGHT_LIMIT)"); + status = SIXEL_BAD_INPUT; + goto end; + } + + if (dither->quality_mode == SIXEL_QUALITY_HIGHCOLOR) { + status = sixel_encode_highcolor(pixels, width, height, + dither, output); + } else { + status = sixel_encode_dither(pixels, width, height, + dither, output); + } + +end: + sixel_output_unref(output); + sixel_dither_unref(dither); + + return status; +} + +/* 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 */ From e8350a0d6e644282da17491fa8049da25347673d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sun, 13 Sep 2026 22:22:06 +0800 Subject: [PATCH 34/76] Logo (Image): drops IM in favor of WIC on Windows --- CMakeLists.txt | 13 +- src/common/impl/init.c | 3 + src/logo/image/im6.c | 141 ++++++++++++++++++- src/logo/image/im7.c | 141 ++++++++++++++++++- src/logo/image/image.c | 303 +++++++++++++++++------------------------ src/logo/image/image.h | 40 ++++-- src/logo/image/sixel.c | 54 ++++++++ src/logo/image/wic.cpp | 127 +++++++++++++++++ 8 files changed, 619 insertions(+), 203 deletions(-) create mode 100644 src/logo/image/sixel.c create mode 100644 src/logo/image/wic.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cdd0f29412..98c0058227 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,9 +88,10 @@ 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_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR ANDROID 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_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows only; replaces ImageMagick's SIXEL coder)" ON "WIN32" OFF) +cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32" 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) @@ -101,7 +102,6 @@ cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR Dragon cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF) option(ENABLE_ZLIB "Enable zlib" ON) -option(ENABLE_SIXEL "Enable sixel image logo output (vendored libsixel encoder)" 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) option(ENABLE_TRACER "Build fastfetch with function tracing" OFF) @@ -504,6 +504,7 @@ set(LIBFASTFETCH_SRC src/logo/image/im6.c src/logo/image/im7.c src/logo/image/image.c + src/logo/image/sixel.c src/logo/logo.c src/modules/modules.c src/options/display.c @@ -1115,6 +1116,7 @@ elseif(WIN32) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_windows.c src/detection/camera/camera_windows.cpp + src/logo/image/wic.cpp ) elseif(SunOS) list(APPEND LIBFASTFETCH_SRC @@ -1913,6 +1915,7 @@ elseif(WIN32) PRIVATE "cfgmgr32" PRIVATE "winbrand" PRIVATE "secur32" + PRIVATE "windowscodecs" ) if(NOT ENABLE_WIN81_COMPAT) target_link_libraries(libfastfetch @@ -2013,6 +2016,10 @@ if(ENABLE_SIXEL) # src/3rdparty/sixel/*.c include (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 diff --git a/src/common/impl/init.c b/src/common/impl/init.c index f4307e1f9f..16932e1e23 100644 --- a/src/common/impl/init.c +++ b/src/common/impl/init.c @@ -281,6 +281,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 diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c index b8743b7ec1..f2c7bb0b35 100644 --- a/src/logo/image/im6.c +++ b/src/logo/image/im6.c @@ -2,8 +2,10 @@ #include "image.h" #include "common/library.h" + #include "common/mallocHelper.h" #include + #include static FF_LIBRARY_SYMBOL(ResizeImage) @@ -11,7 +13,10 @@ static FF_LIBRARY_SYMBOL(ResizeImage) return ffResizeImage(image, width, height, UndefinedFilter, 1.0, exceptionInfo); } -FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { +// Decode the image source, resize it to the requested pixel size and export it as a blob +// in the given ImageMagick format ("RGBA" for kitty / chafa, "SIXEL" for sixel output). +// On success requestData->logoPixelWidth / logoPixelHeight hold the real image dimensions. +static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const char* magick, uint32_t magickLength, void** outBlob, size_t* outLength) { // clang-format off FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-6.Q16HDRI" FF_LIBRARY_EXTENSION, 8, @@ -21,13 +26,137 @@ FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { // clang-format on FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { - .resizeFunc = logoResize, - .library = imageMagick, - }); + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - imageMagick = nullptr; // leak imageMagick to prevent fastfetch from crashing #552 + FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_RUN_ERROR; + ExceptionInfo* exceptionInfo = nullptr; + Image* image = nullptr; + ImageInfo* imageInfoOut = nullptr; + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + + ffMagickCoreGenesis(nullptr, MagickFalse); + + exceptionInfo = ffAcquireExceptionInfo(); + if (exceptionInfo == nullptr) { + goto cleanup; + } + + { + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == nullptr) { + goto cleanup; + } + + //+1, because we need to copy the null byte too + ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); + + image = ffReadImage(imageInfoIn, exceptionInfo); + ffDestroyImageInfo(imageInfoIn); + if (image == nullptr) { + goto cleanup; + } + } + + if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { + requestData->logoPixelWidth = (uint32_t) image->columns; + requestData->logoPixelHeight = (uint32_t) image->rows; + } else if (requestData->logoPixelWidth == 0) { + requestData->logoPixelWidth = (uint32_t) ((double) image->columns / (double) image->rows * requestData->logoPixelHeight); + } else if (requestData->logoPixelHeight == 0) { + requestData->logoPixelHeight = (uint32_t) ((double) image->rows / (double) image->columns * requestData->logoPixelWidth); + } + + if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0) { + goto cleanup; + } + + { + Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); + ffDestroyImage(image); + image = resized; + if (image == nullptr) { + goto cleanup; + } + } + + imageInfoOut = ffAcquireImageInfo(); + if (imageInfoOut == nullptr) { + goto cleanup; + } + + ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); + if (blob == nullptr || length == 0) { + goto cleanup; + } + + *outBlob = blob; + *outLength = length; + blob = nullptr; // Ownership is transferred to the caller + result = FF_LOGO_IMAGE_RESULT_SUCCESS; + +cleanup: + if (imageInfoOut) { + ffDestroyImageInfo(imageInfoOut); + } + if (image) { + ffDestroyImage(image); + } + if (exceptionInfo) { + ffDestroyExceptionInfo(exceptionInfo); + } + ffMagickCoreTerminus(); + + // leak imageMagick to prevent fastfetch from crashing #552 + imageMagick = nullptr; return result; } +static void setError(FFLogoImageResult result, const char** error) { + if (error) { + *error = result == FF_LOGO_IMAGE_RESULT_INIT_ERROR + ? "Image Magick library not found" + : "Failed to load / convert the image source"; + } +} + +bool ffImageCreateIM6(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im6EncodeImage(requestData, "RGBA", 5, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + out->data = blob; + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} + +bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im6EncodeImage(requestData, "SIXEL", 6, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + ffStrbufSetNS(out, (uint32_t) length, (const char*) blob); + return true; +} + #endif diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c index b44241ce5c..ab8ae94bba 100644 --- a/src/logo/image/im7.c +++ b/src/logo/image/im7.c @@ -2,8 +2,10 @@ #include "image.h" #include "common/library.h" + #include "common/mallocHelper.h" #include + #include static FF_LIBRARY_SYMBOL(ResizeImage) @@ -11,7 +13,10 @@ static FF_LIBRARY_SYMBOL(ResizeImage) return ffResizeImage(image, width, height, UndefinedFilter, exceptionInfo); } -FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { +// Decode the image source, resize it to the requested pixel size and export it as a blob +// in the given ImageMagick format ("RGBA" for kitty / chafa, "SIXEL" for sixel output). +// On success requestData->logoPixelWidth / logoPixelHeight hold the real image dimensions. +static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const char* magick, uint32_t magickLength, void** outBlob, size_t* outLength) { // clang-format off #if _WIN32 FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, @@ -27,13 +32,137 @@ FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { // clang-format on FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { - .resizeFunc = logoResize, - .library = imageMagick, - }); + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - imageMagick = nullptr; // leak imageMagick to prevent fastfetch from crashing #552 + FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_RUN_ERROR; + ExceptionInfo* exceptionInfo = nullptr; + Image* image = nullptr; + ImageInfo* imageInfoOut = nullptr; + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + + ffMagickCoreGenesis(nullptr, MagickFalse); + + exceptionInfo = ffAcquireExceptionInfo(); + if (exceptionInfo == nullptr) { + goto cleanup; + } + + { + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == nullptr) { + goto cleanup; + } + + //+1, because we need to copy the null byte too + ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); + + image = ffReadImage(imageInfoIn, exceptionInfo); + ffDestroyImageInfo(imageInfoIn); + if (image == nullptr) { + goto cleanup; + } + } + + if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { + requestData->logoPixelWidth = (uint32_t) image->columns; + requestData->logoPixelHeight = (uint32_t) image->rows; + } else if (requestData->logoPixelWidth == 0) { + requestData->logoPixelWidth = (uint32_t) ((double) image->columns / (double) image->rows * requestData->logoPixelHeight); + } else if (requestData->logoPixelHeight == 0) { + requestData->logoPixelHeight = (uint32_t) ((double) image->rows / (double) image->columns * requestData->logoPixelWidth); + } + + if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0) { + goto cleanup; + } + + { + Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); + ffDestroyImage(image); + image = resized; + if (image == nullptr) { + goto cleanup; + } + } + + imageInfoOut = ffAcquireImageInfo(); + if (imageInfoOut == nullptr) { + goto cleanup; + } + + ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); + if (blob == nullptr || length == 0) { + goto cleanup; + } + + *outBlob = blob; + *outLength = length; + blob = nullptr; // Ownership is transferred to the caller + result = FF_LOGO_IMAGE_RESULT_SUCCESS; + +cleanup: + if (imageInfoOut) { + ffDestroyImageInfo(imageInfoOut); + } + if (image) { + ffDestroyImage(image); + } + if (exceptionInfo) { + ffDestroyExceptionInfo(exceptionInfo); + } + ffMagickCoreTerminus(); + + // leak imageMagick to prevent fastfetch from crashing #552 + imageMagick = nullptr; return result; } +static void setError(FFLogoImageResult result, const char** error) { + if (error) { + *error = result == FF_LOGO_IMAGE_RESULT_INIT_ERROR + ? "Image Magick library not found" + : "Failed to load / convert the image source"; + } +} + +bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im7EncodeImage(requestData, "RGBA", 5, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + out->data = blob; + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} + +bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im7EncodeImage(requestData, "SIXEL", 6, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + ffStrbufSetNS(out, (uint32_t) length, (const char*) blob); + return true; +} + #endif diff --git a/src/logo/image/image.c b/src/logo/image/image.c index c97e1f7ba9..69973f1c86 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -1,5 +1,6 @@ #include "image.h" #include "common/io.h" +#include "common/mallocHelper.h" #include "common/printing.h" #include "common/processing.h" #include "common/strutil.h" @@ -340,7 +341,7 @@ static bool printImageKittyDirect(bool printError) { return true; } -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) #define FF_KITTY_MAX_CHUNK_SIZE 4096 @@ -351,6 +352,7 @@ static bool printImageKittyDirect(bool printError) { #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu" #define FF_CACHE_FILE_CHAFA "chafa" + #include #include #include #include @@ -364,7 +366,6 @@ static bool printImageKittyDirect(bool printError) { #ifdef FF_HAVE_ZLIB #include "common/library.h" - #include #include static bool compressBlob(void** blob, size_t* length) { @@ -372,13 +373,21 @@ static bool compressBlob(void** blob, size_t* length) { FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false) FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false) - uLong compressedLength = ffcompressBound(*length); + #if _WIN32 + // zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be + // compressed through this API; reject it instead of silently truncating + if (*length > (size_t) ULONG_MAX) { + return false; + } + #endif + + uLong compressedLength = ffcompressBound((uLong) *length); void* compressed = malloc(compressedLength); if (compressed == nullptr) { return false; } - if (ffcompress2(compressed, &compressedLength, *blob, *length, Z_BEST_COMPRESSION) != Z_OK) { + if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_BEST_COMPRESSION) != Z_OK) { free(compressed); return false; } @@ -392,36 +401,6 @@ static bool compressBlob(void** blob, size_t* length) { #endif // FF_HAVE_ZLIB - // We use only the defines from here, that are exactly the same in both versions - #ifdef FF_HAVE_IMAGEMAGICK7 - #include - #else - #include - #endif - -typedef struct ImageData { - FF_LIBRARY_SYMBOL(CopyMagickString) - FF_LIBRARY_SYMBOL(ImageToBlob) - FF_LIBRARY_SYMBOL(Base64Encode) - - ImageInfo* imageInfo; - Image* image; - ExceptionInfo* exceptionInfo; -} ImageData; - -static inline bool checkAllocationResult(void* data, size_t length) { - if (data == nullptr) { - return false; - } - - if (length == 0) { - free(data); - return false; - } - - return true; -} - static void writeCacheStrbuf(FFLogoRequestData* requestData, const FFstrbuf* value, const char* cacheFileName) { uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, cacheFileName); @@ -473,22 +452,18 @@ static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* res } } -static bool printImageSixel(FFLogoRequestData* requestData, const ImageData* imageData) { - imageData->ffCopyMagickString(imageData->imageInfo->magick, "SIXEL", 6); +// The backends report the real pixel dimensions; the character dimensions are derived here +static void fillCharacterDimensions(FFLogoRequestData* requestData) { + requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth); + requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight); +} - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { +static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) { + if (result->length == 0) { return false; } - FFstrbuf result; - result.chars = (char*) blob; - result.length = (uint32_t) length; - - printImagePixels(requestData, &result, FF_CACHE_FILE_SIXEL); - - free(blob); + printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL); return true; } @@ -509,14 +484,13 @@ static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length *blob += chunkSize; } -static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* imageData) { - imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5); - - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { +static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* buffer) { + size_t length = (size_t) buffer->width * buffer->height * 4; + FF_AUTO_FREE void* blob = malloc(length); + if (blob == nullptr) { return false; } + memcpy(blob, buffer->data, length); #ifdef FF_HAVE_ZLIB bool isCompressed = compressBlob(&blob, &length); @@ -524,18 +498,16 @@ static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* ima bool isCompressed = false; #endif - char* chars = imageData->ffBase64Encode(blob, length, &length); - free(blob); - if (!checkAllocationResult(chars, length)) { - return false; - } + // base64 output is 4 * ceil(length / 3) bytes, plus the terminating null byte + FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + length * 4 / 3)); + ffBase64EncodeRaw((uint32_t) length, (const char*) blob, &base64.length, base64.chars); - FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA((uint32_t) (length + 1024)); + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(base64.length + 1024); - const char* currentPos = chars; - size_t remainingLength = length; + const char* currentPos = base64.chars; + size_t remainingLength = base64.length; - ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", requestData->logoPixelWidth, requestData->logoPixelHeight); + ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", buffer->width, buffer->height); if (isCompressed) { ffStrbufAppendS(&result, ",o=z"); } @@ -545,19 +517,17 @@ static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* ima } printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED); - - free(chars); return true; } #ifdef FF_HAVE_CHAFA #include -static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* imageData) { - #if _WIN32 +static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* buffer) { + #if _WIN32 FF_LIBRARY_LOAD(chafa, false, "libchafa-0" FF_LIBRARY_EXTENSION, 0) - #else + #else FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1) - #endif + #endif FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_new, false) @@ -570,13 +540,6 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_unref, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_unref, false) - imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5); - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { - return false; - } - ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new(); GError* error = nullptr; if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) { @@ -616,10 +579,10 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima ffchafa_canvas_draw_all_pixels( canvas, CHAFA_PIXEL_RGBA8_UNASSOCIATED, - blob, - (gint) imageData->image->columns, - (gint) imageData->image->rows, - (gint) imageData->image->columns * 4); + buffer->data, + (gint) buffer->width, + (gint) buffer->height, + (gint) buffer->width * 4); GString* str = ffchafa_canvas_print(canvas, nullptr); FFstrbuf result; @@ -650,101 +613,62 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima } #endif -FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData) { - FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - - ImageData imageData; - - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, Base64Encode, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - - ffMagickCoreGenesis(nullptr, MagickFalse); - - imageData.exceptionInfo = ffAcquireExceptionInfo(); - if (imageData.exceptionInfo == nullptr) { - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; +bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + #ifdef _WIN32 + return ffImageCreateWIC(requestData, out, error); + #else + #ifdef FF_HAVE_IMAGEMAGICK7 + if (ffImageCreateIM7(requestData, out, error)) { + return true; } - - ImageInfo* imageInfoIn = ffAcquireImageInfo(); - if (imageInfoIn == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + #endif + #ifdef FF_HAVE_IMAGEMAGICK6 + if (ffImageCreateIM6(requestData, out, error)) { + return true; } + #endif + return false; + #endif +} - //+1, because we need to copy the null byte too - imageData.ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); - - imageData.image = ffReadImage(imageInfoIn, imageData.exceptionInfo); - ffDestroyImageInfo(imageInfoIn); - if (imageData.image == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; - } +void ffImageDestroy(FFImageBuffer* buffer) { + free(buffer->data); + buffer->data = nullptr; + buffer->width = 0; + buffer->height = 0; +} - if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { - requestData->logoPixelWidth = (uint32_t) imageData.image->columns; - requestData->logoPixelHeight = (uint32_t) imageData.image->rows; - } else if (requestData->logoPixelWidth == 0) { - requestData->logoPixelWidth = (uint32_t) ((double) imageData.image->columns / (double) imageData.image->rows * requestData->logoPixelHeight); - } else if (requestData->logoPixelHeight == 0) { - requestData->logoPixelHeight = (uint32_t) ((double) imageData.image->rows / (double) imageData.image->columns * requestData->logoPixelWidth); +bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + #ifdef _WIN32 + // Windows: WIC decodes and resizes to RGBA, then the embedded libsixel encoder takes over + #ifdef FF_HAVE_SIXEL + FFImageBuffer buffer = {}; + if (!ffImageCreate(requestData, &buffer, error)) { + return false; } - - requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth); - requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight); - - if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0 || requestData->logoCharacterWidth == 0 || requestData->logoCharacterHeight == 0) { - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + bool ok = ffSixelEncode(&buffer, out, error); + ffImageDestroy(&buffer); + return ok; + #else + if (error) { + *error = "sixel support is not compiled in"; } - - Image* resized = imData->resizeFunc(imageData.image, requestData->logoPixelWidth, requestData->logoPixelHeight, imageData.exceptionInfo); - ffDestroyImage(imageData.image); - if (resized == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + return false; + #endif + #else + // Off Windows: ImageMagick encodes straight from the decoded image, without an RGBA round trip + #ifdef FF_HAVE_IMAGEMAGICK7 + if (ffImageSixelEncodeIM7(requestData, out, error)) { + return true; } - imageData.image = resized; - - imageData.imageInfo = ffAcquireImageInfo(); - if (imageData.imageInfo == nullptr) { - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + #endif + #ifdef FF_HAVE_IMAGEMAGICK6 + if (ffImageSixelEncodeIM6(requestData, out, error)) { + return true; } - - bool printSuccessful = false; - if (requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA) { - #if FF_HAVE_CHAFA - printSuccessful = printImageChafa(requestData, &imageData); + #endif + return false; #endif - } else if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) { - printSuccessful = printImageKitty(requestData, &imageData); - } else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) { - printSuccessful = printImageSixel(requestData, &imageData); - } - - ffDestroyImageInfo(imageData.imageInfo); - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - - return printSuccessful ? FF_LOGO_IMAGE_RESULT_SUCCESS : FF_LOGO_IMAGE_RESULT_RUN_ERROR; } static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) { @@ -937,6 +861,17 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { ffStrbufRecalculateLength(&requestData.cacheDir); ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/'); + + // The cache is indexed by source path and pixel size only, so it has to be namespaced + // by backend: different backends (and different sixel encoders) produce different bytes + #ifdef _WIN32 + ffStrbufAppendS(&requestData.cacheDir, "wic/"); + #elif defined(FF_HAVE_IMAGEMAGICK7) + ffStrbufAppendS(&requestData.cacheDir, "im7/"); + #elif defined(FF_HAVE_IMAGEMAGICK6) + ffStrbufAppendS(&requestData.cacheDir, "im6/"); + #endif + ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight); if (!instance.config.logo.recache) { @@ -949,30 +884,40 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { } } - FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_INIT_ERROR; + const char* error = nullptr; + bool printSuccessful = false; - #ifdef FF_HAVE_IMAGEMAGICK7 - result = ffLogoPrintImageIM7(&requestData); + if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) { + // The sixel encoder belongs to the backend, so it is not fed through ffImageCreate + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + if (ffImageSixelEncode(&requestData, &result, &error)) { + fillCharacterDimensions(&requestData); + printSuccessful = printImageSixel(&requestData, &result); + } + } else { + FFImageBuffer buffer = {}; + if (ffImageCreate(&requestData, &buffer, &error)) { + fillCharacterDimensions(&requestData); + if (requestData.type == FF_LOGO_TYPE_IMAGE_KITTY) { + printSuccessful = printImageKitty(&requestData, &buffer); + } + #if FF_HAVE_CHAFA + else if (requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA) { + printSuccessful = printImageChafa(&requestData, &buffer); + } #endif - - #ifdef FF_HAVE_IMAGEMAGICK6 - if (result == FF_LOGO_IMAGE_RESULT_INIT_ERROR) { - result = ffLogoPrintImageIM6(&requestData); + ffImageDestroy(&buffer); + } } - #endif ffStrbufDestroy(&requestData.cacheDir); - if (result == FF_LOGO_IMAGE_RESULT_SUCCESS) { + if (printSuccessful) { return true; } if (printError) { - if (result == FF_LOGO_IMAGE_RESULT_INIT_ERROR) { - fputs("Logo: Image Magick library not found\n", stderr); - } else { - fputs("Logo: Failed to load / convert the image source\n", stderr); - } + fprintf(stderr, "Logo: %s\n", error ? error : "Failed to load / convert the image source"); } return false; @@ -1024,7 +969,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { } #endif -#if !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) +#if !defined(_WIN32) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) if (printError) { fputs("Logo: Image Magick support is not compiled in\n", stderr); } diff --git a/src/logo/image/image.h b/src/logo/image/image.h index 5fbd39a004..ff4d4dbcf3 100644 --- a/src/logo/image/image.h +++ b/src/logo/image/image.h @@ -2,7 +2,7 @@ #include "../logo.h" -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) typedef enum FFLogoImageResult: uint8_t { FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed @@ -24,20 +24,42 @@ typedef struct FFLogoRequestData { uint32_t logoCharacterWidth; } FFLogoRequestData; -typedef struct FFIMData { - void* library; - void* (*resizeFunc)(const void* image, size_t width, size_t height, void* exceptionInfo); -} FFIMData; +// Decoded and resized image: RGBA8, straight (unassociated) alpha, no row padding +typedef struct FFImageBuffer { + uint8_t* data; + uint32_t width; + uint32_t height; +} FFImageBuffer; -FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData); +// Backend contract. Both functions update requestData->logoPixelWidth / logoPixelHeight +// with the real dimensions of the produced image, so the caller can derive the character +// dimensions afterwards. + +// Decode the image source and resize it to the requested pixel size +bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +void ffImageDestroy(FFImageBuffer* buffer); + +// Encode the image source as a sixel byte stream (including the DCS envelope). +// The encoder belongs to the backend: ImageMagick's SIXEL coder off Windows, +// the embedded libsixel on Windows. +bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); #endif #ifdef FF_HAVE_IMAGEMAGICK7 -FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData); +bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); #endif #ifdef FF_HAVE_IMAGEMAGICK6 - #include -FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData); +bool ffImageCreateIM6(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); +#endif + +#ifdef _WIN32 +bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +#endif + +#ifdef FF_HAVE_SIXEL +bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error); #endif diff --git a/src/logo/image/sixel.c b/src/logo/image/sixel.c new file mode 100644 index 0000000000..3b0c0ae83c --- /dev/null +++ b/src/logo/image/sixel.c @@ -0,0 +1,54 @@ +#include "image.h" + +#ifdef FF_HAVE_SIXEL + + #include // src/3rdparty/sixel/sixel.h + +static int sixelWriteCallback(char* data, int size, void* priv) { + ffStrbufAppendNS((FFstrbuf*) priv, (uint32_t) size, data); + return 1; // non-zero means "keep going" +} + +bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error) { + sixel_dither_t* dither = nullptr; + if (sixel_dither_new(&dither, 256, nullptr) != SIXEL_OK) { + if (error) { + *error = "sixel_dither_new() failed"; + } + return false; + } + + // Feed RGBA8 directly and let libsixel do the palette quantization and dithering + if (sixel_dither_initialize(dither, buffer->data, (int) buffer->width, (int) buffer->height, SIXEL_PIXELFORMAT_RGBA8888, SIXEL_LARGE_AUTO, SIXEL_REP_AUTO, SIXEL_QUALITY_HIGH) != SIXEL_OK) { + sixel_dither_unref(dither); + if (error) { + *error = "sixel_dither_initialize() failed"; + } + return false; + } + + sixel_output_t* output = nullptr; + if (sixel_output_new(&output, sixelWriteCallback, result, nullptr) != SIXEL_OK) { + sixel_dither_unref(dither); + if (error) { + *error = "sixel_output_new() failed"; + } + return false; + } + + // The depth parameter is unused by libsixel; the DCS envelope is emitted by default + SIXELSTATUS status = sixel_encode(buffer->data, (int) buffer->width, (int) buffer->height, 4, dither, output); + + sixel_output_unref(output); + sixel_dither_unref(dither); + + if (status != SIXEL_OK || result->length == 0) { + if (error) { + *error = "sixel_encode() failed"; + } + return false; + } + return true; +} + +#endif diff --git a/src/logo/image/wic.cpp b/src/logo/image/wic.cpp new file mode 100644 index 0000000000..cac0b2b1d7 --- /dev/null +++ b/src/logo/image/wic.cpp @@ -0,0 +1,127 @@ +extern "C" { +#include "image.h" +#include "common/mallocHelper.h" +#include "common/windows/com.h" +#include "common/windows/nt.h" +} + +#include +#include +#include + +bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + const char* comError = ffInitCom(); + if (comError) { + if (error) *error = comError; + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapScaler* scaler = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* premultiplyConverter = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* converter = nullptr; + + if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**) &factory))) { + if (error) *error = "WIC imaging factory is unavailable"; + return false; + } + + // The source path is UTF-8, WIC only accepts UTF-16 + wchar_t widePath[MAX_PATH + 1]; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(widePath, (ULONG) sizeof(widePath), nullptr, + instance.config.logo.source.chars, (ULONG) instance.config.logo.source.length + 1))) { + if (error) *error = "failed to convert the image path to UTF-16"; + return false; + } + + if (FAILED(factory->CreateDecoderFromFilename(widePath, nullptr, GENERIC_READ, + WICDecodeMetadataCacheOnDemand, &decoder))) { + if (error) *error = "unsupported or unreadable image format"; + return false; + } + + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) + if (FAILED(decoder->GetFrame(0, &frame))) { + if (error) *error = "failed to get the first frame"; + return false; + } + + UINT sourceWidth = 0, sourceHeight = 0; + frame->GetSize(&sourceWidth, &sourceHeight); + if (sourceWidth == 0 || sourceHeight == 0) { + if (error) *error = "invalid image dimensions"; + return false; + } + + // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = sourceWidth; + height = sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + + if (width == 0 || height == 0) { + if (error) *error = "invalid target dimensions"; + return false; + } + + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + + IWICBitmapSource* source = nullptr; + if (width == sourceWidth && height == sourceHeight) { + // Same size: don't resample. ImageMagick clones the image in this case too, + // and resampling would only blur it + source = frame; + } else { + // Scale premultiplied alpha to avoid transparent RGB values bleeding into + // the visible edge pixels. The final output is converted back to straight + // alpha below, matching FFImageBuffer's RGBA8 contract. + if (FAILED(factory->CreateFormatConverter(&premultiplyConverter)) || + FAILED(premultiplyConverter->Initialize(frame, GUID_WICPixelFormat32bppPBGRA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) || + FAILED(factory->CreateBitmapScaler(&scaler)) || + FAILED(scaler->Initialize(premultiplyConverter, width, height, + (WICBitmapInterpolationMode) 0x4 /* WICBitmapInterpolationModeHighQualityCubic */))) { + if (error) *error = "image scaling failed"; + return false; + } + source = scaler; + } + + // Normalize to straight-alpha RGBA8: kitty (f=32) and chafa + // (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume exactly this + if (FAILED(factory->CreateFormatConverter(&converter)) || + FAILED(converter->Initialize(source, GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom))) { + if (error) *error = "pixel format conversion failed"; + return false; + } + + UINT stride = width * 4; + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc((size_t) stride * height); + if (pixels == nullptr) { + if (error) *error = "out of memory"; + return false; + } + + // prc == nullptr means the whole image; WIC fills the buffer using the stride we pass in + if (FAILED(converter->CopyPixels(nullptr, stride, stride * height, pixels))) { + if (error) *error = "pixel copy failed"; + return false; + } + + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; // Ownership is transferred to `out` + return true; +} From 35ffdb3995a860a40baef87fb9f2c6b71afdf1ae Mon Sep 17 00:00:00 2001 From: Carter Li Date: Mon, 14 Sep 2026 10:59:03 +0800 Subject: [PATCH 35/76] Logo (Image): drops IM in favor of ImageIO on macOS --- CMakeLists.txt | 11 ++- src/logo/image/image.c | 16 ++-- src/logo/image/image.h | 6 +- src/logo/image/imageio.c | 185 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 src/logo/image/imageio.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 98c0058227..25e2e4dc08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,10 +88,10 @@ 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 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_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows only; replaces ImageMagick's SIXEL coder)" ON "WIN32" OFF) -cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32" OFF) +cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF) +cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "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 and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF) +cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" 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) @@ -1023,6 +1023,7 @@ elseif(APPLE) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_apple.c src/detection/camera/camera_apple.m + src/logo/image/imageio.c ) # CMAKE_SYSTEM_PROCESSOR has been normalized before if(CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64") @@ -1887,6 +1888,8 @@ elseif(APPLE) PRIVATE "-framework CoreAudio" PRIVATE "-framework CoreMedia" PRIVATE "-framework CoreVideo" + PRIVATE "-framework ImageIO" + PRIVATE "-framework Accelerate" PRIVATE "-framework CoreWLAN" PRIVATE "-framework IOBluetooth" PRIVATE "-framework IOKit" diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 69973f1c86..21a46ae890 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -341,7 +341,7 @@ static bool printImageKittyDirect(bool printError) { return true; } -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) #define FF_KITTY_MAX_CHUNK_SIZE 4096 @@ -616,6 +616,8 @@ static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { #ifdef _WIN32 return ffImageCreateWIC(requestData, out, error); + #elif defined(__APPLE__) + return ffImageCreateImageIO(requestData, out, error); #else #ifdef FF_HAVE_IMAGEMAGICK7 if (ffImageCreateIM7(requestData, out, error)) { @@ -639,8 +641,10 @@ void ffImageDestroy(FFImageBuffer* buffer) { } bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { - #ifdef _WIN32 - // Windows: WIC decodes and resizes to RGBA, then the embedded libsixel encoder takes over + // Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded + // libsixel encoder takes over. Other platforms let ImageMagick encode straight from the + // decoded image without an RGBA round trip. + #if defined(_WIN32) || defined(__APPLE__) #ifdef FF_HAVE_SIXEL FFImageBuffer buffer = {}; if (!ffImageCreate(requestData, &buffer, error)) { @@ -650,13 +654,13 @@ bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const cha ffImageDestroy(&buffer); return ok; #else + FF_UNUSED(requestData, out); if (error) { *error = "sixel support is not compiled in"; } return false; #endif #else - // Off Windows: ImageMagick encodes straight from the decoded image, without an RGBA round trip #ifdef FF_HAVE_IMAGEMAGICK7 if (ffImageSixelEncodeIM7(requestData, out, error)) { return true; @@ -866,6 +870,8 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { // by backend: different backends (and different sixel encoders) produce different bytes #ifdef _WIN32 ffStrbufAppendS(&requestData.cacheDir, "wic/"); + #elif defined(__APPLE__) + ffStrbufAppendS(&requestData.cacheDir, "imageio/"); #elif defined(FF_HAVE_IMAGEMAGICK7) ffStrbufAppendS(&requestData.cacheDir, "im7/"); #elif defined(FF_HAVE_IMAGEMAGICK6) @@ -969,7 +975,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { } #endif -#if !defined(_WIN32) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) if (printError) { fputs("Logo: Image Magick support is not compiled in\n", stderr); } diff --git a/src/logo/image/image.h b/src/logo/image/image.h index ff4d4dbcf3..f096dd7a85 100644 --- a/src/logo/image/image.h +++ b/src/logo/image/image.h @@ -2,7 +2,7 @@ #include "../logo.h" -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) typedef enum FFLogoImageResult: uint8_t { FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed @@ -60,6 +60,10 @@ bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); #endif +#ifdef __APPLE__ +bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +#endif + #ifdef FF_HAVE_SIXEL bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error); #endif diff --git a/src/logo/image/imageio.c b/src/logo/image/imageio.c new file mode 100644 index 0000000000..a86e0ccc8a --- /dev/null +++ b/src/logo/image/imageio.c @@ -0,0 +1,185 @@ +#include "image.h" +#include "common/mallocHelper.h" +#include "common/apple/cf_helpers.h" + +#include +#include +#include + +bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + FF_CFTYPE_AUTO_RELEASE CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) instance.config.logo.source.chars, + (CFIndex) instance.config.logo.source.length, + false); + if (url == nullptr) { + if (error) { + *error = "failed to create the image URL"; + } + return false; + } + + FF_CFTYPE_AUTO_RELEASE CGImageSourceRef source = CGImageSourceCreateWithURL(url, nullptr); + if (source == nullptr) { + if (error) { + *error = "unsupported or unreadable image format"; + } + return false; + } + + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) + FF_CFTYPE_AUTO_RELEASE CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, nullptr); + if (image == nullptr) { + if (error) { + *error = "failed to get the first frame"; + } + return false; + } + + size_t sourceWidth = CGImageGetWidth(image); + size_t sourceHeight = CGImageGetHeight(image); + if (sourceWidth == 0 || sourceHeight == 0) { + if (error) { + *error = "invalid image dimensions"; + } + return false; + } + + // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = (uint32_t) sourceWidth; + height = (uint32_t) sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + + if (width == 0 || height == 0) { + if (error) { + *error = "invalid target dimensions"; + } + return false; + } + + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + + const bool sameSize = width == sourceWidth && height == sourceHeight; + + // Decode straight into RGBA for the same-size fast path. For resizing, decode + // into premultiplied ARGB so interpolation does not bleed transparent RGB into + // visible edge pixels. + // vImageBuffer_InitWithCGImage handles format conversion, color management and + // byte order in one call. NULL colorSpace means sRGB, matching kCGColorSpaceSRGB. + const vImage_CGImageFormat format = { + .bitsPerComponent = 8, + .bitsPerPixel = 32, + .colorSpace = nullptr, + .bitmapInfo = (CGBitmapInfo) ((sameSize ? kCGImageAlphaLast : kCGImageAlphaPremultipliedFirst) | kCGImageByteOrder32Big), + .version = 0, + .decode = nullptr, + .renderingIntent = kCGRenderingIntentDefault + }; + + vImage_Buffer src = {}; + vImage_Error vErr = vImageBuffer_InitWithCGImage(&src, (vImage_CGImageFormat*) &format, nullptr, image, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to decode the image with vImage"; + } + return false; + } + + // Final output buffer: straight (unassociated) RGBA8888 at the target size. + const size_t dstStride = (size_t) width * 4; + if (sameSize) { + // The requested format is already straight RGBA. If vImage did not add + // row padding, transfer its allocation directly to the caller; otherwise + // copy rows into the required tightly packed FFImageBuffer allocation. + if (src.rowBytes == dstStride) { + out->data = (uint8_t*) src.data; + out->width = width; + out->height = height; + src.data = nullptr; + return true; + } + + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + if (pixels == nullptr) { + free(src.data); + if (error) { + *error = "out of memory"; + } + return false; + } + for (uint32_t y = 0; y < height; ++y) { + memcpy(pixels + (size_t) y * dstStride, + (const uint8_t*) src.data + (size_t) y * src.rowBytes, + dstStride); + } + free(src.data); + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; + return true; + } else { + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + if (pixels == nullptr) { + free(src.data); + if (error) { + *error = "out of memory"; + } + return false; + } + + const vImage_Buffer dst = { + .data = pixels, + .width = width, + .height = height, + .rowBytes = dstStride + }; + + // Resample while still premultiplied: interpolating premultiplied data is the + // correct way to scale (it avoids the dark fringes you get from averaging + // straight-alpha RGB). NULL temp buffer lets vImage allocate internally. + vErr = vImageScale_ARGB8888(&src, &dst, nullptr, kvImageHighQualityResampling); + free(src.data); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to scale the image"; + } + return false; + } + + // Un-premultiply in place (pointwise, alpha == 0 is handled safely) so the result + // matches the kitty f=32 / chafa CHAFA_PIXEL_RGBA8_UNASSOCIATED contract. + vErr = vImageUnpremultiplyData_ARGB8888(&dst, &dst, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to un-premultiply the image"; + } + return false; + } + + // Reorder ARGB -> RGBA in place (permute supports in-place when data/rowBytes match). + // The downstream consumers and the WIC backend all expect R,G,B,A byte order. + const uint8_t permuteMap[4] = { 1, 2, 3, 0 }; // A,R,G,B -> R,G,B,A + vErr = vImagePermuteChannels_ARGB8888(&dst, &dst, permuteMap, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to reorder image channels"; + } + return false; + } + + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; // Ownership is transferred to `out` + return true; + } +} From e3b3b5dfc659b8425377416cf3ff910731d84914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Mon, 14 Sep 2026 19:25:10 +0800 Subject: [PATCH 36/76] Packages: fixes a copy-paste bug --- src/modules/packages/packages.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/packages/packages.c b/src/modules/packages/packages.c index 6edf2a5a7e..cae7da8b4b 100644 --- a/src/modules/packages/packages.c +++ b/src/modules/packages/packages.c @@ -155,7 +155,7 @@ bool ffPrintPackages(FFPackagesOptions* options) { FF_ARG(counts.brewCask, "brew-cask"), FF_ARG(counts.cards, "cards"), FF_ARG(counts.choco, "choco"), - FF_ARG(counts.choco, "crux"), + FF_ARG(counts.crux, "crux"), FF_ARG(counts.dpkg, "dpkg"), FF_ARG(counts.emerge, "emerge"), FF_ARG(counts.eopkg, "eopkg"), From 375d0c6b254dd5e84b98d5bf8f8ba46169a66f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Mon, 14 Sep 2026 22:21:49 +0800 Subject: [PATCH 37/76] Common (IO): adds `ffPathGetMtime` and uses it --- src/common/impl/io_unix.c | 16 ++++++++++++ src/common/impl/io_windows.c | 17 +++++++++++++ src/common/io.h | 4 +++ src/detection/packages/packages.c | 41 +++++++------------------------ 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c index 840df3be68..83ef3e8d0f 100644 --- a/src/common/impl/io_unix.c +++ b/src/common/impl/io_unix.c @@ -11,6 +11,7 @@ #include #else #include + #define st_mtim st_mtimespec // `struct stat` spells it `st_mtimespec` on Apple #endif #if FF_HAVE_WORDEXP @@ -331,3 +332,18 @@ FFNativeFD ffGetNullFD(void) { 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; +} diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index b9691eb39e..65115a34d9 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -1,6 +1,7 @@ #include "fastfetch.h" #include "common/io.h" #include "common/strutil.h" +#include "common/time.h" #include "common/windows/nt.h" #include "common/windows/unicode.h" @@ -499,3 +500,19 @@ FFNativeFD ffGetNullFD(void) { bool ffRemoveFile(const char* fileName) { return DeleteFileA(fileName) != FALSE; } + +uint64_t ffPathGetMtime(const char* path) { + FF_AUTO_CLOSE_FD HANDLE handle = CreateFileA(path, 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); +} diff --git a/src/common/io.h b/src/common/io.h index 9086bea43e..7a8c7e7b05 100644 --- a/src/common/io.h +++ b/src/common/io.h @@ -291,3 +291,7 @@ static inline void wrapClosedir(HANDLE* pdir) { FFNativeFD ffGetNullFD(void); bool ffRemoveFile(const char* fileName); +// Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read. +// The representation is uniform across platforms so that a value derived from it means the +// same thing everywhere, which matters for callers that store it as a cache key. +[[gnu::nonnull(1)]] uint64_t ffPathGetMtime(const char* path); diff --git a/src/detection/packages/packages.c b/src/detection/packages/packages.c index c72ea87577..a508342d8c 100644 --- a/src/detection/packages/packages.c +++ b/src/detection/packages/packages.c @@ -1,14 +1,9 @@ #include "packages.h" #include "common/io.h" -#include "common/time.h" #include #include -#ifdef __APPLE__ - #define st_mtim st_mtimespec -#endif - void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options); const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options) { @@ -22,38 +17,20 @@ const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* option } bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result) { -#ifndef _WIN32 - struct stat st; - if (stat(filePath, &st) < 0) // file doesn't exist or isn't accessible - { - *result = 0; - return true; - } - - if (__builtin_expect(st.st_mtim.tv_sec <= 0, false)) { - return false; - } - - uint64_t mtime_current = (uint64_t) st.st_mtim.tv_sec * 1000ull + (uint64_t) st.st_mtim.tv_nsec / 1000000ull; -#else - FF_AUTO_CLOSE_FD HANDLE handle = CreateFileA(filePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + const uint64_t mtime_current = ffPathGetMtime(filePath); + if (__builtin_expect(mtime_current == 0, false)) { + // A missing database legitimately means "no packages installed", and must not be cached. + // A database whose modification time could not be read must not be cached either, and + // reporting 0 for it would be a lie. ffPathGetMtime() reports 0 for both, so tell them + // apart before deciding which of the two applies. + if (ffPathExists(filePath, FF_PATHTYPE_FILE)) { + return false; + } - if (handle == INVALID_HANDLE_VALUE) // file doesn't exist or isn't accessible - { *result = 0; return true; } - uint64_t mtime_current; - FILE_BASIC_INFORMATION fileInfo; - IO_STATUS_BLOCK iosb; - if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileBasicInformation))) { - return false; - } - - mtime_current = ffFileTimeToUnixMs((uint64_t) fileInfo.LastWriteTime.QuadPart); -#endif - ffStrbufSet(cacheDir, &instance.state.platform.cacheDir); ffStrbufEnsureEndsWithC(cacheDir, '/'); ffStrbufAppendF(cacheDir, "fastfetch/packages/%s.txt", packageId); From a90ae0249d3b302dc259fa5e58e6443821cc9fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Mon, 14 Sep 2026 22:42:16 +0800 Subject: [PATCH 38/76] IO (Windows): prefers unicode APIs --- src/common/impl/io_windows.c | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index 65115a34d9..48380c2ffa 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -1,4 +1,5 @@ #include "fastfetch.h" +#include "common/debug.h" #include "common/io.h" #include "common/strutil.h" #include "common/time.h" @@ -160,7 +161,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; } @@ -498,11 +501,25 @@ FFNativeFD ffGetNullFD(void) { } bool ffRemoveFile(const char* fileName) { - return DeleteFileA(fileName) != FALSE; + wchar_t fileNameW[MAX_PATH]; + 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) { - FF_AUTO_CLOSE_FD HANDLE handle = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + wchar_t fileNameW[MAX_PATH]; + 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; From 3d7ea720fb561ac1f2ee2e0eb3bac645d23a5fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Mon, 14 Sep 2026 22:43:19 +0800 Subject: [PATCH 39/76] Logo (Image): improves caching logic; takes mtime into account --- src/logo/image/image.c | 104 +++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 21a46ae890..514a0bc88a 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -351,6 +351,9 @@ static bool printImageKittyDirect(bool printError) { #define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc" #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu" #define FF_CACHE_FILE_CHAFA "chafa" + // Modification time of the image source the entry was produced from. Written last, so an + // entry that was interrupted mid-write is never mistaken for a complete one. + #define FF_CACHE_FILE_MTIME "mtime" #include #include @@ -401,20 +404,13 @@ static bool compressBlob(void** blob, size_t* length) { #endif // FF_HAVE_ZLIB -static void writeCacheStrbuf(FFLogoRequestData* requestData, const FFstrbuf* value, const char* cacheFileName) { +static void writeCacheData(FFLogoRequestData* requestData, const void* value, size_t len, const char* cacheFileName) { uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, cacheFileName); - ffWriteFileBuffer(requestData->cacheDir.chars, value); + ffWriteFileData(requestData->cacheDir.chars, len, value); ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); } -static void writeCacheUint32(FFLogoRequestData* requestData, uint32_t value, const char* cacheFileName) { - FFstrbuf content; - content.chars = (char*) &value; - content.length = sizeof(value); - writeCacheStrbuf(requestData, &content, cacheFileName); -} - static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) { const FFOptionsLogo* options = &instance.config.logo; // Calculate character dimensions @@ -422,14 +418,14 @@ static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* res instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1; // Write cache files - writeCacheStrbuf(requestData, result, cacheFileName); + writeCacheData(requestData, result->chars, result->length, cacheFileName); if (options->width == 0) { - writeCacheUint32(requestData, requestData->logoCharacterWidth, FF_CACHE_FILE_WIDTH); + writeCacheData(requestData, &requestData->logoCharacterWidth, sizeof(requestData->logoCharacterWidth), FF_CACHE_FILE_WIDTH); } if (options->height == 0) { - writeCacheUint32(requestData, requestData->logoCharacterHeight, FF_CACHE_FILE_HEIGHT); + writeCacheData(requestData, &requestData->logoCharacterHeight, sizeof(requestData->logoCharacterHeight), FF_CACHE_FILE_HEIGHT); } // Write result to stdout @@ -591,7 +587,7 @@ static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* result.chars = str->str; ffLogoPrintChars(result.chars, false); - writeCacheStrbuf(requestData, &result, FF_CACHE_FILE_CHAFA); + writeCacheData(requestData, &result.chars, result.length, FF_CACHE_FILE_CHAFA); // FIXME: These functions must be imported from `libglib` dlls on Windows FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free); @@ -691,28 +687,62 @@ static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileNam return fd; } -static void readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) { +static bool readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) { uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, cacheFileName); - ffAppendFileBuffer(requestData->cacheDir.chars, result); + bool res = ffAppendFileBuffer(requestData->cacheDir.chars, result); ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); + return res; } -static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) { - FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); - readCachedStrbuf(requestData, &content, cacheFileName); +static bool readCachedData(FFLogoRequestData* requestData, void* buffer, size_t bufferSize, const char* cacheFileName) { + uint32_t cacheDirLength = requestData->cacheDir.length; + ffStrbufAppendS(&requestData->cacheDir, cacheFileName); + bool res = ffReadFileData(requestData->cacheDir.chars, bufferSize, buffer) == (ssize_t) bufferSize; + ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); + return res; +} +static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) { uint32_t result = 0; - - if (content.length != sizeof(result)) { + if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) { return 0; } - memcpy(&result, content.chars, sizeof(result)); + return result; +} + +static uint64_t readCachedUint64(FFLogoRequestData* requestData, const char* cacheFileName) { + uint64_t result = 0; + if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) { + return 0; + } return result; } +// Drops everything a previous version of the source left in the entry directory. +// The directory is keyed on the source path and the pixel size only, so it is reused across +// edits; without this, a payload written for another logo type would be read back. +static void removeCachedFiles(FFLogoRequestData* requestData) { + static const char* const files[] = { + FF_CACHE_FILE_MTIME, + FF_CACHE_FILE_WIDTH, + FF_CACHE_FILE_HEIGHT, + FF_CACHE_FILE_SIXEL, + FF_CACHE_FILE_KITTY_COMPRESSED, + FF_CACHE_FILE_KITTY_UNCOMPRESSED, + FF_CACHE_FILE_CHAFA, + }; + + uint32_t cacheDirLength = requestData->cacheDir.length; + for (uint32_t i = 0; i < ARRAY_SIZE(files); ++i) { + ffStrbufAppendS(&requestData->cacheDir, files[i]); + ffRemoveFile(requestData->cacheDir.chars); + ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); + } +} + static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) { FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); readCachedStrbuf(requestData, &content, cacheFileName); @@ -866,21 +896,18 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { ffStrbufRecalculateLength(&requestData.cacheDir); ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/'); - // The cache is indexed by source path and pixel size only, so it has to be namespaced - // by backend: different backends (and different sixel encoders) produce different bytes - #ifdef _WIN32 - ffStrbufAppendS(&requestData.cacheDir, "wic/"); - #elif defined(__APPLE__) - ffStrbufAppendS(&requestData.cacheDir, "imageio/"); - #elif defined(FF_HAVE_IMAGEMAGICK7) - ffStrbufAppendS(&requestData.cacheDir, "im7/"); - #elif defined(FF_HAVE_IMAGEMAGICK6) - ffStrbufAppendS(&requestData.cacheDir, "im6/"); - #endif + ffStrbufAppendF(&requestData.cacheDir, "%ux%u/", requestData.logoPixelWidth, requestData.logoPixelHeight); - ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight); + // The cached payload is a rendering, not a bit-exact artefact: every backend produces a + // valid one for the same source and pixel size, so the backend is deliberately not part of + // the key. What the key does have to capture is the content of the source, which the path + // can not: the same file can be replaced in place. Hence the recorded mtime. + // 0 means the mtime could not be read, in which case the entry is never trusted. + const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars); - if (!instance.config.logo.recache) { + if (!instance.config.logo.recache && + sourceMtime != 0 && + readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime) { bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA ? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA) : printCachedPixel(&requestData); @@ -890,6 +917,10 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { } } + // Cache miss. The entry directory is keyed on the source path and the pixel size only, so + // it is reused when the source is edited; drop what the previous version left behind. + removeCachedFiles(&requestData); + const char* error = nullptr; bool printSuccessful = false; @@ -916,6 +947,11 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { } } + if (printSuccessful) { + // Written last: an entry only becomes usable once its payload is complete + writeCacheData(&requestData, &sourceMtime, sizeof(sourceMtime), FF_CACHE_FILE_MTIME); + } + ffStrbufDestroy(&requestData.cacheDir); if (printSuccessful) { From 464adb3540db786845a30fa63a8e0faf705135b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Mon, 14 Sep 2026 23:45:28 +0800 Subject: [PATCH 40/76] Logo (Image): migrates `recache` to `cache: bool|regen` --- doc/help.json | 13 +++++++++---- doc/json_schema.json | 17 +++++++++++++---- src/logo/image/image.c | 13 +++++++++++-- src/options/logo.c | 34 +++++++++++++++++++++++++++++----- src/options/logo.h | 8 +++++++- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/doc/help.json b/doc/help.json index 1d882d1330..d320a36749 100644 --- a/doc/help.json +++ b/doc/help.json @@ -291,12 +291,17 @@ } }, { - "long": "logo-recache", - "desc": "If true, regenerate the image logo cache", + "long": "logo-cache", + "desc": "Specify how the image logo cache is used", "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" + } } }, { diff --git a/doc/json_schema.json b/doc/json_schema.json index cfa85ea4c0..aa472e6afd 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -766,10 +766,19 @@ "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", diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 514a0bc88a..b9e39c7b71 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -405,6 +405,12 @@ static bool compressBlob(void** blob, size_t* length) { #endif // FF_HAVE_ZLIB static void writeCacheData(FFLogoRequestData* requestData, const void* value, size_t len, const char* cacheFileName) { + // Every payload file goes through here, including the ones written from the printing helpers, + // so this is the single place that keeps `--logo-cache false` from writing anything at all. + if (instance.config.logo.cache == FF_LOGO_CACHE_OFF) { + return; + } + uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, cacheFileName); ffWriteFileData(requestData->cacheDir.chars, len, value); @@ -905,7 +911,7 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { // 0 means the mtime could not be read, in which case the entry is never trusted. const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars); - if (!instance.config.logo.recache && + if (instance.config.logo.cache == FF_LOGO_CACHE_ON && sourceMtime != 0 && readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime) { bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA @@ -919,7 +925,10 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { // Cache miss. The entry directory is keyed on the source path and the pixel size only, so // it is reused when the source is edited; drop what the previous version left behind. - removeCachedFiles(&requestData); + // With the cache turned off the directory is left alone entirely. + if (instance.config.logo.cache != FF_LOGO_CACHE_OFF) { + removeCachedFiles(&requestData); + } const char* error = nullptr; bool printSuccessful = false; diff --git a/src/options/logo.c b/src/options/logo.c index e62bd23b75..08a35cc456 100644 --- a/src/options/logo.c +++ b/src/options/logo.c @@ -17,7 +17,7 @@ void ffOptionsInitLogo(FFOptionsLogo* options) { options->paddingRight = 4; options->printRemaining = true; options->preserveAspectRatio = false; - options->recache = false; + options->cache = FF_LOGO_CACHE_ON; options->position = FF_LOGO_POSITION_LEFT; #if FF_HAVE_CHAFA @@ -105,8 +105,15 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons options->printRemaining = ffOptionParseBoolean(value); } else if (ffStrEqualsIgnCase(subKey, "preserve-aspect-ratio")) { options->preserveAspectRatio = ffOptionParseBoolean(value); + } else if (ffStrEqualsIgnCase(subKey, "cache")) { + if (value && ffStrEqualsIgnCase(value, "regen")) { + options->cache = FF_LOGO_CACHE_REGEN; + } else { + options->cache = ffOptionParseBoolean(value) ? FF_LOGO_CACHE_ON : FF_LOGO_CACHE_OFF; + } } else if (ffStrEqualsIgnCase(subKey, "recache")) { - options->recache = ffOptionParseBoolean(value); + fputs("--logo-recache has been replaced by --logo-cache regen\n", stderr); + exit(477); } else if (ffStrEqualsIgnCase(subKey, "separate")) { fputs("--logo-separate has been renamed to --logo-position\n", stderr); exit(477); @@ -335,8 +342,14 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo } else if (unsafe_yyjson_equals_str(key, "preserveAspectRatio")) { options->preserveAspectRatio = yyjson_get_bool(val); continue; - } else if (unsafe_yyjson_equals_str(key, "recache")) { - options->recache = yyjson_get_bool(val); + } else if (unsafe_yyjson_equals_str(key, "cache")) { + if (yyjson_is_bool(val)) { + options->cache = yyjson_get_bool(val) ? FF_LOGO_CACHE_ON : FF_LOGO_CACHE_OFF; + } else if (yyjson_is_str(val) && ffStrEqualsIgnCase(yyjson_get_str(val), "regen")) { + options->cache = FF_LOGO_CACHE_REGEN; + } else { + return "Property 'logo.cache' must be a boolean or the string \"regen\""; + } continue; } else if (unsafe_yyjson_equals_str(key, "position")) { int value; @@ -521,7 +534,18 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { yyjson_mut_obj_add_bool(doc, obj, "preserveAspectRatio", options->preserveAspectRatio); - yyjson_mut_obj_add_bool(doc, obj, "recache", options->recache); + // Written the way the parser accepts it back: a boolean for on / off, the string for regen + switch (options->cache) { + case FF_LOGO_CACHE_OFF: + yyjson_mut_obj_add_bool(doc, obj, "cache", false); + break; + case FF_LOGO_CACHE_REGEN: + yyjson_mut_obj_add_str(doc, obj, "cache", "regen"); + break; + case FF_LOGO_CACHE_ON: + yyjson_mut_obj_add_bool(doc, obj, "cache", true); + break; + } yyjson_mut_obj_add_str(doc, obj, "position", ((const char*[]) { "left", diff --git a/src/options/logo.h b/src/options/logo.h index 51d5cfbfc5..c6e1eb9f62 100644 --- a/src/options/logo.h +++ b/src/options/logo.h @@ -30,6 +30,12 @@ typedef enum FFLogoPosition: uint8_t { FF_LOGO_POSITION_RIGHT, } FFLogoPosition; +typedef enum FFLogoCacheStrategy: uint8_t { + FF_LOGO_CACHE_ON, // reuse a cached rendering when it is valid, and write it back on a cache miss + FF_LOGO_CACHE_OFF, // ignore the image logo cache completely: neither read nor write it + FF_LOGO_CACHE_REGEN, // ignore any existing cached rendering and regenerate it +} FFLogoCacheStrategy; + typedef struct FFOptionsLogo { FFstrbuf source; FFLogoType type; @@ -43,7 +49,7 @@ typedef struct FFOptionsLogo { uint32_t paddingBottom; bool printRemaining; bool preserveAspectRatio; - bool recache; + FFLogoCacheStrategy cache; #if FF_HAVE_CHAFA bool chafaFgOnly; From 129bb7309e2d73a118df434b0f4673367c67fd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 15 Sep 2026 00:13:06 +0800 Subject: [PATCH 41/76] Logo (Icat): adds `--stdin=no` so icat doesn't try to read stdin --- src/logo/image/image.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/logo/image/image.c b/src/logo/image/image.c index b9e39c7b71..fa4d642cae 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -173,6 +173,7 @@ static bool printImageKittyIcat(bool printError) { "kitten", "icat", "-n", + "--stdin=no", "--align=center", place, "--scale-up", @@ -184,6 +185,7 @@ static bool printImageKittyIcat(bool printError) { "kitten", "icat", "-n", + "--stdin=no", "--align=left", options->source.chars, nullptr, From 6178256404063bc489044e80489030523563f81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 15 Sep 2026 00:44:02 +0800 Subject: [PATCH 42/76] Logo (Icat): adds `logo-padding` support --- src/logo/image/image.c | 116 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/src/logo/image/image.c b/src/logo/image/image.c index fa4d642cae..5627c8ac4d 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -122,6 +122,100 @@ static bool printImageIterm(bool printError) { return true; } +// `kitten icat` parses a cursor positioning sequence it may have written: `\e[C` (relative) or +// `\e[;H` (absolute). Returns 1 for the first form, 2 for the second one, 0 for anything +// else, in which case nothing is written back. +static int parseIcatCsi(const char* data, uint32_t length, uint32_t pos, uint32_t* end, uint32_t* first, uint32_t* second) { + if (pos + 2 >= length || data[pos] != '\e' || data[pos + 1] != '[') { + return 0; + } + + uint32_t values[2] = { 0, 0 }; + uint32_t count = 0; + uint32_t i = pos + 2; + while (i < length) { + char c = data[i]; + if (c >= '0' && c <= '9') { + if (count == 0) { + count = 1; + } + values[count - 1] = values[count - 1] * 10 + (uint32_t) (c - '0'); + ++i; + } else if (c == ';' && count == 1) { + count = 2; + ++i; + } else { + break; + } + } + + if (i >= length) { + return 0; + } + + if (data[i] == 'C' && count == 1) { + *end = i + 1; + *first = values[0]; + *second = 0; + return 1; + } + + if (data[i] == 'H' && count == 2) { + *end = i + 1; + *first = values[0]; + *second = values[1]; + return 2; + } + + return 0; +} + +// `kitten icat` positions the image itself: it always writes a carriage return, followed by at most +// one positioning sequence, and then the graphics escape. The carriage return alone would pull the +// cursor back to column 1 and undo the padding, and the absolute cursor move `kitten` writes when +// `--place` is used points at screen coordinates it picked itself, which are not necessarily the ones +// the padding asks for. Both are dropped here and the position is left to the caller, which has +// already moved the cursor to where the padding asks for. The horizontal centering `kitten` applied +// inside the requested rectangle is kept, but re-applied relatively, so it still follows the caller's +// padding. +static void appendIcatOutput(FFstrbuf* buf, const FFstrbuf* output, const FFOptionsLogo* options) { + const char* data = output->chars; + uint32_t pos = 0; // Start of the graphics escape; 0 means "layout not recognised" + uint32_t offset = 0; // Column offset `kitten` applied inside the requested rectangle + + if (output->length > 0 && data[0] == '\r') { + uint32_t cursor = 1; + uint32_t end = 0, first = 0, second = 0; + int kind = parseIcatCsi(data, output->length, cursor, &end, &first, &second); + if (kind == 1) { // Relative move, written instead of the absolute one when `--place` is not used + offset = first; + cursor = end; + } else if (kind == 2 && second >= options->paddingLeft + 1) { // Absolute move, produced by `--place` + // The row is dropped along with the move, so it does not matter which one `kitten` picked. + // The column is where the padding put it, plus the centering, which is all that is kept. + offset = second - options->paddingLeft - 1; + cursor = end; + } + + // The graphics escape has to follow once the positioning is gone + if (cursor + 1 < output->length && data[cursor] == '\e' && data[cursor + 1] == '_') { + pos = cursor; + } + } + + if (pos == 0) { + // Unrecognised layout: keep it as is, positioning included + ffStrbufAppend(buf, output); + return; + } + + if (offset > 0) { + // The cursor is already at the position the padding asks for + ffStrbufAppendF(buf, "\e[%uC", (unsigned) offset); + } + ffStrbufAppendNS(buf, output->length - pos, data + pos); +} + static bool printImageKittyIcat(bool printError) { const FFOptionsLogo* options = &instance.config.logo; @@ -155,21 +249,29 @@ static bool printImageKittyIcat(bool printError) { return false; } - uint32_t prevLength = buf.length; + // `kitten icat` writes its own positioning, so its output is collected separately and only + // appended once that positioning has been dropped (see appendIcatOutput) + FF_STRBUF_AUTO_DESTROY icatOutput = ffStrbufCreate(); const char* error = nullptr; if (options->width) { + // `--place` measures `left` and `top` from the top left corner of the screen, so the padding is + // passed through unchanged. The move `kitten` writes for it is dropped again in appendIcatOutput, + // which leaves the position to the caller, so this is not what puts the image at the padding in + // the normal case. It is what keeps the fallback there working: when the output layout is not + // recognised - as under tmux, where `kitten` switches to unicode placeholders - its own + // positioning is kept, and this is what then places the image at the padding. char place[64]; snprintf(place, ARRAY_SIZE(place), "--place=%ux%u@%ux%u", options->width, options->height == 0 ? 9999 : options->height, - options->paddingLeft + 1, - options->paddingTop + 1); + options->paddingLeft, + options->paddingTop); - error = ffProcessAppendStdOut(&buf, (char*[]) { + error = ffProcessAppendStdOut(&icatOutput, (char*[]) { "kitten", "icat", "-n", @@ -181,7 +283,7 @@ static bool printImageKittyIcat(bool printError) { nullptr, }); } else { - error = ffProcessAppendStdOut(&buf, (char*[]) { + error = ffProcessAppendStdOut(&icatOutput, (char*[]) { "kitten", "icat", "-n", @@ -198,13 +300,15 @@ static bool printImageKittyIcat(bool printError) { return false; } - if (buf.length == prevLength) { + if (icatOutput.length == 0) { if (printError) { fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr); } return false; } + appendIcatOutput(&buf, &icatOutput, options); + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) { From 98ec0a674cd843f713622af0b5c96151cf9a495a Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 15 Sep 2026 08:44:53 +0800 Subject: [PATCH 43/76] IO: increases `FF_IO_TERM_RESP_WAIT_MS` --- src/common/io.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/io.h b/src/common/io.h index 7a8c7e7b05..eba05683fa 100644 --- a/src/common/io.h +++ b/src/common/io.h @@ -230,7 +230,7 @@ typedef enum FFPathType: uint8_t { [[gnu::nonnull(1, 2)]] bool ffPathExpandEnv(const char* in, FFstrbuf* out); -#define FF_IO_TERM_RESP_WAIT_MS 100 // #554 +#define FF_IO_TERM_RESP_WAIT_MS 200 // #554 [[gnu::format(scanf, 3, 4), gnu::nonnull(1, 3)]] const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...); From ff3360e0360f4dec274e2cb9ce2e7b7bb8f4b4b1 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 15 Sep 2026 08:50:40 +0800 Subject: [PATCH 44/76] Logo (Image): fixes potencial memleaks --- src/logo/image/im6.c | 8 +++++--- src/logo/image/im7.c | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c index f2c7bb0b35..047f519056 100644 --- a/src/logo/image/im6.c +++ b/src/logo/image/im6.c @@ -33,7 +33,7 @@ static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const ch FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageList, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) @@ -82,7 +82,9 @@ static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const ch { Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); - ffDestroyImage(image); + // ReadImage may return a list of images (e.g. for multi-frame formats like GIF). + // We only need the first frame, so destroy the whole list to avoid leaking the rest. + ffDestroyImageList(image); image = resized; if (image == nullptr) { goto cleanup; @@ -111,7 +113,7 @@ static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const ch ffDestroyImageInfo(imageInfoOut); } if (image) { - ffDestroyImage(image); + ffDestroyImageList(image); } if (exceptionInfo) { ffDestroyExceptionInfo(exceptionInfo); diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c index ab8ae94bba..8f311f4cc2 100644 --- a/src/logo/image/im7.c +++ b/src/logo/image/im7.c @@ -39,7 +39,7 @@ static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const ch FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageList, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) @@ -88,7 +88,9 @@ static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const ch { Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); - ffDestroyImage(image); + // ReadImage may return a list of images (e.g. for multi-frame formats like GIF). + // We only need the first frame, so destroy the whole list to avoid leaking the rest. + ffDestroyImageList(image); image = resized; if (image == nullptr) { goto cleanup; @@ -117,7 +119,7 @@ static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const ch ffDestroyImageInfo(imageInfoOut); } if (image) { - ffDestroyImage(image); + ffDestroyImageList(image); } if (exceptionInfo) { ffDestroyExceptionInfo(exceptionInfo); From 416715a505fbf6d85bcc4ae46c132f127c284053 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 15 Sep 2026 09:17:49 +0800 Subject: [PATCH 45/76] Presets: fixes neofetch keys Fixes #2588 --- presets/neofetch.jsonc | 15 ++++++++++++--- src/flashfetch.c | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/presets/neofetch.jsonc b/presets/neofetch.jsonc index 6c6eb0c308..c55d562496 100644 --- a/presets/neofetch.jsonc +++ b/presets/neofetch.jsonc @@ -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", diff --git a/src/flashfetch.c b/src/flashfetch.c index d75dcb5127..f789267371 100644 --- a/src/flashfetch.c +++ b/src/flashfetch.c @@ -68,11 +68,13 @@ int main(void) { } { MODULE_OPTION(DE) + ffStrbufSetStatic(&options.moduleArgs.key, "DE"); ffPrintDE(&options); } { instance.config.general.detectVersion = false; MODULE_OPTION(WM) + ffStrbufSetStatic(&options.moduleArgs.key, "WM"); options.detectPlugin = true; ffPrintWM(&options); instance.config.general.detectVersion = true; @@ -100,6 +102,7 @@ int main(void) { } { MODULE_OPTION(CPU) + options.showPeCoreCount = false; ffPrintCPU(&options); } { From fbf79ac6fe3868ee6c428f1d962f44cc7b680771 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 15 Sep 2026 10:33:00 +0800 Subject: [PATCH 46/76] FFstrbuf: improves empty-string handling for ffStrbufMatchSeparatedNS and ffStrbufSeparatedContainNS Fixes #2585 --- src/common/impl/FFstrbuf.c | 39 +++++++++++++++++++-------------- tests/strbuf.c | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c index 27cb4d66a6..20693c1ecf 100644 --- a/src/common/impl/FFstrbuf.c +++ b/src/common/impl/FFstrbuf.c @@ -723,16 +723,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 +753,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 +799,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 +822,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; diff --git a/tests/strbuf.c b/tests/strbuf.c index d1ca642658..06cfe4754c 100644 --- a/tests/strbuf.c +++ b/tests/strbuf.c @@ -681,6 +681,30 @@ int main(void) { VERIFY(ffStrbufMatchSeparatedS(&strbuf, ":abcdef", ':') == false); } + { + // ffStrbufMatchSeparatedNS with explicit compLength: the length-bounded + // contract is load-bearing for the Linux keyboard handler, which passes + // a fixed-size buffer length rather than a NUL-terminated string. + ffStrbufSetStatic(&strbuf, "abc"); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ':') == true); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 3, "abc:def:ghi", ':') == true); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 7, "abc:def:ghi", ':') == true); // "abc:def" + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, "abc:def:ghi", ':') == true); // "abc:" + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 5, "abc:def:ghi", ':') == true); // "abc:d" truncated, but "abc" is a complete segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 3, "abd:def:ghi", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "abc:def:ghi", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ' ') == false); // no separator in bounds + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 12, "abc:def:ghi\0j", ':') == true); // embedded NUL inside bounds + + ffStrbufClear(&strbuf); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ':') == false); // no empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 8, "abc::def", ':') == true); // empty segment in the middle + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, "abc:", ':') == true); // trailing empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, ":abc", ':') == true); // leading empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "abc", ':') == true); // empty separated string itself + } + { ffStrbufSetStatic(&strbuf, "ABC"); VERIFY(ffStrbufMatchSeparatedIgnCaseS(&strbuf, "abc:def:ghi", ' ') == false); @@ -721,6 +745,27 @@ int main(void) { VERIFY(ffStrbufSeparatedContainIgnCaseS(&strbuf, "i", ':') == false); } + { + // ffStrbufSeparatedContainNS with empty strings: an empty list contains + // only the empty segment (symmetric to ffStrbufMatchSeparatedNS); an + // empty comp matches only an empty segment in the list. + ffStrbufClear(&strbuf); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // empty list contains the empty segment + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 3, "abc", ':') == false); // empty list contains no non-empty segment + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "abc", ':') == true); // compLength == 0 means an empty segment + + ffStrbufSetStatic(&strbuf, "abc::def"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // empty segment in the middle + ffStrbufSetStatic(&strbuf, "abc:"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // trailing empty segment + ffStrbufSetStatic(&strbuf, ":abc"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // leading empty segment + ffStrbufSetStatic(&strbuf, "abc:def"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == false); // no empty segment + ffStrbufSetStatic(&strbuf, "abc"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == false); // no empty segment + } + { ffStrbufSetStatic(&strbuf, "abc"); ffStrbufSubstr(&strbuf, 0, 1); // start, end From 53e868667ab7c512612918f74c399edf76f1fd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 15 Sep 2026 20:11:15 +0800 Subject: [PATCH 47/76] Logo (Image): fixes a copy-paste error --- src/logo/image/image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 5627c8ac4d..0ae1348f5a 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -699,7 +699,7 @@ static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* result.chars = str->str; ffLogoPrintChars(result.chars, false); - writeCacheData(requestData, &result.chars, result.length, FF_CACHE_FILE_CHAFA); + writeCacheData(requestData, result.chars, result.length, FF_CACHE_FILE_CHAFA); // FIXME: These functions must be imported from `libglib` dlls on Windows FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free); From 02dc6a4428c015cdf8b003239be3c4238fdcd5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 15 Sep 2026 22:20:17 +0800 Subject: [PATCH 48/76] Modules: adds `*` to format args which are available in key format --- src/fastfetch.c | 1 + src/modules/battery/battery.c | 4 ++-- src/modules/bios/bios.c | 4 ++-- src/modules/bluetoothradio/bluetoothradio.c | 2 +- src/modules/brightness/brightness.c | 2 +- src/modules/btrfs/btrfs.c | 2 +- src/modules/codec/codec.c | 4 ++-- src/modules/cpucache/cpucache.c | 2 ++ src/modules/disk/disk.c | 8 ++++---- src/modules/diskio/diskio.c | 6 +++--- src/modules/display/display.c | 4 ++-- src/modules/localip/localip.c | 4 ++-- src/modules/monitor/monitor.c | 2 +- src/modules/netio/netio.c | 4 ++-- src/modules/os/os.c | 4 ++-- src/modules/physicaldisk/physicaldisk.c | 4 ++-- src/modules/swap/swap.c | 2 +- src/modules/zpool/zpool.c | 4 ++-- 18 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/fastfetch.c b/src/fastfetch.c index 3a936fb766..a5c3ec20f9 100644 --- a/src/fastfetch.c +++ b/src/fastfetch.c @@ -70,6 +70,7 @@ static void printCommandFormatHelp(const char* command) { printf("-- In config file: { \"type\": \"%s\", \"format\": \"{}\" }\n", type.chars); printf("Sets the format string for %s output.\n", baseInfo->name); puts("To see how a format string is constructed, take a look at https://github.com/fastfetch-cli/fastfetch/wiki/Format-String-Guide."); + puts("Descriptions which end with a '*' are available in key format too."); puts("The following variables are passed:"); uint32_t maxWidth = 20; diff --git a/src/modules/battery/battery.c b/src/modules/battery/battery.c index f1faa0a796..b89af77c4d 100644 --- a/src/modules/battery/battery.c +++ b/src/modules/battery/battery.c @@ -147,7 +147,7 @@ static void printBattery(FFBatteryOptions* options, FFBatteryResult* result, uin FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) { FF_ARG(result->manufacturer, "manufacturer"), - FF_ARG(result->modelName, "model-name"), + FF_ARG(result->modelName, "name"), FF_ARG(result->technology, "technology"), FF_ARG(capacityNum, "capacity"), FF_ARG(status, "status"), @@ -330,7 +330,7 @@ FFModuleBaseInfo ffBatteryModuleInfo = { .generateJsonConfig = (void*) ffGenerateBatteryJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Battery manufacturer", "manufacturer" }, - { "Battery model name", "model-name" }, + { "Battery model name *", "name" }, { "Battery technology", "technology" }, { "Battery capacity (percentage num)", "capacity" }, { "Battery status", "status" }, diff --git a/src/modules/bios/bios.c b/src/modules/bios/bios.c index 55a6e03f0b..3bde59597d 100644 --- a/src/modules/bios/bios.c +++ b/src/modules/bios/bios.c @@ -161,12 +161,12 @@ FFModuleBaseInfo ffBiosModuleInfo = { .printModule = (void*) ffPrintBios, .generateJsonResult = (void*) ffGenerateBiosJsonResult, .generateJsonConfig = (void*) ffGenerateBiosJsonConfig, - .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { + .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){ { "BIOS date", "date" }, { "BIOS release", "release" }, { "BIOS vendor", "vendor" }, { "BIOS version", "version" }, - { "Firmware type", "type" }, + { "Firmware type *", "type" }, })), .defaultOrder = 5, }; diff --git a/src/modules/bluetoothradio/bluetoothradio.c b/src/modules/bluetoothradio/bluetoothradio.c index 63c27fdda8..dcff4a6f5f 100644 --- a/src/modules/bluetoothradio/bluetoothradio.c +++ b/src/modules/bluetoothradio/bluetoothradio.c @@ -220,7 +220,7 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = { .generateJsonResult = (void*) ffGenerateBluetoothRadioJsonResult, .generateJsonConfig = (void*) ffGenerateBluetoothRadioJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Radio name for discovering", "name" }, + { "Radio name for discovering *", "name" }, { "Address", "address" }, { "LMP version", "lmp-version" }, { "LMP subversion", "lmp-subversion" }, diff --git a/src/modules/brightness/brightness.c b/src/modules/brightness/brightness.c index 2bfe4d8021..3cdb28bf11 100644 --- a/src/modules/brightness/brightness.c +++ b/src/modules/brightness/brightness.c @@ -223,7 +223,7 @@ FFModuleBaseInfo ffBrightnessModuleInfo = { .generateJsonConfig = (void*) ffGenerateBrightnessJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Screen brightness (percentage num)", "percentage" }, - { "Screen name", "name" }, + { "Screen name *", "name" }, { "Maximum brightness value", "max" }, { "Minimum brightness value", "min" }, { "Current brightness value", "current" }, diff --git a/src/modules/btrfs/btrfs.c b/src/modules/btrfs/btrfs.c index 345f7927e7..05ad2eb81b 100644 --- a/src/modules/btrfs/btrfs.c +++ b/src/modules/btrfs/btrfs.c @@ -230,7 +230,7 @@ FFModuleBaseInfo ffBtrfsModuleInfo = { .generateJsonResult = (void*) ffGenerateBtrfsJsonResult, .generateJsonConfig = (void*) ffGenerateBtrfsJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Name / Label", "name" }, + { "Name / Label *", "name" }, { "UUID", "uuid" }, { "Associated devices", "devices" }, { "Enabled features", "features" }, diff --git a/src/modules/codec/codec.c b/src/modules/codec/codec.c index 6294448057..b432cf471b 100644 --- a/src/modules/codec/codec.c +++ b/src/modules/codec/codec.c @@ -283,8 +283,8 @@ FFModuleBaseInfo ffCodecModuleInfo = { .generateJsonResult = (void*) ffGenerateCodecJsonResult, .generateJsonConfig = (void*) ffGenerateCodecJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){ - { "GPU name", "gpu" }, - { "Decoder / Encoder", "direction" }, + { "GPU name *", "gpu" }, + { "Decoder / Encoder *", "direction" }, { "Compatibility alias of codec types", "types" }, { "Platform API used for detection", "platform-api" }, })), diff --git a/src/modules/cpucache/cpucache.c b/src/modules/cpucache/cpucache.c index d4c9cee5d8..bf93dcca5a 100644 --- a/src/modules/cpucache/cpucache.c +++ b/src/modules/cpucache/cpucache.c @@ -65,6 +65,7 @@ static void printCPUCacheNormal(const FFCPUCacheResult* result, FFCPUCacheOption FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) { FF_ARG(buffer, "result"), FF_ARG(buffer2, "sum"), + FF_ARG(levelStr, "level"), })); } } @@ -255,6 +256,7 @@ FFModuleBaseInfo ffCPUCacheModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Separate result", "result" }, { "Sum result", "sum" }, + { "Cache level *", "level" }, })), .defaultOrder = 34, }; diff --git a/src/modules/disk/disk.c b/src/modules/disk/disk.c index b87105471a..b73bcab2be 100644 --- a/src/modules/disk/disk.c +++ b/src/modules/disk/disk.c @@ -491,8 +491,8 @@ FFModuleBaseInfo ffDiskModuleInfo = { { "Files percentage num", "files-percentage" }, { "True if external volume", "is-external" }, { "True if hidden volume", "is-hidden" }, - { "Filesystem", "filesystem" }, - { "Label / name", "name" }, + { "Filesystem *", "filesystem" }, + { "Label / name *", "name" }, { "True if read-only", "is-readonly" }, { "Create time in local timezone", "create-time" }, { "Size percentage bar", "size-percentage-bar" }, @@ -502,8 +502,8 @@ FFModuleBaseInfo ffDiskModuleInfo = { { "Minutes after creation", "minutes" }, { "Seconds after creation", "seconds" }, { "Milliseconds after creation", "milliseconds" }, - { "Mount point / drive letter", "mountpoint" }, - { "Mount from (device path)", "mount-from" }, + { "Mount point / drive letter *", "mountpoint" }, + { "Mount from (device path) *", "mount-from" }, { "Years integer after creation", "years" }, { "Days of year after creation", "days-of-year" }, { "Years fraction after creation", "years-fraction" }, diff --git a/src/modules/diskio/diskio.c b/src/modules/diskio/diskio.c index c052983f5d..fac2d64594 100644 --- a/src/modules/diskio/diskio.c +++ b/src/modules/diskio/diskio.c @@ -205,10 +205,10 @@ FFModuleBaseInfo ffDiskIOModuleInfo = { .generateJsonResult = (void*) ffGenerateDiskIOJsonResult, .generateJsonConfig = (void*) ffGenerateDiskIOJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Size of data read [per second] (formatted)", "size-read" }, + { "Size of data read [per second] (formatted)" , "size-read" }, { "Size of data written [per second] (formatted)", "size-written" }, - { "Device name", "name" }, - { "Device raw file path", "dev-path" }, + { "Device name *", "name" }, + { "Device raw file path *", "dev-path" }, { "Size of data read [per second] (in bytes)", "bytes-read" }, { "Size of data written [per second] (in bytes)", "bytes-written" }, { "Number of reads", "read-count" }, diff --git a/src/modules/display/display.c b/src/modules/display/display.c index f3ba4d75a9..b2ec904597 100644 --- a/src/modules/display/display.c +++ b/src/modules/display/display.c @@ -448,8 +448,8 @@ FFModuleBaseInfo ffDisplayModuleInfo = { { "Screen configured refresh rate (in Hz)", "refresh-rate" }, { "Screen scaled width (in pixels)", "scaled-width" }, { "Screen scaled height (in pixels)", "scaled-height" }, - { "Screen name", "name" }, - { "Screen type (Built-in or External)", "type" }, + { "Screen name *", "name" }, + { "Screen type (Built-in or External) *", "type" }, { "Screen rotation (in degrees)", "rotation" }, { "True if being the primary screen", "is-primary" }, { "Screen physical width (in millimeters)", "physical-width" }, diff --git a/src/modules/localip/localip.c b/src/modules/localip/localip.c index c817c7ac90..6ad26bd54c 100644 --- a/src/modules/localip/localip.c +++ b/src/modules/localip/localip.c @@ -455,8 +455,8 @@ FFModuleBaseInfo ffLocalIPModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "IPv4 address", "ipv4" }, { "IPv6 address", "ipv6" }, - { "MAC address", "mac" }, - { "Interface name", "ifname" }, + { "MAC address *", "mac" }, + { "Interface name *", "ifname" }, { "Is default route", "is-default-route" }, { "MTU size in bytes", "mtu" }, { "Link speed (formatted)", "speed" }, diff --git a/src/modules/monitor/monitor.c b/src/modules/monitor/monitor.c index ab53af9189..7022a044d1 100644 --- a/src/modules/monitor/monitor.c +++ b/src/modules/monitor/monitor.c @@ -136,7 +136,7 @@ FFModuleBaseInfo ffMonitorModuleInfo = { .generateJsonResult = (void*) ffGenerateMonitorJsonResult, .generateJsonConfig = (void*) ffGenerateMonitorJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Display name", "name" }, + { "Display name *", "name" }, { "Native resolution width in pixels", "width" }, { "Native resolution height in pixels", "height" }, { "Physical width in millimeters", "physical-width" }, diff --git a/src/modules/netio/netio.c b/src/modules/netio/netio.c index 69de01058a..ef3da5d6a2 100644 --- a/src/modules/netio/netio.c +++ b/src/modules/netio/netio.c @@ -20,7 +20,7 @@ static void formatKey(const FFNetIOOptions* options, FFNetIOResult* inf, uint32_ ffStrbufClear(key); FF_PARSE_FORMAT_STRING_CHECKED(key, &options->moduleArgs.key, ((FFformatarg[]) { FF_ARG(index, "index"), - FF_ARG(inf->name, "name"), + FF_ARG(inf->name, "ifname"), FF_ARG(options->moduleArgs.keyIcon, "icon"), FF_ARG(FF_MODULE_GET_DISPLAY_NAME(NetIO), "module-name"), })); @@ -234,7 +234,7 @@ FFModuleBaseInfo ffNetIOModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Size of data received [per second] (formatted)", "rx-size" }, { "Size of data sent [per second] (formatted)", "tx-size" }, - { "Interface name", "ifname" }, + { "Interface name *", "ifname" }, { "Is default route", "is-default-route" }, { "Size of data received [per second] (in bytes)", "rx-bytes" }, { "Size of data sent [per second] (in bytes)", "tx-bytes" }, diff --git a/src/modules/os/os.c b/src/modules/os/os.c index c8fa4987fe..ad7a2bdcbb 100644 --- a/src/modules/os/os.c +++ b/src/modules/os/os.c @@ -204,8 +204,8 @@ FFModuleBaseInfo ffOSModuleInfo = { .generateJsonResult = (void*) ffGenerateOSJsonResult, .generateJsonConfig = (void*) ffGenerateOSJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Name of the kernel", "sysname" }, - { "Name of the OS", "name" }, + { "Name of the kernel *", "sysname" }, + { "Name of the OS *", "name" }, { "Pretty name of the OS, if available", "pretty-name" }, { "ID of the OS", "id" }, { "ID like of the OS", "id-like" }, diff --git a/src/modules/physicaldisk/physicaldisk.c b/src/modules/physicaldisk/physicaldisk.c index 0690f72224..3275bb4687 100644 --- a/src/modules/physicaldisk/physicaldisk.c +++ b/src/modules/physicaldisk/physicaldisk.c @@ -296,9 +296,9 @@ FFModuleBaseInfo ffPhysicalDiskModuleInfo = { .generateJsonConfig = (void*) ffGeneratePhysicalDiskJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Device size (formatted)", "size" }, - { "Device name", "name" }, + { "Device name *", "name" }, { "Device interconnect type", "interconnect" }, - { "Device raw file path", "dev-path" }, + { "Device raw file path *", "dev-path" }, { "Serial number", "serial" }, { "Device kind (SSD or HDD)", "physical-type" }, { "Device kind (Removable or Fixed)", "removable-type" }, diff --git a/src/modules/swap/swap.c b/src/modules/swap/swap.c index b1cc38321c..f5441b4c87 100644 --- a/src/modules/swap/swap.c +++ b/src/modules/swap/swap.c @@ -218,7 +218,7 @@ FFModuleBaseInfo ffSwapModuleInfo = { { "Total size", "total" }, { "Percentage used (num)", "percentage" }, { "Percentage used (bar)", "percentage-bar" }, - { "Name", "name" }, + { "Name *", "name" }, })), .defaultOrder = 40, }; diff --git a/src/modules/zpool/zpool.c b/src/modules/zpool/zpool.c index dc39668246..440e58d868 100644 --- a/src/modules/zpool/zpool.c +++ b/src/modules/zpool/zpool.c @@ -224,8 +224,8 @@ FFModuleBaseInfo ffZpoolModuleInfo = { .generateJsonResult = (void*) ffGenerateZpoolJsonResult, .generateJsonConfig = (void*) ffGenerateZpoolJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { - { "Zpool name", "name" }, - { "Zpool guid", "guid" }, + { "Zpool name *", "name" }, + { "Zpool guid *", "guid" }, { "Zpool state", "state" }, { "Size used", "size-used" }, { "Size allocated", "size-allocated" }, From 7b28ad07d71cbe1211d3b829613afaf68c1c9db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 15 Sep 2026 22:40:41 +0800 Subject: [PATCH 49/76] Chore: updates gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9fe9c7bfb4..553bdfe75a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ fastfetch.kdev4 *.swp *.log /*.zh.md -/.*-ai/ +/.work* From b96d7e7becda2d47feb6dc22f8b43ba7b987dc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Wed, 16 Sep 2026 00:56:21 +0800 Subject: [PATCH 50/76] Networking: supports chunked response & custom port number --- CMakeLists.txt | 8 + src/common/impl/networking_common.c | 209 +++++++++++++++++++++++++++ src/common/impl/networking_linux.c | 155 ++++++++++++++++---- src/common/impl/networking_windows.c | 96 ++++++++++-- src/common/networking.h | 34 ++++- src/detection/publicip/publicip.c | 50 ++++++- src/detection/weather/weather.c | 2 +- tests/networking.c | 86 +++++++++++ 8 files changed, 600 insertions(+), 40 deletions(-) create mode 100644 tests/networking.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 25e2e4dc08..6f09152a89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2204,6 +2204,13 @@ if (BUILD_TESTS) PRIVATE libfastfetch ) + add_executable(fastfetch-test-networking + tests/networking.c + ) + target_link_libraries(fastfetch-test-networking + PRIVATE libfastfetch + ) + enable_testing() add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf) add_test(NAME test-list COMMAND fastfetch-test-list) @@ -2211,6 +2218,7 @@ if (BUILD_TESTS) 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) endif() ################## diff --git a/src/common/impl/networking_common.c b/src/common/impl/networking_common.c index 2c436590e8..cc6f5e24b5 100644 --- a/src/common/impl/networking_common.c +++ b/src/common/impl/networking_common.c @@ -190,3 +190,212 @@ bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) { return true; } #endif // FF_HAVE_ZLIB + +const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen) { + assert(headers != nullptr && valueLen != nullptr); + + uint32_t nameLen = (uint32_t) strlen(name); + uint32_t pos = 0; + + 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; + + // 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; + } + + if (eol >= headerEnd) { + break; + } + pos = eol + 1; + } + + return nullptr; +} + +FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen) { + assert(value != nullptr); + + // 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) { + assert(body != nullptr && consumed != nullptr); + + 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. +static void 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); +} + +bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd) { + assert(buffer->allocated > 0); + + // The header block is terminated by CR LF CR LF + if (headerEnd + 4 > buffer->length) { + return false; + } + + char* body = buffer->chars + headerEnd + 4; + uint32_t bodyLen = buffer->length - headerEnd - 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); + rebuildResponse(buffer, headerEnd, out, outLen, "Transfer-Encoding:"); + return true; +} diff --git a/src/common/impl/networking_linux.c b/src/common/impl/networking_linux.c index 770c0ce774..c9e708e246 100644 --- a/src/common/impl/networking_linux.c +++ b/src/common/impl/networking_linux.c @@ -16,6 +16,9 @@ #include #include +// 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__ @@ -187,7 +190,7 @@ static const char* connectAndSend(FFNetworkingState* state) { 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,7 +198,19 @@ 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); + 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 // If compression needs to be enabled @@ -220,10 +235,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 +277,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; @@ -298,8 +325,8 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos 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"); @@ -321,7 +348,7 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho 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 +402,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 +439,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 +450,46 @@ 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); + close(state->sockfd); + state->sockfd = -1; + return "Response too large"; + } + FF_DEBUG("Receive buffer is full, extending it"); + ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated); + } - do { - FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u", + // 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 +513,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 +531,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; + } + } + } + } + + // 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; } - } while (ffStrbufGetFree(buffer) > 0); + } FF_DEBUG("Closing socket: fd=%d", state->sockfd); close(state->sockfd); @@ -488,6 +587,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf return "No HTTP header end found"; } + if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) { + 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"; diff --git a/src/common/impl/networking_windows.c b/src/common/impl/networking_windows.c index 409edff7e5..f5ed6c4005 100644 --- a/src/common/impl/networking_windows.c +++ b/src/common/impl/networking_windows.c @@ -9,6 +9,9 @@ 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) { @@ -45,8 +48,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); +const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) { + FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path); if (state->compression) { #ifdef FF_HAVE_ZLIB @@ -92,8 +95,11 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho 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) { + wchar_t portW[6]; + _itow(port, portW, 10); + + FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4"); + if (GetAddrInfoW(hostW, portW, &hints, &addr) != 0) { FF_DEBUG("GetAddrInfoW() failed"); return "GetAddrInfoW() failed"; } @@ -156,7 +162,19 @@ 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); + 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 // Add compression support if enabled @@ -266,8 +284,23 @@ 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"); + ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated); + } - do { FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u", ++recvCount, buffer->length, @@ -306,11 +339,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 +357,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; + } + } + } + } + + // 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; } - } while (ffStrbufGetFree(buffer) > 0); + } FF_DEBUG("Closing socket: fd=%u", (unsigned) state->sockfd); closesocket(state->sockfd); @@ -341,6 +413,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf return "No HTTP header end found"; } + if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) { + 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"; diff --git a/src/common/networking.h b/src/common/networking.h index 2554e2231e..f7dbba422b 100644 --- a/src/common/networking.h +++ b/src/common/networking.h @@ -29,9 +29,41 @@ typedef struct FFNetworkingState { bool tfo; // if true, TCP Fast Open will be attempted first, and fallback to traditional connection if it fails } FFNetworkingState; -const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers); +const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers); const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer); +// Case-insensitive header lookup restricted to the header block [0, headerEnd). +// Restricting the range matters because the body may already share the same buffer. +// Returns a pointer to the first character of the value; `valueLen` receives its +// length excluding the terminating CRLF. Returns nullptr when the header is absent. +const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen); + +// Checks whether a `Transfer-Encoding: chunked` body has been received in full, so that +// framing does not have to rely on the server closing the connection. +// Returns 1 when complete (`consumed` receives the body length including trailers), +// 0 when more data is needed, and -1 when the body is malformed. +// The caller must not wait for a fixed amount of data (e.g. `MSG_WAITALL`) while the +// response length is still unknown, otherwise this check never gets to run. +int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* consumed); + +// Decodes a `Transfer-Encoding: chunked` body in place and rewrites the response with a +// `Content-Length` header in place of `Transfer-Encoding`. +bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd); + +// Result of parsing a `Transfer-Encoding` header value +typedef enum FFNetworkingTransferEncoding { + FF_NETWORKING_TE_NONE, // the value holds no coding at all + FF_NETWORKING_TE_CHUNKED, // exactly `chunked`, the only framing this client decodes + FF_NETWORKING_TE_UNSUPPORTED, // another coding or a chain of them, e.g. `gzip, chunked` +} FFNetworkingTransferEncoding; + +// Parses a `Transfer-Encoding` header value. Codings are applied in the order they are +// listed, so `chunked` has to be the last one for the framing to be readable at all -- +// and any other coding (e.g. `gzip, chunked`) leaves the payload encoded, which this +// client cannot decode. Only a lone `chunked` is accepted; everything else is reported +// as unsupported so that the caller fails the response instead of returning garbage. +FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen); + #ifdef FF_HAVE_ZLIB const char* ffNetworkingLoadZlibLibrary(void); bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd); diff --git a/src/detection/publicip/publicip.c b/src/detection/publicip/publicip.c index c0e8afb478..90efa12d26 100644 --- a/src/detection/publicip/publicip.c +++ b/src/detection/publicip/publicip.c @@ -5,6 +5,20 @@ static FFNetworkingState states[2]; static const char* statuses[2] = { FF_UNINITIALIZED, FF_UNINITIALIZED }; +// Reads the port that follows the colon at `colonIndex`. The port has to be the last thing in +// `host`, so this must run before any part of the host is trimmed off. +static uint16_t ffPublicIpParseUrlPort(const FFstrbuf* host, uint32_t colonIndex) { + const char* portStr = host->chars + colonIndex + 1; + char* portEnd = nullptr; + unsigned long portValue = strtoul(portStr, &portEnd, 10); + if (portEnd == portStr || *portEnd != '\0' || portValue == 0 || portValue > 65535) { + fputs("Error: invalid port in the PublicIp module URL\n", stderr); + exit(1); + } + + return (uint16_t) portValue; +} + void ffPreparePublicIp(FFPublicIPOptions* options) { FFNetworkingState* state = &states[options->ipv6]; const char** status = &statuses[options->ipv6]; @@ -19,7 +33,7 @@ void ffPreparePublicIp(FFPublicIPOptions* options) { if (options->url.length == 0) { state->compression = true; state->tfo = true; - *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", "/json", nullptr); + *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", 80, "/json", nullptr); } else { FF_STRBUF_AUTO_DESTROY host = ffStrbufCreateCopy(&options->url); uint32_t hostStartIndex = ffStrbufFirstIndexS(&host, "://"); @@ -38,7 +52,39 @@ void ffPreparePublicIp(FFPublicIPOptions* options) { ffStrbufSubstrBefore(&host, pathStartIndex); } - *status = ffNetworkingSendHttpRequest(state, host.chars, path.length == 0 ? "/" : path.chars, nullptr); + // An optional `:port` must be split off the host, otherwise getaddrinfo() is asked to + // resolve a host name that still carries the port. Only a single colon can separate a + // port: a bare IPv6 literal (`::1`) holds several of them and has no port at all, + // which is why a bracketed literal (`[::1]:8080`) is the only unambiguous spelling. + uint16_t port = 0; + if (ffStrbufStartsWithC(&host, '[')) { + uint32_t bracketEnd = ffStrbufFirstIndexC(&host, ']'); + if (bracketEnd == host.length) { + fputs("Error: unmatched '[' in the PublicIp module URL\n", stderr); + exit(1); + } + + if (bracketEnd + 1 < host.length) { + if (host.chars[bracketEnd + 1] != ':') { + fputs("Error: unexpected characters after the IPv6 literal in the PublicIp module URL\n", stderr); + exit(1); + } + // Read the port while the string is still intact: trimming the brackets first + // would invalidate the index it was found at. + port = ffPublicIpParseUrlPort(&host, bracketEnd + 1); + } + + ffStrbufSubstrBefore(&host, bracketEnd); + ffStrbufSubstrAfter(&host, 0); // drop the leading '[' + } else { + uint32_t firstColon = ffStrbufFirstIndexC(&host, ':'); + if (firstColon < host.length && firstColon == ffStrbufLastIndexC(&host, ':')) { + port = ffPublicIpParseUrlPort(&host, firstColon); + ffStrbufSubstrBefore(&host, firstColon); + } + } + + *status = ffNetworkingSendHttpRequest(state, host.chars, port ?: 80, path.length == 0 ? "/" : path.chars, nullptr); } } diff --git a/src/detection/weather/weather.c b/src/detection/weather/weather.c index 8a70a966a3..2899456f18 100644 --- a/src/detection/weather/weather.c +++ b/src/detection/weather/weather.c @@ -29,7 +29,7 @@ void ffPrepareWeather(FFWeatherOptions* options) { default: break; } - status = ffNetworkingSendHttpRequest(&state, "wttr.in", path.chars, "User-Agent: curl/0.0.0\r\n"); + status = ffNetworkingSendHttpRequest(&state, "wttr.in", 80, path.chars, "User-Agent: curl/0.0.0\r\n"); } const char* ffDetectWeather(FFWeatherOptions* options, FFstrbuf* result) { diff --git a/tests/networking.c b/tests/networking.c new file mode 100644 index 0000000000..18f0fc927b --- /dev/null +++ b/tests/networking.c @@ -0,0 +1,86 @@ +#include "common/networking.h" + +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, "[%d] %s\n", lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +static FFNetworkingTransferEncoding parse(const char* value) { + return ffNetworkingParseTransferEncoding(value, (uint32_t) strlen(value)); +} + +int main(void) { + { + VERIFY(parse("chunked") == FF_NETWORKING_TE_CHUNKED); + VERIFY(parse("Chunked") == FF_NETWORKING_TE_CHUNKED); + VERIFY(parse(" chunked ") == FF_NETWORKING_TE_CHUNKED); + VERIFY(parse("chunked\t") == FF_NETWORKING_TE_CHUNKED); + + // A coding chain leaves the payload encoded, so only a lone `chunked` is decodable + VERIFY(parse("gzip, chunked") == FF_NETWORKING_TE_UNSUPPORTED); + VERIFY(parse("gzip,chunked") == FF_NETWORKING_TE_UNSUPPORTED); + VERIFY(parse("chunked, gzip") == FF_NETWORKING_TE_UNSUPPORTED); + VERIFY(parse("gzip") == FF_NETWORKING_TE_UNSUPPORTED); + VERIFY(parse("x-chunked") == FF_NETWORKING_TE_UNSUPPORTED); + VERIFY(parse("chunkedx") == FF_NETWORKING_TE_UNSUPPORTED); + + VERIFY(parse("") == FF_NETWORKING_TE_NONE); + VERIFY(parse(" , ") == FF_NETWORKING_TE_NONE); + } + + { + // The lookup is restricted to the header block, so a body that happens to contain a + // `Transfer-Encoding:` line can never be mistaken for a header + const char* response = "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nTransfer-Encoding: chunked\r\n"; + uint32_t headerEnd = (uint32_t) (strstr(response, "\r\n\r\n") - response); + uint32_t valueLen = 0; + + VERIFY(ffNetworkingFindHeader(response, headerEnd, "Transfer-Encoding:", &valueLen) == nullptr); + + const char* clHeader = ffNetworkingFindHeader(response, headerEnd, "Content-Length:", &valueLen); + VERIFY(clHeader != nullptr && valueLen == 1 && *clHeader == '4'); + } + + { + uint32_t consumed = 0; + const char* complete = "5\r\nhello\r\n0\r\n\r\n"; + VERIFY(ffNetworkingChunkedComplete(complete, (uint32_t) strlen(complete), &consumed) == 1); + VERIFY(consumed == strlen(complete)); + + const char* withTrailer = "5\r\nhello\r\n0\r\nX-Foo: bar\r\n\r\n"; + VERIFY(ffNetworkingChunkedComplete(withTrailer, (uint32_t) strlen(withTrailer), &consumed) == 1); + VERIFY(consumed == strlen(withTrailer)); + + const char* partial = "5\r\nhel"; + VERIFY(ffNetworkingChunkedComplete(partial, (uint32_t) strlen(partial), &consumed) == 0); + + const char* malformed = "5\r\nhelloXX0\r\n\r\n"; + VERIFY(ffNetworkingChunkedComplete(malformed, (uint32_t) strlen(malformed), &consumed) == -1); + } + + { + // Decoding replaces the framing header with a matching Content-Length + FFstrbuf response = ffStrbufCreateS("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n"); + uint32_t headerEnd = (uint32_t) (strstr(response.chars, "\r\n\r\n") - response.chars); + + VERIFY(ffNetworkingDecodeChunked(&response, headerEnd)); + VERIFY(ffStrbufContainS(&response, "Content-Length: 5")); + VERIFY(!ffStrbufContainS(&response, "Transfer-Encoding")); + VERIFY(ffStrbufEndsWithS(&response, "hello")); + + ffStrbufDestroy(&response); + } + + puts("All networking tests passed!"); + return 0; +} From d1b747192f16c9bf1155315b455ac1448b66f56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Wed, 16 Sep 2026 20:48:02 +0800 Subject: [PATCH 51/76] Logo (Image): adds animation (gif / apng) support --- CHANGELOG.md | 53 ++- CMakeLists.txt | 8 + doc/help.json | 16 + src/common/impl/base64.c | 10 +- src/logo/image/im7.c | 437 ++++++++++++++++++++++++ src/logo/image/image.c | 706 +++++++++++++++++++++++++++++++++++++-- src/logo/image/image.h | 55 +++ src/logo/image/imageio.c | 489 +++++++++++++++++++++------ src/logo/image/wic.cpp | 562 +++++++++++++++++++++++++++---- src/options/logo.c | 11 + src/options/logo.h | 10 + tests/base64.c | 153 +++++++++ 12 files changed, 2323 insertions(+), 187 deletions(-) create mode 100644 tests/base64.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 786fbef96e..f4816265d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,58 @@ # Unreleased -Features: +Changes: +* ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo, Windows / macOS) + * Image logos on Windows and macOS no longer depend on ImageMagick being installed. + * Image sources that only ImageMagick could decode, such as SVG, PDF and PostScript, are no longer supported on Windows: neither WIC nor the embedded sixel encoder can decode them. +* The `--logo-recache` option has been replaced by `--logo-cache `, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo) + * `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss. + * `--logo-cache false` ignores the image logo cache completely: nothing is read from it and nothing is written to it. + * `--logo-cache regen` does what `--logo-recache true` used to do. + * `logo.cache` accepts a boolean, or the string `"regen"`. + +Features: +* Improved image logo support + * Backend rewritten + * Added a native image decoding backend on Windows (WIC) and macOS (ImageIO). + * Added an embedded libsixel encoder, used to produce sixel output on Windows and macOS. It is reported by `fastfetch --list-features` as "Embedded sixel". + * Enabled chafa image output on Windows and macOS independently of ImageMagick. + * As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal. + * Image logo cache entries are now validated against the modification time of the source image. (Logo) + * Editing an image logo in place now invalidates its cached rendering. + * Cache entries written by older versions are not reused, as they carry no modification time. + * Image logos can now be animated, when the terminal and the image protocol support it. (Logo) + * `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved. + * `--logo-animation-frame ` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option. + * The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. + * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single frame GIF falls back to a still image. A build with none of those, or with ImageMagick 6, reports an error instead of quietly showing a still image. + * A terminal that supports the kitty graphics protocol but not its animation frames, as Konsole does not, shows the first frame. * Added CPU name and frequency detection support on SPARC. (CPU, Linux) +* Added package detection support for CRUX. (Packages, Linux) + * Exposed in custom format as `{crux}`. +* Improved COSMIC detection (DE / WM, Linux) + * The version is now read from the `COSMIC_VERSION` environment variable when it is set. +* Improved accuracy and performance of process name detection in the Top module. (Top, macOS) +* Improved Packages detection on Windows (Packages, Windows) + * `winget list` is now invoked with `--source winget`, so only packages installed by winget itself are counted, and the slow msstore HTTP round trips are skipped. +* Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS) + * The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is tried last. +* Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD) +* Modules that were selected on the command line via `--structure` / `-s` now honors module options configured in the JSON config. (CommandOption) + +Bugfixes: +* Fixed Base64 encoding producing wrong output for some inputs. (General) +* Fixed image logos not working when ImageMagick is built without a quantum depth suffix in its library name, as on FreeBSD. (Logo, FreeBSD) +* Fixed TerminalFont detection on Windows ignoring Windows Terminal JSON fragment files. (#2573, TerminalFont, Windows) +* Fixed 64-bit values being truncated by `strtoul` on platforms where `unsigned long` is 32-bit. (Swap / PhysicalDisk / PhysicalMemory / GPU) +* Fixed read-only SQLite databases failing with `SQLITE_READONLY` when the database directory is not writable. (Packages) + * This fixes PKG package count detection on FreeBSD +* Fixed `{#keys}` and `{#title}` in module format strings not honoring the `brightColor` display option. (Format) +* Fixed `paddingTop` and `paddingLeft` being ignored by the `kitty-icat` image logo type. (Logo) +* Some internal cleanups and optimizations. + +Logos: +* Added ALT Atomic +* Removed Zerene # 2.68.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f09152a89..54fa295c53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2211,6 +2211,13 @@ if (BUILD_TESTS) PRIVATE libfastfetch ) + add_executable(fastfetch-test-base64 + tests/base64.c + ) + target_link_libraries(fastfetch-test-base64 + PRIVATE libfastfetch + ) + enable_testing() add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf) add_test(NAME test-list COMMAND fastfetch-test-list) @@ -2219,6 +2226,7 @@ if (BUILD_TESTS) 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) endif() ################## diff --git a/doc/help.json b/doc/help.json index d320a36749..14ef40fdb8 100644 --- a/doc/help.json +++ b/doc/help.json @@ -293,6 +293,7 @@ { "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": "enum", "optional": true, @@ -304,6 +305,21 @@ } } }, + { + "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 + } + }, { "long": "file", "desc": "Short for --logo-type file --logo ", diff --git a/src/common/impl/base64.c b/src/common/impl/base64.c index 3d743691b1..50d7d47183 100644 --- a/src/common/impl/base64.c +++ b/src/common/impl/base64.c @@ -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]; diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c index 8f311f4cc2..0768b4a377 100644 --- a/src/logo/image/im7.c +++ b/src/logo/image/im7.c @@ -3,6 +3,7 @@ #include "image.h" #include "common/library.h" #include "common/mallocHelper.h" + #include "common/strutil.h" #include #include @@ -104,6 +105,15 @@ static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const ch ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + // The raw pixel coders write image->depth bits per sample, not 8: a 1-bit grayscale source comes + // back as columns*4/8 bytes per row and a 16-bit one as columns*8, while the RGBA caller hands + // the blob on as RGBA8 and derives its length from width*height*4. Pin the depth for the raw + // formats only -- the SIXEL coder quantises on its own, so it keeps the source depth and its + // output stays byte identical. + if (ffStrEquals(magick, "RGBA")) { + image->depth = 8; + } + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); if (blob == nullptr || length == 0) { goto cleanup; @@ -148,6 +158,17 @@ bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const return false; } + // FFImageBuffer carries no length, so every consumer derives it from width*height*4. Refuse any + // other size rather than let them read past the blob -- the raw coder's depth scaling used to + // produce one (see the depth pin in im7EncodeImage). + if (length != (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4) { + if (error) { + *error = "Image Magick did not return an RGBA8 buffer"; + } + free(blob); + return false; + } + out->data = blob; out->width = requestData->logoPixelWidth; out->height = requestData->logoPixelHeight; @@ -167,4 +188,420 @@ bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const return true; } +// Encodes pixels the caller already has. The selected-frame path composes its frame itself, so it +// can not go through ffImageSixelEncodeIM7 -- that one re-reads the source and would encode +// whatever the still path would have shown instead of the frame that was asked for. +bool ffImageSixelEncodeBufferIM7(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) { + // clang-format off + #if _WIN32 + FF_LIBRARY_LOAD(imageMagick, false, + "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, 0 + ) + #else + FF_LIBRARY_LOAD(imageMagick, false, + "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11, + "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11, + "libMagickCore-7" FF_LIBRARY_EXTENSION, 11 + ) + #endif + // clang-format on + + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ConstituteImage, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, false) + + ffMagickCoreGenesis(nullptr, MagickFalse); + + ExceptionInfo* exceptionInfo = ffAcquireExceptionInfo(); + if (exceptionInfo == nullptr) { + if (error) *error = "failed to acquire an ImageMagick exception"; + ffMagickCoreTerminus(); + return false; + } + + Image* image = ffConstituteImage(buffer->width, buffer->height, "RGBA", CharPixel, + buffer->data, exceptionInfo); + if (image == nullptr) { + if (error) *error = "failed to wrap the animation frame"; + ffDestroyExceptionInfo(exceptionInfo); + ffMagickCoreTerminus(); + return false; + } + + ImageInfo* imageInfoOut = ffAcquireImageInfo(); + if (imageInfoOut == nullptr) { + if (error) *error = "failed to acquire an ImageMagick image info"; + ffDestroyImage(image); + ffDestroyExceptionInfo(exceptionInfo); + ffMagickCoreTerminus(); + return false; + } + ffCopyMagickString(imageInfoOut->magick, "SIXEL", 6); + + size_t length = 0; + FF_AUTO_FREE void* blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); + + ffDestroyImageInfo(imageInfoOut); + ffDestroyImage(image); + ffDestroyExceptionInfo(exceptionInfo); + ffMagickCoreTerminus(); + + if (blob == nullptr || length == 0) { + if (error) *error = "failed to encode the animation frame as sixel"; + return false; + } + + ffStrbufSetNS(out, (uint32_t) length, (const char*) blob); + + // leak imageMagick to prevent fastfetch from crashing #552, as the static path does + imageMagick = nullptr; + return true; +} + +// --------------------------------------------------------------------------------------------- +// Animation +// +// ImageMagick reads an animated GIF as a list of *sub-frames*: every image holds only the frame's +// own rectangle in image->columns x image->rows, and where it belongs on the logical screen in +// image->page. CoalesceImages turns that into canvas-sized frames, which is the shape the session +// contract asks for. +// +// Its idea of composition is not a browser's, though, so three things are steered before it runs. +// All three were measured against an independent reference compositor rather than guessed, and so +// was the fourth difference, which cannot be steered and is normalised after the fact instead. The +// measurements and the memory cost that comes with reusing the composition are in +// doc/kitty-animation.md §11-E. +// --------------------------------------------------------------------------------------------- + +// The entry points the session calls have to outlive ffImageAnimationOpenIM7, so unlike the +// locals im7EncodeImage loads they belong to the session. +typedef struct FFIm7Animation { + FF_LIBRARY_SYMBOL(ResizeImage) + FF_LIBRARY_SYMBOL(ImageToBlob) + FF_LIBRARY_SYMBOL(DestroyImageList) + FF_LIBRARY_SYMBOL(DestroyImageInfo) + FF_LIBRARY_SYMBOL(DestroyExceptionInfo) + FF_LIBRARY_SYMBOL(MagickCoreTerminus) + FF_LIBRARY_SYMBOL(SetImageAlphaChannel) + + Image* images; // coalesced, so every frame is canvas sized + Image* cursor; // the next frame to hand out + ImageInfo* blobInfo; // magick "RGBA", reused for every frame + ExceptionInfo* exceptionInfo; + uint32_t nextIndex; + uint32_t outputWidth; + uint32_t outputHeight; + int32_t minGap; +} FFIm7Animation; + +// The aspect-ratio rule the static path applies inline, needed here for the canvas rather than +// for a decoded frame. +static bool im7ResolveTargetSize(FFLogoRequestData* requestData, uint32_t sourceWidth, uint32_t sourceHeight, const char** error) { + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = sourceWidth; + height = sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + + if (width == 0 || height == 0) { + if (error) *error = "invalid target dimensions"; + return false; + } + + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + return true; +} + +// Three measured differences between ImageMagick's composition and a browser's, all fixed on the +// input side because the composition itself is the part worth reusing: +// +// 1. ImageMagick fills the canvas with the logical screen's background colour, opaque. Browsers +// start transparent and never paint it. -> make every frame's background colour transparent. +// 2. A frame with no transparent index carries no alpha channel, and the canvas CoalesceImages +// clones from it inherits that, so the transparent fill above would be discarded. -> give +// those frames an opaque alpha channel, which is what having no transparent index means. +// 3. A frame that is never displayed (delay 0) must not erase the one before it, which is what +// browsers and kitty do. ImageMagick honours the disposal anyway. -> drop the disposal. +static void im7SteerComposition(FFIm7Animation* session, Image* images) { + for (Image* image = images; image != nullptr; image = image->next) { + image->background_color.red = 0; + image->background_color.green = 0; + image->background_color.blue = 0; + image->background_color.alpha = 0; + image->background_color.alpha_trait = BlendPixelTrait; + + if (image->alpha_trait == UndefinedPixelTrait) { + session->ffSetImageAlphaChannel(image, OpaqueAlphaChannel, session->exceptionInfo); + } + + if (image->delay == 0 && image->dispose == BackgroundDispose) { + image->dispose = NoneDispose; + } + } +} + +// Where a BackgroundDispose region is cleared, ImageMagick uses the background colour it read from +// the source and only forces the alpha to 0. The RGB under a zero alpha is unused by kitty and by +// chafa, but the sixel encoder does look at it, and the Windows backend clears to zero -- so the +// pixels are normalised here rather than left to differ between the two. +static void im7ClearTransparentPixels(uint8_t* pixels, size_t length) { + for (size_t i = 0; i + 3 < length; i += 4) { + if (pixels[i + 3] == 0) { + pixels[i] = 0; + pixels[i + 1] = 0; + pixels[i + 2] = 0; + } + } +} + +// The source's delay in centiseconds. GIFs are read with 100 ticks per second, so the conversion +// is a no-op there, but nothing guarantees that for every format the coder may hand back. +static uint32_t im7FrameDelayCs(const Image* image) { + if (image->ticks_per_second <= 0) { + return (uint32_t) image->delay; + } + + return (uint32_t) ((uint64_t) image->delay * 100u / (uint64_t) image->ticks_per_second); +} + +// ImageMagick counts how often the animation is *played*; the source counts how often it repeats +// after the first play. 0 means forever and 1 means the source declared no loop count at all, +// which is the -1 the session contract uses. Checked against the Windows backend on the same +// files: no NETSCAPE block -> 1 here and -1 there, NETSCAPE 0 -> 0 and 0, NETSCAPE 3 -> 4 and 3. +static int32_t im7LoopCount(const Image* image) { + if (image->iterations == 0) { + return 0; + } + if (image->iterations == 1) { + return -1; + } + + const size_t loops = image->iterations - 1; + return loops > (size_t) INT32_MAX ? INT32_MAX : (int32_t) loops; +} + +static bool im7GetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) { + FFIm7Animation* session = (FFIm7Animation*) ffImageAnimationGetImpl(animation); + + // CoalesceImages has already composed every frame, so a frame is only ever walked past once, + // in order -- and taking one does not depend on having taken the previous one. + if (index < session->nextIndex) { + *error = "animation frames must be taken in order"; + return false; + } + + while (session->nextIndex < index && session->cursor != nullptr) { + session->cursor = session->cursor->next; + ++session->nextIndex; + } + + if (session->cursor == nullptr) { + *error = "the animation has no such frame"; + return false; + } + + const uint32_t rawDelay = im7FrameDelayCs(session->cursor); + + // kitty's mapping, which the design settled on: the raw delay is in centiseconds, the 100 ms + // floor exists only for sources whose frames are *all* zero, and whatever is left at <= 0 means + // "gapless" rather than 100 ms. + const int32_t gap = (int32_t) (rawDelay > (uint32_t) session->minGap ? rawDelay : (uint32_t) session->minGap) * 10; + out->delayMs = gap > 0 ? gap : -1; + + // Each coalesced frame is canvas sized already; the scaling to the output size is per frame, + // exactly as it is for a still image. + Image* resized = session->ffResizeImage(session->cursor, session->outputWidth, session->outputHeight, + UndefinedFilter, session->exceptionInfo); + if (resized == nullptr) { + *error = "failed to resize the animation frame"; + return false; + } + + size_t length = 0; + void* blob = session->ffImageToBlob(session->blobInfo, resized, &length, session->exceptionInfo); + session->ffDestroyImageList(resized); + if (blob == nullptr || length != (size_t) session->outputWidth * session->outputHeight * 4) { + if (blob != nullptr) free(blob); + *error = "failed to export the animation frame"; + return false; + } + + im7ClearTransparentPixels((uint8_t*) blob, length); + + out->data = (uint8_t*) blob; + session->cursor = session->cursor->next; + ++session->nextIndex; + return true; +} + +static void im7FreeSession(FFIm7Animation* session) { + if (session->images) session->ffDestroyImageList(session->images); + if (session->blobInfo) session->ffDestroyImageInfo(session->blobInfo); + if (session->exceptionInfo) session->ffDestroyExceptionInfo(session->exceptionInfo); + // Only ever reached once MagickCoreGenesis has run, and the symbol load in front of that has + // already been checked, so this pointer is non-null here. + session->ffMagickCoreTerminus(); + + // The library handle is leaked on purpose, exactly as the static path does it: unloading + // libMagickCore while the process still exits through it crashes (#552). + free(session); +} + +static void im7DestroyAnimation(FFImageAnimation* animation) { + FFIm7Animation* session = (FFIm7Animation*) ffImageAnimationGetImpl(animation); + if (session != nullptr) { + im7FreeSession(session); + } +} + +bool ffImageAnimationOpenIM7(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) { + // clang-format off + #if _WIN32 + FF_LIBRARY_LOAD(imageMagick, false, + "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, 0 + ) + #else + FF_LIBRARY_LOAD(imageMagick, false, + "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11, + "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11, + "libMagickCore-7" FF_LIBRARY_EXTENSION, 11 + ) + #endif + // clang-format on + + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CoalesceImages, false) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, false) + + FFIm7Animation* session = (FFIm7Animation*) calloc(1, sizeof(*session)); + if (session == nullptr) { + if (error) *error = "out of memory"; + return false; + } + + // The session owns these, so every failure below has to free the session by hand rather than + // go through im7DestroyAnimation, which expects the whole thing to be in place. + session->ffResizeImage = (typeof(&ResizeImage)) dlsym(imageMagick, "ResizeImage"); + session->ffImageToBlob = (typeof(&ImageToBlob)) dlsym(imageMagick, "ImageToBlob"); + session->ffDestroyImageList = (typeof(&DestroyImageList)) dlsym(imageMagick, "DestroyImageList"); + session->ffDestroyImageInfo = (typeof(&DestroyImageInfo)) dlsym(imageMagick, "DestroyImageInfo"); + session->ffDestroyExceptionInfo = (typeof(&DestroyExceptionInfo)) dlsym(imageMagick, "DestroyExceptionInfo"); + session->ffMagickCoreTerminus = (typeof(&MagickCoreTerminus)) dlsym(imageMagick, "MagickCoreTerminus"); + session->ffSetImageAlphaChannel = (typeof(&SetImageAlphaChannel)) dlsym(imageMagick, "SetImageAlphaChannel"); + if (session->ffResizeImage == nullptr || session->ffImageToBlob == nullptr || + session->ffDestroyImageList == nullptr || session->ffDestroyImageInfo == nullptr || + session->ffDestroyExceptionInfo == nullptr || session->ffMagickCoreTerminus == nullptr || + session->ffSetImageAlphaChannel == nullptr) { + if (error) *error = "the ImageMagick library is incomplete"; + free(session); + return false; + } + + // The core stays initialised for the whole session: the Image objects below belong to it, and + // terminating it while they are alive would leave them dangling. The static path can afford to + // initialise and terminate within one call because it never keeps an Image across calls. + ffMagickCoreGenesis(nullptr, MagickFalse); + + session->exceptionInfo = ffAcquireExceptionInfo(); + if (session->exceptionInfo == nullptr) { + if (error) *error = "failed to acquire an ImageMagick exception"; + im7FreeSession(session); + return false; + } + + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == nullptr) { + if (error) *error = "failed to acquire an ImageMagick image info"; + im7FreeSession(session); + return false; + } + + //+1, because we need to copy the null byte too + ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); + + Image* raw = ffReadImage(imageInfoIn, session->exceptionInfo); + session->ffDestroyImageInfo(imageInfoIn); + if (raw == nullptr) { + if (error) *error = "failed to load the image source"; + im7FreeSession(session); + return false; + } + + im7SteerComposition(session, raw); + + session->images = ffCoalesceImages(raw, session->exceptionInfo); + // CoalesceImages built a new list; the one it was given is still ours to destroy. + session->ffDestroyImageList(raw); + if (session->images == nullptr) { + if (error) *error = "failed to compose the animation frames"; + im7FreeSession(session); + return false; + } + + // The coalesced frames are canvas sized, which is what the target size is derived from. + uint32_t frameCount = 0; + for (Image* image = session->images; image != nullptr; image = image->next) { + ++frameCount; + } + + if (frameCount == 0 || + !im7ResolveTargetSize(requestData, (uint32_t) session->images->columns, (uint32_t) session->images->rows, error)) { + if (frameCount == 0 && error) *error = "the image source has no frames"; + im7FreeSession(session); + return false; + } + + session->blobInfo = ffAcquireImageInfo(); + if (session->blobInfo == nullptr) { + if (error) *error = "failed to acquire an ImageMagick image info"; + im7FreeSession(session); + return false; + } + ffCopyMagickString(session->blobInfo->magick, "RGBA", 5); + + // kitty's rule: the 100 ms floor applies only when every frame's raw delay is <= 0. One frame + // with a delay settles it, so the normal case stops after the first read. + session->minGap = 10; + for (Image* image = session->images; image != nullptr; image = image->next) { + if (im7FrameDelayCs(image) > 0) { + session->minGap = 0; + break; + } + } + + session->cursor = session->images; + session->nextIndex = 0; + session->outputWidth = requestData->logoPixelWidth; + session->outputHeight = requestData->logoPixelHeight; + + FFImageAnimation* animation = ffImageAnimationCreate(frameCount, im7LoopCount(session->images), + session, im7GetFrame, im7DestroyAnimation); + if (animation == nullptr) { + if (error) *error = "out of memory"; + im7FreeSession(session); + return false; + } + + // leak imageMagick to prevent fastfetch from crashing #552, as the static path does + imageMagick = nullptr; + *out = animation; + return true; +} + #endif diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 0ae1348f5a..5bd440aa74 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -456,7 +456,21 @@ static bool printImageKittyDirect(bool printError) { #define FF_CACHE_FILE_SIXEL "sixel" #define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc" #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu" + // Animation payload. The envelope can not be cached because it carries the image id, and that + // id has to differ on every run; this entry holds only what is id-independent (see §7.2). + #define FF_CACHE_FILE_KITTY_ANIMATION "kittyanim" #define FF_CACHE_FILE_CHAFA "chafa" + // The frame the payloads in the same directory were rendered from. A selected frame is stored + // exactly like the first frame is -- same entries, same payload format -- so this sidecar is + // what tells the two apart, and what makes a change of --logo-animation-frame refresh the entry + // instead of being served the frame that was asked for last time. It can not be part of the + // entry name: the frame number ranges over the whole int32, and removeCachedFiles works from a + // fixed list that could never enumerate it. + // The entry sits next to every payload of the directory, so it describes whichever of them was + // written last. Switching --logo-protocol between two protocols that both have a payload here, + // and asking for different frames, can therefore pair one protocol's payload with the other's + // frame number; the cache has to be refreshed after such a switch (`--logo-cache regen`). + #define FF_CACHE_FILE_FRAME "frame" // Modification time of the image source the entry was produced from. Written last, so an // entry that was interrupted mid-write is never mistaken for a complete one. #define FF_CACHE_FILE_MTIME "mtime" @@ -466,6 +480,8 @@ static bool printImageKittyDirect(bool printError) { #include #include + #include "common/time.h" + #ifndef _WIN32 #include #else @@ -478,7 +494,14 @@ static bool printImageKittyDirect(bool printError) { #include static bool compressBlob(void** blob, size_t* length) { - FF_LIBRARY_LOAD(zlib, false, "libz" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD(zlib, false, + #ifdef _WIN32 + "zlib1" + #else + "libz" + #endif + FF_LIBRARY_EXTENSION, + 2) FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false) FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false) @@ -496,7 +519,15 @@ static bool compressBlob(void** blob, size_t* length) { return false; } - if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_BEST_COMPRESSION) != Z_OK) { + // Level 6, not 9. Measured on a 44 frame 400x400 GIF (2026-09-16): the level 9 encode is 573 ms + // of the 720 ms the whole kitty animation path takes, i.e. it dwarfs decoding and the per frame + // export combined. Level 6 does the same job in a fifth of the time for 4.9% more bytes, which is + // also the level kitten icat uses. Level 1 would halve it again but doubles the payload, and past + // that point the terminal is the slow one, not us. + // The gap is far wider on Windows, where the same frames cost 37.7 ms each at level 9 against + // 6.4 at level 6: raising this back to 9 now that the DLL name above actually loads is a 4.2x + // slowdown there (398 ms -> 1692 ms). Re-measure both platforms before touching it. + if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) { free(compressed); return false; } @@ -523,15 +554,10 @@ static void writeCacheData(FFLogoRequestData* requestData, const void* value, si ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); } -static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) { +// The character dimensions describe the source, not one particular rendering of it, so they live +// in their own entries. The animation path reuses them without writing a payload of its own. +static void writeImageSizeCache(FFLogoRequestData* requestData) { const FFOptionsLogo* options = &instance.config.logo; - // Calculate character dimensions - instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; - instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1; - - // Write cache files - writeCacheData(requestData, result->chars, result->length, cacheFileName); - if (options->width == 0) { writeCacheData(requestData, &requestData->logoCharacterWidth, sizeof(requestData->logoCharacterWidth), FF_CACHE_FILE_WIDTH); } @@ -539,7 +565,20 @@ static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* res if (options->height == 0) { writeCacheData(requestData, &requestData->logoCharacterHeight, sizeof(requestData->logoCharacterHeight), FF_CACHE_FILE_HEIGHT); } +} +// Records which frame the payload written alongside it came from. Every payload write is followed +// by this one, so asking for another frame refreshes the entry instead of being served the frame +// that was asked for last time. What goes in is the *requested* selector, not the index it resolves +// to: on a single-frame source every selector resolves to frame 0, and storing that would make +// every selector but one miss on every run. +static void writeFrameCache(FFLogoRequestData* requestData) { + const int32_t frame = instance.config.logo.animationFrame; + writeCacheData(requestData, &frame, sizeof(frame), FF_CACHE_FILE_FRAME); +} + +static void printImageResult(FFLogoRequestData* requestData, const FFstrbuf* result) { + const FFOptionsLogo* options = &instance.config.logo; // Write result to stdout ffPrintCharTimes('\n', options->paddingTop); if (options->position == FF_LOGO_POSITION_RIGHT) { @@ -560,6 +599,34 @@ static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* res } } +static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) { + const FFOptionsLogo* options = &instance.config.logo; + // Calculate character dimensions + instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1; + + // Write cache files + writeCacheData(requestData, result->chars, result->length, cacheFileName); + writeImageSizeCache(requestData); + + printImageResult(requestData, result); +} + +// Same as printImagePixels, but the payload is not written to the cache: the bytes handed to the +// terminal are not the cached ones. Used by the animation path, whose envelope carries the image +// id and therefore differs on every run, and which stores its frames in the `kittyanim` entry +// instead. +static void printImagePixelsNoCache(FFLogoRequestData* requestData, const FFstrbuf* result) { + const FFOptionsLogo* options = &instance.config.logo; + // Calculate character dimensions + instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1; + + writeImageSizeCache(requestData); + + printImageResult(requestData, result); +} + // The backends report the real pixel dimensions; the character dimensions are derived here static void fillCharacterDimensions(FFLogoRequestData* requestData) { requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth); @@ -572,10 +639,16 @@ static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* resu } printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL); + writeFrameCache(requestData); return true; } -static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode) { +// Appends one chunk of a chunked graphics command. `printEscapeCode` is false for the first chunk +// only, because the caller has already written the escape introducer and the control data. +// `controlPrefix` adds keys in front of `m`; animation frame data needs it on continuation chunks +// ("When sending animation frame data, subsequent chunks must also specify the a=f key"), the +// static path passes nullptr and is therefore unchanged. +static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode, const char* controlPrefix) { uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length; if (printEscapeCode) { @@ -584,6 +657,11 @@ static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length ffStrbufAppendC(result, ','); } + if (controlPrefix) { + ffStrbufAppendS(result, controlPrefix); + ffStrbufAppendC(result, ','); + } + ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0"); ffStrbufAppendC(result, ';'); ffStrbufAppendNS(result, chunkSize, *blob); @@ -592,6 +670,7 @@ static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length *blob += chunkSize; } +// The compressed and the uncompressed entry hold the same rendering and share one frame sidecar. static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* buffer) { size_t length = (size_t) buffer->width * buffer->height * 4; FF_AUTO_FREE void* blob = malloc(length); @@ -619,12 +698,13 @@ static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* if (isCompressed) { ffStrbufAppendS(&result, ",o=z"); } - appendKittyChunk(&result, ¤tPos, &remainingLength, false); + appendKittyChunk(&result, ¤tPos, &remainingLength, false, nullptr); while (remainingLength > 0) { - appendKittyChunk(&result, ¤tPos, &remainingLength, true); + appendKittyChunk(&result, ¤tPos, &remainingLength, true, nullptr); } printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED); + writeFrameCache(requestData); return true; } @@ -700,6 +780,7 @@ static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* ffLogoPrintChars(result.chars, false); writeCacheData(requestData, result.chars, result.length, FF_CACHE_FILE_CHAFA); + writeFrameCache(requestData); // FIXME: These functions must be imported from `libglib` dlls on Windows FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free); @@ -748,6 +829,91 @@ void ffImageDestroy(FFImageBuffer* buffer) { buffer->height = 0; } +void ffImageFrameDestroy(FFImageFrame* frame) { + free(frame->data); + frame->data = nullptr; + frame->delayMs = 0; +} + +// The session itself is platform independent; a backend only has to fill it in through +// ffImageAnimationCreate and implement the two callbacks. +struct FFImageAnimation { + uint32_t frameCount; + int32_t loopCount; + void* impl; + bool (*getFrame)(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error); + void (*destroy)(FFImageAnimation* animation); +}; + +FFImageAnimation* ffImageAnimationCreate(uint32_t frameCount, int32_t loopCount, void* impl, + bool (*getFrame)(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error), + void (*destroy)(FFImageAnimation* animation)) { + FFImageAnimation* animation = malloc(sizeof(*animation)); + if (animation == nullptr) { + return nullptr; + } + + *animation = (FFImageAnimation) { + .frameCount = frameCount, + .loopCount = loopCount, + .impl = impl, + .getFrame = getFrame, + .destroy = destroy, + }; + return animation; +} + +void* ffImageAnimationGetImpl(const FFImageAnimation* animation) { + return animation->impl; +} + +bool ffImageAnimationOpen(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) { + // The sessions belong to the backends: ImageIO composes frames internally, WIC does not and + // has to keep a canvas, ImageMagick needs CoalesceImages. Windows, macOS and ImageMagick 7 are + // wired up; ImageMagick 6 is deliberately left out (see image.h), and a build with none of them + // keeps failing loudly rather than falling back to a still image. + // + // The order mirrors ffImageCreate: on macOS ImageIO is the decoder even when ImageMagick is + // available too, so the animation has to come from the same place the stills do. + #ifdef _WIN32 + return ffImageAnimationOpenWIC(requestData, out, error); + #elif defined(__APPLE__) + return ffImageAnimationOpenImageIO(requestData, out, error); + #elif defined(FF_HAVE_IMAGEMAGICK7) + return ffImageAnimationOpenIM7(requestData, out, error); + #else + FF_UNUSED(requestData, out); + *error = "the image source can not be animated by this build"; + return false; + #endif +} + +uint32_t ffImageAnimationFrameCount(const FFImageAnimation* animation) { + return animation->frameCount; +} + +int32_t ffImageAnimationLoopCount(const FFImageAnimation* animation) { + return animation->loopCount; +} + +bool ffImageAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) { + if (index >= animation->frameCount) { + *error = "the requested frame is out of range"; + return false; + } + + return animation->getFrame(animation, index, out, error); +} + +void ffImageAnimationClose(FFImageAnimation* animation) { + if (animation == nullptr) { + return; + } + + animation->destroy(animation); + free(animation); +} + bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { // Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded // libsixel encoder takes over. Other platforms let ImageMagick encode straight from the @@ -783,6 +949,35 @@ bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const cha #endif } +bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) { + // Windows and macOS hand the pixels to the embedded encoder. ImageMagick has no way to encode + // pixels it was not given an Image for, so its SIXEL coder is reached through a ConstituteImage + // round trip instead. + #if defined(_WIN32) || defined(__APPLE__) + #ifdef FF_HAVE_SIXEL + return ffSixelEncode(buffer, out, error); + #else + FF_UNUSED(buffer, out); + if (error) { + *error = "sixel support is not compiled in"; + } + return false; + #endif + #else + #ifdef FF_HAVE_IMAGEMAGICK7 + return ffImageSixelEncodeBufferIM7(buffer, out, error); + #else + // Only the animation path calls this, and only ImageMagick 7 has an animation backend, so a + // build without it never gets here with something to encode. + FF_UNUSED(buffer, out); + if (error) { + *error = "sixel support is not compiled in"; + } + return false; + #endif + #endif +} + static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) { uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, fileName); @@ -833,6 +1028,15 @@ static uint64_t readCachedUint64(FFLogoRequestData* requestData, const char* cac return result; } +// The frame the payload in the same directory was rendered from. A payload with no sidecar predates +// the sidecar and holds the first frame, which is exactly what the default selector asks for, so +// treating it as such keeps the caches of older builds usable. +static int32_t readCachedFrame(FFLogoRequestData* requestData, const char* frameFileName) { + int32_t frame = FF_LOGO_ANIMATION_FRAME_FIRST; + readCachedData(requestData, &frame, sizeof(frame), frameFileName); + return frame; +} + // Drops everything a previous version of the source left in the entry directory. // The directory is keyed on the source path and the pixel size only, so it is reused across // edits; without this, a payload written for another logo type would be read back. @@ -844,9 +1048,13 @@ static void removeCachedFiles(FFLogoRequestData* requestData) { FF_CACHE_FILE_SIXEL, FF_CACHE_FILE_KITTY_COMPRESSED, FF_CACHE_FILE_KITTY_UNCOMPRESSED, + FF_CACHE_FILE_KITTY_ANIMATION, FF_CACHE_FILE_CHAFA, + FF_CACHE_FILE_FRAME, }; + // Every entry that is ever written is listed here; an entry left out would survive a source + // change and be read back for the new source. uint32_t cacheDirLength = requestData->cacheDir.length; for (uint32_t i = 0; i < ARRAY_SIZE(files); ++i) { ffStrbufAppendS(&requestData->cacheDir, files[i]); @@ -856,6 +1064,13 @@ static void removeCachedFiles(FFLogoRequestData* requestData) { } static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) { + if (instance.config.logo.animationFrame != readCachedFrame(requestData, FF_CACHE_FILE_FRAME)) { + // The entry holds the rendering of another frame, or -- for the animation selector -- of + // no single frame at all. Reporting a miss here is what sends the slow path off to render + // the one that was asked for. + return false; + } + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); readCachedStrbuf(requestData, &content, cacheFileName); @@ -867,6 +1082,411 @@ static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFi return true; } +// --------------------------------------------------------------------------------------------- +// kitty animations +// +// A `kittyanim` entry holds only what is independent of the image id: the pixel size, the frame +// count, each frame's gap, the loop count, and the frames' base64 payloads. The envelope is +// rebuilt on every run, because it carries the id and that id has to differ between runs. +// --------------------------------------------------------------------------------------------- + +static const char FF_KITTY_ANIMATION_MAGIC[4] = { 'F', 'K', 'A', '1' }; + +typedef struct FFKittyAnimationFrame { + int32_t delayMs; + uint32_t payloadLength; // base64 bytes this frame occupies inside the payload block + bool compressed; // the frame's base64 decodes to a zlib stream, so it needs `o=z` +} FFKittyAnimationFrame; + +typedef struct FFKittyAnimation { + uint32_t width; + uint32_t height; + uint32_t frameCount; + int32_t loopCount; // 0 = infinite; -1 = the source declares none + FFKittyAnimationFrame* frames; + char* payload; // base64 of every frame, concatenated in frame order + uint32_t payloadLength; +} FFKittyAnimation; + +static void destroyKittyAnimation(FFKittyAnimation* animation) { + free(animation->frames); + free(animation->payload); + animation->frames = nullptr; + animation->payload = nullptr; +} + +// The entry is written and read by the same machine, so the host's own representation is used. +static void appendRaw(FFstrbuf* result, const void* value, uint32_t size) { + ffStrbufAppendNS(result, size, (const char*) value); +} + +static void appendUint32(FFstrbuf* result, uint32_t value) { + appendRaw(result, &value, sizeof(value)); +} + +static void appendInt32(FFstrbuf* result, int32_t value) { + appendRaw(result, &value, sizeof(value)); +} + +typedef struct FFKittyAnimationReader { + const char* data; + uint32_t length; + uint32_t offset; +} FFKittyAnimationReader; + +static bool readRaw(FFKittyAnimationReader* reader, void* out, uint32_t size) { + if (size > reader->length - reader->offset) { + return false; + } + + memcpy(out, reader->data + reader->offset, size); + reader->offset += size; + return true; +} + +static bool readUint32(FFKittyAnimationReader* reader, uint32_t* out) { + return readRaw(reader, out, sizeof(*out)); +} + +static bool readInt32(FFKittyAnimationReader* reader, int32_t* out) { + return readRaw(reader, out, sizeof(*out)); +} + +static void serializeKittyAnimation(FFstrbuf* result, const FFKittyAnimation* animation) { + appendRaw(result, FF_KITTY_ANIMATION_MAGIC, sizeof(FF_KITTY_ANIMATION_MAGIC)); + appendUint32(result, animation->width); + appendUint32(result, animation->height); + appendUint32(result, animation->frameCount); + appendInt32(result, animation->loopCount); + + for (uint32_t i = 0; i < animation->frameCount; ++i) { + const FFKittyAnimationFrame* frame = &animation->frames[i]; + uint8_t flags = frame->compressed ? 1 : 0; + appendInt32(result, frame->delayMs); + appendUint32(result, frame->payloadLength); + appendRaw(result, &flags, sizeof(flags)); + } + + appendRaw(result, animation->payload, animation->payloadLength); +} + +static bool parseKittyAnimation(const char* data, uint32_t length, FFKittyAnimation* out, const char** error) { + FFKittyAnimationReader reader = { .data = data, .length = length, .offset = 0 }; + + char magic[sizeof(FF_KITTY_ANIMATION_MAGIC)]; + if (!readRaw(&reader, magic, sizeof(magic)) || memcmp(magic, FF_KITTY_ANIMATION_MAGIC, sizeof(magic)) != 0) { + *error = "the cached animation has an unknown format"; + return false; + } + + if (!readUint32(&reader, &out->width) || !readUint32(&reader, &out->height) || + !readUint32(&reader, &out->frameCount) || !readInt32(&reader, &out->loopCount)) { + *error = "the cached animation is truncated"; + return false; + } + + if (out->frameCount == 0) { + *error = "the cached animation has no frames"; + return false; + } + + out->frames = calloc(out->frameCount, sizeof(*out->frames)); + if (out->frames == nullptr) { + *error = "out of memory"; + return false; + } + + uint32_t totalPayloadLength = 0; + for (uint32_t i = 0; i < out->frameCount; ++i) { + FFKittyAnimationFrame* frame = &out->frames[i]; + uint8_t flags = 0; + if (!readInt32(&reader, &frame->delayMs) || !readUint32(&reader, &frame->payloadLength) || + !readRaw(&reader, &flags, sizeof(flags))) { + *error = "the cached animation is truncated"; + return false; + } + + frame->compressed = flags != 0; + + if (totalPayloadLength > UINT32_MAX - frame->payloadLength) { + *error = "the cached animation is too large"; + return false; + } + totalPayloadLength += frame->payloadLength; + } + + if (totalPayloadLength > reader.length - reader.offset) { + *error = "the cached animation is truncated"; + return false; + } + + out->payloadLength = totalPayloadLength; + out->payload = malloc((size_t) totalPayloadLength + 1); + if (out->payload == nullptr) { + *error = "out of memory"; + return false; + } + + memcpy(out->payload, reader.data + reader.offset, totalPayloadLength); + out->payload[totalPayloadLength] = '\0'; + return true; +} + +// The id has to differ on every run: reusing one makes the terminal append the new frames to the +// image the previous run left in its storage, which doubles the frame count. The low 24 bits take +// part in the colour / decoration encoding, so they are kept non-zero (kitten icat does the same +// in next_random()). The entropy comes from the clock, the pid and a stack address, which is +// plenty for a process that runs once. +static uint32_t getKittyImageId(void) { + uint32_t id = (uint32_t) ffTimeGetNow(); + id ^= (uint32_t) getpid() * 2654435761u; + id ^= (uint32_t) ((uintptr_t) &id >> 4); + id &= 0xFFFFFF; + return id != 0 ? id : 1; +} + +// The envelope, per the kitty protocol: the root frame is transmitted with `a=T` and has no gap +// of its own, so its gap is set with a separate `a=a` command; every further frame is an `a=f` +// command that replaces the pixels (`X=1`) because the backend already composed a full canvas. +// Every command carries the image id and `q=2`, so nothing is ever written back to the tty. +static void emitKittyAnimation(FFstrbuf* result, const FFKittyAnimation* animation, uint32_t imageId) { + const char* currentPos = animation->payload; + + for (uint32_t i = 0; i < animation->frameCount; ++i) { + const FFKittyAnimationFrame* frame = &animation->frames[i]; + size_t frameLength = frame->payloadLength; + + if (i == 0) { + // Here s / v are the source rectangle, not the animation state + ffStrbufAppendF(result, "\033_Ga=T,f=32,s=%u,v=%u,i=%u,q=2", animation->width, animation->height, imageId); + } else { + // A frame that covers the whole image is transmitted exactly like image data, plus the + // frame keys. s / v are not optional here even though they always repeat the image size: + // the terminal sizes the frame's canvas from them and rejects the command outright when + // they are missing. + ffStrbufAppendF(result, "\033_Ga=f,f=32,s=%u,v=%u,i=%u,z=%d,X=1,q=2", + animation->width, animation->height, imageId, (int) frame->delayMs); + } + + if (frame->compressed) { + ffStrbufAppendS(result, ",o=z"); + } + + appendKittyChunk(result, ¤tPos, &frameLength, false, nullptr); + while (frameLength > 0) { + // "When sending animation frame data, subsequent chunks must also specify the a=f key" + appendKittyChunk(result, ¤tPos, &frameLength, true, i == 0 ? "q=2" : "a=f,q=2"); + } + + if (i == 0) { + // "the first frame or root frame is created with the base image data and has no gap, + // so its gap must be set using this control code" + ffStrbufAppendF(result, "\033_Ga=a,i=%u,r=1,z=%d,q=2\033\\", imageId, (int) frame->delayMs); + } + } + + // v is the loop count here: 1 loops forever, N loops N-1 times + int32_t loops = 1; + if (animation->loopCount > 0) { + loops = animation->loopCount < INT32_MAX ? animation->loopCount + 1 : INT32_MAX; + } + ffStrbufAppendF(result, "\033_Ga=a,i=%u,v=%d,q=2\033\\", imageId, (int) loops); + ffStrbufAppendF(result, "\033_Ga=a,i=%u,s=3,q=2\033\\", imageId); +} + +static bool printCachedKittyAnimation(FFLogoRequestData* requestData) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!readCachedStrbuf(requestData, &content, FF_CACHE_FILE_KITTY_ANIMATION) || content.length == 0) { + return false; + } + + FFKittyAnimation animation = {}; + const char* error = "the cached animation is corrupt"; + if (!parseKittyAnimation(content.chars, content.length, &animation, &error)) { + return false; + } + + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(animation.payloadLength + animation.frameCount * 128 + 512); + emitKittyAnimation(&result, &animation, getKittyImageId()); + + const FFOptionsLogo* options = &instance.config.logo; + instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop; + printImageResult(requestData, &result); + + destroyKittyAnimation(&animation); + return true; +} + +// Decodes the source frame by frame, encodes every frame, and stores the result as a `kittyanim` +// entry. Frames are taken, encoded and released one at a time: keeping them all would cost +// frames * W * H * 4 bytes, which a tool that prints a logo once can not afford. +static bool encodeKittyAnimation(FFLogoRequestData* requestData, const char** error) { + FFImageAnimation* animation = nullptr; + if (!ffImageAnimationOpen(requestData, &animation, error)) { + return false; + } + + fillCharacterDimensions(requestData); + + const uint32_t frameCount = ffImageAnimationFrameCount(animation); + const int32_t loopCount = ffImageAnimationLoopCount(animation); + + if (frameCount == 1) { + // Nothing to animate. Rendering it through the static path keeps the cache entry, and + // therefore the bytes written to the terminal, identical to the default rendering. + FFImageFrame frame = {}; + bool ok = ffImageAnimationGetFrame(animation, 0, &frame, error); + ffImageAnimationClose(animation); + if (!ok) { + return false; + } + + FFImageBuffer buffer = { + .data = frame.data, + .width = requestData->logoPixelWidth, + .height = requestData->logoPixelHeight, + }; + ok = printImageKitty(requestData, &buffer); + ffImageDestroy(&buffer); + return ok; + } + + FFKittyAnimationFrame* frames = calloc(frameCount, sizeof(*frames)); + if (frames == nullptr) { + ffImageAnimationClose(animation); + *error = "out of memory"; + return false; + } + + const size_t frameSize = (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4; + FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate(); + bool ok = true; + + for (uint32_t i = 0; i < frameCount && ok; ++i) { + FFImageFrame frame = {}; + if (!ffImageAnimationGetFrame(animation, i, &frame, error)) { + ok = false; + break; + } + + frames[i].delayMs = frame.delayMs; + + FF_AUTO_FREE void* blob = malloc(frameSize); + if (blob == nullptr) { + ffImageFrameDestroy(&frame); + *error = "out of memory"; + ok = false; + break; + } + memcpy(blob, frame.data, frameSize); + ffImageFrameDestroy(&frame); + + size_t blobLength = frameSize; + #ifdef FF_HAVE_ZLIB + frames[i].compressed = compressBlob(&blob, &blobLength); + #else + frames[i].compressed = false; + #endif + + FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + blobLength * 4 / 3)); + ffBase64EncodeRaw((uint32_t) blobLength, (const char*) blob, &base64.length, base64.chars); + frames[i].payloadLength = base64.length; + ffStrbufAppend(&payload, &base64); + } + + if (ok) { + FFKittyAnimation built = { + .width = requestData->logoPixelWidth, + .height = requestData->logoPixelHeight, + .frameCount = frameCount, + .loopCount = loopCount, + .frames = frames, + .payload = payload.chars, + .payloadLength = payload.length, + }; + + FF_STRBUF_AUTO_DESTROY serialized = ffStrbufCreate(); + serializeKittyAnimation(&serialized, &built); + writeCacheData(requestData, serialized.chars, serialized.length, FF_CACHE_FILE_KITTY_ANIMATION); + + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(serialized.length + frameCount * 128 + 512); + emitKittyAnimation(&result, &built, getKittyImageId()); + printImagePixelsNoCache(requestData, &result); + } + + free(frames); + ffImageAnimationClose(animation); + return ok; +} + +// Maps --logo-animation-frame to a 0-based frame index. Negative values count back from the end, +// so -1 is the last frame. Anything out of range clamps instead of failing: the frame count of the +// source is only known once it has been opened, and a typo in a logo option is not worth aborting +// the run over (see §10.2). +static uint32_t getAnimationFrameIndex(int32_t requested, uint32_t frameCount) { + if (frameCount == 0) { + return 0; + } + + if (requested < 0) { + uint32_t back = (uint32_t) (-(int64_t) requested); + return back <= frameCount ? frameCount - back : 0; + } + + uint32_t index = (uint32_t) (requested - 1); + return index < frameCount ? index : frameCount - 1; +} + +// Renders one frame of an animated source as a still image. `N` is 1-based and negative values +// count from the end, matching --logo-animation-frame. Every protocol that goes through this slow +// path is served: the decoded frame is handed to the same renderers the static path uses, and the +// result is cached exactly like the first frame is (see FF_CACHE_FILE_FRAME). +static bool printAnimationFrame(FFLogoRequestData* requestData, const char** error) { + FFImageAnimation* animation = nullptr; + if (!ffImageAnimationOpen(requestData, &animation, error)) { + return false; + } + + fillCharacterDimensions(requestData); + + const uint32_t frameCount = ffImageAnimationFrameCount(animation); + FFImageFrame frame = {}; + bool ok = ffImageAnimationGetFrame(animation, getAnimationFrameIndex(instance.config.logo.animationFrame, frameCount), &frame, error); + ffImageAnimationClose(animation); + if (!ok) { + return false; + } + + FFImageBuffer buffer = { + .data = frame.data, + .width = requestData->logoPixelWidth, + .height = requestData->logoPixelHeight, + }; + + if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) { + // The sixel encoder belongs to the backend, so it does not go through ffImageCreate -- but + // it does have to be handed the frame that was just composed. ffImageSixelEncode would + // re-read the source and encode the first frame instead of the selected one. + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ok = ffImageSixelEncodeBuffer(&buffer, &result, error) && printImageSixel(requestData, &result); + } else if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) { + ok = printImageKitty(requestData, &buffer); + } +#if FF_HAVE_CHAFA + else if (requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA) { + ok = printImageChafa(requestData, &buffer); + } +#endif + else { + *error = "this image protocol can not render a selected frame"; + ok = false; + } + + ffImageDestroy(&buffer); + return ok; +} + static bool printCachedPixel(FFLogoRequestData* requestData) { FFOptionsLogo* options = &instance.config.logo; @@ -886,10 +1506,26 @@ static bool printCachedPixel(FFLogoRequestData* requestData) { } } + if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY && + options->animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE) { + // An animation entry holds payloads, not an envelope, so it can not be streamed out like + // the static ones. It is also never allowed to fall back to a static entry: doing so would + // silently show a still logo to a user who asked for an animation. + return printCachedKittyAnimation(requestData); + } + + // A selected frame shares the entry with the first frame; the sidecar is what tells the two + // apart. This also covers the animation selector: no still rendering may answer it. + if (options->animationFrame != readCachedFrame(requestData, FF_CACHE_FILE_FRAME)) { + return false; + } + FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD; if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) { fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED); if (!ffIsValidNativeFD(fd)) { + // The pre-existing pair. Falling back between them is fine, they hold the same + // rendering; falling back to anything else is not (see §7.1). fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED); } } else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) { @@ -1017,29 +1653,47 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { // 0 means the mtime could not be read, in which case the entry is never trusted. const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars); - if (instance.config.logo.cache == FF_LOGO_CACHE_ON && - sourceMtime != 0 && - readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime) { - bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA - ? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA) - : printCachedPixel(&requestData); - if (cacheValid) { - ffStrbufDestroy(&requestData.cacheDir); - return true; + bool sourceUnchanged = false; + if (instance.config.logo.cache == FF_LOGO_CACHE_ON && sourceMtime != 0) { + sourceUnchanged = readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime; + if (sourceUnchanged) { + bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA + ? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA) + : printCachedPixel(&requestData); + if (cacheValid) { + ffStrbufDestroy(&requestData.cacheDir); + return true; + } } } - // Cache miss. The entry directory is keyed on the source path and the pixel size only, so - // it is reused when the source is edited; drop what the previous version left behind. + // Cache miss. The entry directory is keyed on the source path and the pixel size only, so it + // is reused when the source is edited; drop what the previous version left behind. This only + // happens when the source actually changed: a miss on one entry of an unchanged source just + // means that rendering was never asked for before, and clearing the directory would throw + // away the sibling entries every time the requested rendering changes. // With the cache turned off the directory is left alone entirely. - if (instance.config.logo.cache != FF_LOGO_CACHE_OFF) { + if (instance.config.logo.cache != FF_LOGO_CACHE_OFF && !sourceUnchanged) { removeCachedFiles(&requestData); } const char* error = nullptr; bool printSuccessful = false; - if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) { + if (instance.config.logo.animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE && + requestData.type != FF_LOGO_TYPE_IMAGE_KITTY) { + // Only the kitty graphics protocol can play an animation. Falling through to the static + // path would silently show a still logo to a user who asked for an animation. + error = "the kitty graphics protocol is the only one that can play an animation"; + } else if (instance.config.logo.animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE) { + // Decided before anything is decoded: the static path never enters a loop over frames, + // and this path never goes through ffImageCreate. + printSuccessful = encodeKittyAnimation(&requestData, &error); + } else if (instance.config.logo.animationFrame != FF_LOGO_ANIMATION_FRAME_FIRST) { + // A frame other than the first. This must not fall through to the static path: that one + // only ever produces the first frame, so it would silently hand back the wrong image. + printSuccessful = printAnimationFrame(&requestData, &error); + } else if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) { // The sixel encoder belongs to the backend, so it is not fed through ffImageCreate FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); if (ffImageSixelEncode(&requestData, &result, &error)) { diff --git a/src/logo/image/image.h b/src/logo/image/image.h index f096dd7a85..25a6f4e9fa 100644 --- a/src/logo/image/image.h +++ b/src/logo/image/image.h @@ -44,11 +44,64 @@ void ffImageDestroy(FFImageBuffer* buffer); // the embedded libsixel on Windows. bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); +// The same, for pixels the caller already has. The selected-frame path needs this: it composes the +// frame itself, and ffImageSixelEncode would go back to the source and encode a different one. +bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error); + +// One frame of an animation: a fully composed canvas, RGBA8, straight (unassociated) alpha, +// no row padding, exactly requestData->logoPixelWidth x logoPixelHeight. +typedef struct FFImageFrame { + uint8_t* data; + int32_t delayMs; // >0: wait this many milliseconds; -1: gapless; 0: unspecified +} FFImageFrame; + +void ffImageFrameDestroy(FFImageFrame* frame); + +// Animation session. Opaque outside src/logo/image/. +// +// Contract: the backend hands out *composed* full-canvas frames through a sequential iterator. +// Composition belongs to the backend (ImageIO already does it, WIC does not), never to the +// output layer. Taking a frame transfers its ownership to the caller, so the caller never holds +// more than one frame. Frames must be taken in non-decreasing index order: the composition of +// frame i depends on frames 0..i-1. +// +// That says nothing about what the backend holds, though. ImageMagick's CoalesceImages composes +// every frame up front and keeps the list, so its peak is the whole animation rather than one +// frame -- 60 frames of 500x500 measured 532 MiB. Bounding that would mean writing the compositor +// by hand, which is the cost reusing the library's composition exists to avoid, so it is accepted +// and recorded instead. See doc/kitty-animation.md 5.4 and 11-E. +typedef struct FFImageAnimation FFImageAnimation; + +// Open a session. Like ffImageCreate, it must fill in requestData->logoPixelWidth / +// logoPixelHeight, because every frame has those dimensions. frameCount / loopCount must be +// known without decoding a frame, because a negative --logo-animation-frame index is resolved +// before the first frame is taken. +bool ffImageAnimationOpen(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); +uint32_t ffImageAnimationFrameCount(const FFImageAnimation* animation); +int32_t ffImageAnimationLoopCount(const FFImageAnimation* animation); // 0 = infinite; -1 = the source declares none +bool ffImageAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error); +void ffImageAnimationClose(FFImageAnimation* animation); + +// Called by the backends once they have a session ready. `destroy` must release the session and +// its backend state; `impl` is handed back to them untouched. +FFImageAnimation* ffImageAnimationCreate(uint32_t frameCount, int32_t loopCount, void* impl, + bool (*getFrame)(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error), + void (*destroy)(FFImageAnimation* animation)); + +// The session is opaque to the backends; this is how the callbacks they supply get their own +// state back out of it. Never null for a session that ffImageAnimationCreate built. +void* ffImageAnimationGetImpl(const FFImageAnimation* animation); + #endif #ifdef FF_HAVE_IMAGEMAGICK7 bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); +bool ffImageSixelEncodeBufferIM7(const FFImageBuffer* buffer, FFstrbuf* out, const char** error); +// ImageMagick 7 is the only ImageMagick version that gets animation: 6 would need its own port +// (PixelPacket / opacity instead of PixelInfo / alpha) and there is no way to test it here, so it +// is left unimplemented rather than written blind. See doc/kitty-animation.md §11-E. +bool ffImageAnimationOpenIM7(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); #endif #ifdef FF_HAVE_IMAGEMAGICK6 @@ -58,10 +111,12 @@ bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const #ifdef _WIN32 bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageAnimationOpenWIC(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); #endif #ifdef __APPLE__ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageAnimationOpenImageIO(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); #endif #ifdef FF_HAVE_SIXEL diff --git a/src/logo/image/imageio.c b/src/logo/image/imageio.c index a86e0ccc8a..3d3086d95b 100644 --- a/src/logo/image/imageio.c +++ b/src/logo/image/imageio.c @@ -6,51 +6,17 @@ #include #include -bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { - FF_CFTYPE_AUTO_RELEASE CFURLRef url = CFURLCreateFromFileSystemRepresentation( - kCFAllocatorDefault, - (const UInt8*) instance.config.logo.source.chars, - (CFIndex) instance.config.logo.source.length, - false); - if (url == nullptr) { - if (error) { - *error = "failed to create the image URL"; - } - return false; - } - - FF_CFTYPE_AUTO_RELEASE CGImageSourceRef source = CGImageSourceCreateWithURL(url, nullptr); - if (source == nullptr) { - if (error) { - *error = "unsupported or unreadable image format"; - } - return false; - } +// --------------------------------------------------------------------------------------------- +// Shared with the still path +// --------------------------------------------------------------------------------------------- - // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) - FF_CFTYPE_AUTO_RELEASE CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, nullptr); - if (image == nullptr) { - if (error) { - *error = "failed to get the first frame"; - } - return false; - } - - size_t sourceWidth = CGImageGetWidth(image); - size_t sourceHeight = CGImageGetHeight(image); - if (sourceWidth == 0 || sourceHeight == 0) { - if (error) { - *error = "invalid image dimensions"; - } - return false; - } - - // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) +// Fills in the missing dimension, keeping the source aspect ratio (same as the IM path). +static bool imageIOResolveTargetSize(FFLogoRequestData* requestData, uint32_t sourceWidth, uint32_t sourceHeight, const char** error) { uint32_t width = requestData->logoPixelWidth; uint32_t height = requestData->logoPixelHeight; if (width == 0 && height == 0) { - width = (uint32_t) sourceWidth; - height = (uint32_t) sourceHeight; + width = sourceWidth; + height = sourceHeight; } else if (width == 0) { width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); } else if (height == 0) { @@ -66,8 +32,13 @@ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, co requestData->logoPixelWidth = width; requestData->logoPixelHeight = height; + return true; +} - const bool sameSize = width == sourceWidth && height == sourceHeight; +// Decodes one CGImage into tightly packed straight (unassociated) RGBA8888 at width x height. +// Both paths need exactly this -- the still path once, the animation path once per frame. +static bool imageIOConvert(CGImageRef image, uint32_t width, uint32_t height, uint8_t** outPixels, const char** error) { + const bool sameSize = width == CGImageGetWidth(image) && height == CGImageGetHeight(image); // Decode straight into RGBA for the same-size fast path. For resizing, decode // into premultiplied ARGB so interpolation does not bleed transparent RGB into @@ -98,16 +69,13 @@ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, co if (sameSize) { // The requested format is already straight RGBA. If vImage did not add // row padding, transfer its allocation directly to the caller; otherwise - // copy rows into the required tightly packed FFImageBuffer allocation. + // copy rows into the required tightly packed allocation. if (src.rowBytes == dstStride) { - out->data = (uint8_t*) src.data; - out->width = width; - out->height = height; - src.data = nullptr; + *outPixels = (uint8_t*) src.data; return true; } - FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); if (pixels == nullptr) { free(src.data); if (error) { @@ -121,65 +89,390 @@ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, co dstStride); } free(src.data); - out->data = pixels; - out->width = width; - out->height = height; - pixels = nullptr; + *outPixels = pixels; return true; - } else { - FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); - if (pixels == nullptr) { - free(src.data); - if (error) { - *error = "out of memory"; - } - return false; + } + + uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + if (pixels == nullptr) { + free(src.data); + if (error) { + *error = "out of memory"; } + return false; + } - const vImage_Buffer dst = { - .data = pixels, - .width = width, - .height = height, - .rowBytes = dstStride - }; + const vImage_Buffer dst = { + .data = pixels, + .width = width, + .height = height, + .rowBytes = dstStride + }; - // Resample while still premultiplied: interpolating premultiplied data is the - // correct way to scale (it avoids the dark fringes you get from averaging - // straight-alpha RGB). NULL temp buffer lets vImage allocate internally. - vErr = vImageScale_ARGB8888(&src, &dst, nullptr, kvImageHighQualityResampling); - free(src.data); - if (vErr != kvImageNoError) { - if (error) { - *error = "failed to scale the image"; - } - return false; + // Resample while still premultiplied: interpolating premultiplied data is the + // correct way to scale (it avoids the dark fringes you get from averaging + // straight-alpha RGB). NULL temp buffer lets vImage allocate internally. + vErr = vImageScale_ARGB8888(&src, &dst, nullptr, kvImageHighQualityResampling); + free(src.data); + if (vErr != kvImageNoError) { + free(pixels); + if (error) { + *error = "failed to scale the image"; } + return false; + } - // Un-premultiply in place (pointwise, alpha == 0 is handled safely) so the result - // matches the kitty f=32 / chafa CHAFA_PIXEL_RGBA8_UNASSOCIATED contract. - vErr = vImageUnpremultiplyData_ARGB8888(&dst, &dst, kvImageNoFlags); - if (vErr != kvImageNoError) { - if (error) { - *error = "failed to un-premultiply the image"; - } - return false; + // Un-premultiply in place (pointwise, alpha == 0 is handled safely) so the result + // matches the kitty f=32 / chafa CHAFA_PIXEL_RGBA8_UNASSOCIATED contract. + vErr = vImageUnpremultiplyData_ARGB8888(&dst, &dst, kvImageNoFlags); + if (vErr != kvImageNoError) { + free(pixels); + if (error) { + *error = "failed to un-premultiply the image"; } + return false; + } - // Reorder ARGB -> RGBA in place (permute supports in-place when data/rowBytes match). - // The downstream consumers and the WIC backend all expect R,G,B,A byte order. - const uint8_t permuteMap[4] = { 1, 2, 3, 0 }; // A,R,G,B -> R,G,B,A - vErr = vImagePermuteChannels_ARGB8888(&dst, &dst, permuteMap, kvImageNoFlags); - if (vErr != kvImageNoError) { - if (error) { - *error = "failed to reorder image channels"; - } - return false; + // Reorder ARGB -> RGBA in place (permute supports in-place when data/rowBytes match). + // The downstream consumers and the WIC backend all expect R,G,B,A byte order. + const uint8_t permuteMap[4] = { 1, 2, 3, 0 }; // A,R,G,B -> R,G,B,A + vErr = vImagePermuteChannels_ARGB8888(&dst, &dst, permuteMap, kvImageNoFlags); + if (vErr != kvImageNoError) { + free(pixels); + if (error) { + *error = "failed to reorder image channels"; } + return false; + } - out->data = pixels; - out->width = width; - out->height = height; - pixels = nullptr; // Ownership is transferred to `out` - return true; + *outPixels = pixels; + return true; +} + +bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + FF_CFTYPE_AUTO_RELEASE CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) instance.config.logo.source.chars, + (CFIndex) instance.config.logo.source.length, + false); + if (url == nullptr) { + if (error) { + *error = "failed to create the image URL"; + } + return false; + } + + FF_CFTYPE_AUTO_RELEASE CGImageSourceRef source = CGImageSourceCreateWithURL(url, nullptr); + if (source == nullptr) { + if (error) { + *error = "unsupported or unreadable image format"; + } + return false; + } + + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) + FF_CFTYPE_AUTO_RELEASE CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, nullptr); + if (image == nullptr) { + if (error) { + *error = "failed to get the first frame"; + } + return false; + } + + const size_t sourceWidth = CGImageGetWidth(image); + const size_t sourceHeight = CGImageGetHeight(image); + if (sourceWidth == 0 || sourceHeight == 0) { + if (error) { + *error = "invalid image dimensions"; + } + return false; } + + if (!imageIOResolveTargetSize(requestData, (uint32_t) sourceWidth, (uint32_t) sourceHeight, error)) { + return false; + } + + if (!imageIOConvert(image, requestData->logoPixelWidth, requestData->logoPixelHeight, &out->data, error)) { + return false; + } + + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} + +// --------------------------------------------------------------------------------------------- +// Animation +// +// ImageIO hands out *composed* canvas-sized frames: CGImageSourceCreateImageAtIndex does the +// disposal work the Windows backend has to do itself, so the session here is thin -- it keeps the +// source open, resolves the canvas size and the per-frame gaps, and converts one frame per call. +// +// Two things it does not do, both measured rather than assumed (doc/kitty-animation.md 5.1.1): +// +// * A `disposal = background` region is cleared to opaque black, where browsers, kitty and +// ImageMagick leave it transparent. It can not be corrected: ImageIO exposes no disposal +// metadata at any level, so there is nothing to branch on. Accepted and documented. +// * Such a region is cleared even when the frame it belongs to is never displayed (delay 0), +// where browsers and kitty keep the frame before it. Same cause, same outcome -- and the +// common case is unaffected, because every ImageMagick optimiser emits disposal none/undefined. +// +// The frame count, the canvas size and every gap come from *metadata*: no frame is decoded before +// the first one is asked for, which is what the session contract requires (a negative +// --logo-animation-frame has to be resolved before the first frame is taken). +// --------------------------------------------------------------------------------------------- + +typedef struct FFImageIOAnimation { + CGImageSourceRef source; + uint32_t nextIndex; + uint32_t outputWidth; + uint32_t outputHeight; + int32_t minGap; + int32_t* delaysCs; // one per frame, read once at open time; a full scan is needed for minGap anyway +} FFImageIOAnimation; + +// Thin wrappers over common/apple/cf_helpers. They exist for two reasons the shared helpers +// deliberately do not cover: an absent dictionary has to be *tolerated* rather than queried +// (CFDictionaryGetValue on NULL is undefined), and every caller here treats "absent" and "of the +// wrong type" the same way, so the descriptive error string is dropped. +static CFDictionaryRef imageIOSubdict(CFDictionaryRef dict, CFStringRef key) { + CFDictionaryRef result = nullptr; + if (dict == nullptr || ffCfDictGetDict(dict, key, &result) != nullptr) { + return nullptr; + } + + return result; +} + +// Both report false when the key is absent, which is how "the source declares nothing" is told +// apart from "the source declares zero". +static bool imageIODictGetDouble(CFDictionaryRef dict, CFStringRef key, double* out) { + return dict != nullptr && ffCfDictGetDouble(dict, key, out) == nullptr; +} + +static bool imageIODictGetInt(CFDictionaryRef dict, CFStringRef key, int64_t* out) { + return dict != nullptr && ffCfDictGetInt64(dict, key, out) == nullptr; +} + +// The frame's delay in centiseconds, which is the unit both GIF and APNG store. +// +// kCGImagePropertyGIFUnclampedDelayTime is the one to read, not kCGImagePropertyGIFDelayTime: +// ImageIO clamps the latter at 100 ms (and the APNG one at 50 ms), so a source whose frames are +// [0, 5, 0] centiseconds comes back as [10, 5, 10]. That would defeat kitty's rule, which floors at +// 100 ms only when *every* frame is zero and otherwise treats a zero as "gapless". Measured on +// macOS 27: mixed_delay.gif reads 0.10/0.05/0.10 clamped against 0.00/0.05/0.00 unclamped, and the +// unclamped values are the ones written into the file. +static int32_t imageIOFrameDelayCs(CFDictionaryRef frameProperties) { + // A GIF keeps its timing in the GIF dictionary, an animated PNG in the PNG one, and the two + // are never both present. + CFDictionaryRef gif = imageIOSubdict(frameProperties, kCGImagePropertyGIFDictionary); + CFDictionaryRef png = imageIOSubdict(frameProperties, kCGImagePropertyPNGDictionary); + + double seconds = 0; + if (imageIODictGetDouble(gif, kCGImagePropertyGIFUnclampedDelayTime, &seconds) || + imageIODictGetDouble(gif, kCGImagePropertyGIFDelayTime, &seconds) || + imageIODictGetDouble(png, kCGImagePropertyAPNGUnclampedDelayTime, &seconds) || + imageIODictGetDouble(png, kCGImagePropertyAPNGDelayTime, &seconds)) { + if (seconds > 0) { + return (int32_t) lround(seconds * 100.0); + } + } + + return 0; +} + +// How often the animation repeats after its first play; 0 means forever and -1 means the source +// declared nothing. Neither is what ImageIO reports -- its two dictionaries count different things +// (measured on macOS 27): +// +// GIF : number of *plays*. NETSCAPE 3 -> 4, NETSCAPE 0 (forever) -> 0, no NETSCAPE at all -> 1. +// That is ImageMagick's `iterations` to the digit, and the same -1 conversion applies. +// Note that "no NETSCAPE" arrives as 1, *not* as 0: reading 0 as the missing value, or +// passing 1 straight through, would turn "loop forever" into "play twice". +// APNG : number of *repeats*. acTL num_plays 3 -> 3, 0 (forever) -> 0. +// +// Which dictionary the value came from is therefore the format check; there is no need to ask +// ImageIO for a UTI and string-compare it. +static int32_t imageIOLoopCount(CGImageSourceRef source) { + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef properties = CGImageSourceCopyProperties(source, nullptr); + if (properties == nullptr) { + return -1; + } + + int64_t value = 0; + if (imageIODictGetInt(imageIOSubdict(properties, kCGImagePropertyGIFDictionary), kCGImagePropertyGIFLoopCount, &value)) { + if (value == 0) { + return 0; + } + if (value == 1) { + return -1; + } + + const int64_t loops = value - 1; + return loops > INT32_MAX ? INT32_MAX : (int32_t) loops; + } + + if (imageIODictGetInt(imageIOSubdict(properties, kCGImagePropertyPNGDictionary), kCGImagePropertyAPNGLoopCount, &value)) { + if (value == 0) { + return 0; + } + + return value > INT32_MAX ? INT32_MAX : (int32_t) value; + } + + return -1; +} + +static bool imageIOGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) { + FFImageIOAnimation* session = (FFImageIOAnimation*) ffImageAnimationGetImpl(animation); + + // ImageIO can decode an arbitrary frame on demand, unlike the Windows backend, but the session + // contract is a sequential iterator and the composition of frame i does depend on 0..i-1, so an + // out-of-order caller is a bug worth reporting rather than something to paper over. + if (index < session->nextIndex) { + *error = "animation frames must be taken in order"; + return false; + } + + // kitty's mapping, which the design settled on: the raw delay is in centiseconds, the 100 ms + // floor exists only for sources whose frames are *all* zero, and whatever is left at <= 0 means + // "gapless" rather than 100 ms. + const int32_t rawDelay = session->delaysCs[index]; + const int32_t gap = (rawDelay > session->minGap ? rawDelay : session->minGap) * 10; + out->delayMs = gap > 0 ? gap : -1; + + FF_CFTYPE_AUTO_RELEASE CGImageRef image = CGImageSourceCreateImageAtIndex(session->source, index, nullptr); + if (image == nullptr) { + *error = "failed to decode the animation frame"; + return false; + } + + if (!imageIOConvert(image, session->outputWidth, session->outputHeight, &out->data, error)) { + return false; + } + + session->nextIndex = index + 1; + return true; +} + +static void imageIOFreeSession(FFImageIOAnimation* session) { + if (session->source != nullptr) { + CFRelease(session->source); + } + free(session->delaysCs); + free(session); +} + +static void imageIODestroyAnimation(FFImageAnimation* animation) { + FFImageIOAnimation* session = (FFImageIOAnimation*) ffImageAnimationGetImpl(animation); + if (session != nullptr) { + imageIOFreeSession(session); + } +} + +bool ffImageAnimationOpenImageIO(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) { + FF_CFTYPE_AUTO_RELEASE CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) instance.config.logo.source.chars, + (CFIndex) instance.config.logo.source.length, + false); + if (url == nullptr) { + if (error) { + *error = "failed to create the image URL"; + } + return false; + } + + // Frames are handed out one at a time and released by the caller, so there is nothing to gain + // from letting ImageIO keep its own copy: without this a long animation is decoded into its + // cache as it is played through, and the peak stops matching the still path's. + const void* optionKeys[] = { kCGImageSourceShouldCache }; + const void* optionValues[] = { kCFBooleanFalse }; + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, + optionKeys, optionValues, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + // Not auto-released: the session keeps it open until the last frame has been handed out. + CGImageSourceRef source = CGImageSourceCreateWithURL(url, options); + if (source == nullptr) { + if (error) { + *error = "unsupported or unreadable image format"; + } + return false; + } + + const size_t frameCount = CGImageSourceGetCount(source); + if (frameCount == 0) { + if (error) { + *error = "the image source has no frames"; + } + CFRelease(source); + return false; + } + + FFImageIOAnimation* session = (FFImageIOAnimation*) calloc(1, sizeof(*session)); + if (session == nullptr) { + if (error) { + *error = "out of memory"; + } + CFRelease(source); + return false; + } + session->source = source; // the session owns it from here on; every failure below frees it + + // The canvas size comes from frame 0's *properties*, not from decoding it: ImageIO hands out + // canvas-sized frames, so this is the same value the still path reads off the decoded CGImage, + // and reading it here keeps the contract's "no frame is decoded to open a session". + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef firstProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nullptr); + int64_t sourceWidth = 0; + int64_t sourceHeight = 0; + if (!imageIODictGetInt(firstProperties, kCGImagePropertyPixelWidth, &sourceWidth) || + !imageIODictGetInt(firstProperties, kCGImagePropertyPixelHeight, &sourceHeight) || + sourceWidth <= 0 || sourceHeight <= 0) { + if (error) { + *error = "failed to read the animation canvas size"; + } + imageIOFreeSession(session); + return false; + } + + if (!imageIOResolveTargetSize(requestData, (uint32_t) sourceWidth, (uint32_t) sourceHeight, error)) { + imageIOFreeSession(session); + return false; + } + session->outputWidth = requestData->logoPixelWidth; + session->outputHeight = requestData->logoPixelHeight; + + session->delaysCs = (int32_t*) malloc(frameCount * sizeof(*session->delaysCs)); + if (session->delaysCs == nullptr) { + if (error) { + *error = "out of memory"; + } + imageIOFreeSession(session); + return false; + } + + // kitty's rule: the 100 ms floor applies only when every frame's raw delay is <= 0. One frame + // with a delay settles the floor, but every frame is still visited here so that its delay is + // known by the time it is handed out. + session->minGap = 10; + for (size_t i = 0; i < frameCount; ++i) { + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(source, i, nullptr); + session->delaysCs[i] = imageIOFrameDelayCs(properties); + if (session->delaysCs[i] > 0) { + session->minGap = 0; + } + } + + FFImageAnimation* animation = ffImageAnimationCreate((uint32_t) frameCount, imageIOLoopCount(source), + session, imageIOGetFrame, imageIODestroyAnimation); + if (animation == nullptr) { + if (error) { + *error = "out of memory"; + } + imageIOFreeSession(session); + return false; + } + + *out = animation; + return true; } diff --git a/src/logo/image/wic.cpp b/src/logo/image/wic.cpp index cac0b2b1d7..d2ca3c4f7e 100644 --- a/src/logo/image/wic.cpp +++ b/src/logo/image/wic.cpp @@ -9,27 +9,162 @@ extern "C" { #include #include -bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { - const char* comError = ffInitCom(); - if (comError) { - if (error) *error = comError; +// --------------------------------------------------------------------------------------------- +// Metadata +// +// `QueryInterface(IID_IWICMetadataQueryReader)` returns E_NOINTERFACE on this platform for every +// decoder and every frame, so the interface has to be reached through the method instead. Copying +// the QueryInterface out of Microsoft's own WicAnimatedGif.cpp sample reads no data at all here. +// +// The two readers are not interchangeable: `/logscrdesc/*` and `/appext/*` exist only on the +// decoder's reader, `/imgdesc/*` and `/grctlext/*` only on a frame's. +// --------------------------------------------------------------------------------------------- + +static bool readMetadata(IWICMetadataQueryReader* reader, const wchar_t* name, PROPVARIANT* out) { + if (reader == nullptr) { + return false; + } + + PropVariantInit(out); + if (FAILED(reader->GetMetadataByName(name, out))) { + PropVariantClear(out); + return false; + } + + return true; +} + +// Every integer path used here is one of these; the width varies with the item. +static bool readUint(IWICMetadataQueryReader* reader, const wchar_t* name, uint32_t* out) { + PROPVARIANT value; + if (!readMetadata(reader, name, &value)) { + return false; + } + + bool ok = true; + switch (value.vt) { + case VT_UI1: *out = value.bVal; break; + case VT_UI2: *out = value.uiVal; break; + case VT_UI4: *out = value.ulVal; break; + case VT_I2: *out = (uint32_t) value.iVal; break; + case VT_I4: *out = (uint32_t) value.lVal; break; + default: ok = false; break; + } + + PropVariantClear(&value); + return ok; +} + +// The loop count lives in the NETSCAPE2.0 application extension: `[0]=size=3`, `[1]=sub-block +// id=1`, `[2..3]=LE16 count`. `/logscrdesc/LoopCount` and `/logscrdesc/Iterations` do not exist. +// A source without the extension declares no loop count at all, which is -1, not 0. +static bool readLoopCount(IWICMetadataQueryReader* reader, int32_t* out) { + PROPVARIANT value; + if (!readMetadata(reader, L"/appext/Data", &value)) { + return false; + } + + bool ok = false; + if (value.vt == (VARTYPE) (VT_VECTOR | VT_UI1) && value.caub.cElems >= 4 && + value.caub.pElems[0] == 3 && value.caub.pElems[1] == 1) { + *out = (int32_t) ((uint32_t) value.caub.pElems[2] | ((uint32_t) value.caub.pElems[3] << 8)); + ok = true; + } + + PropVariantClear(&value); + return ok; +} + +// --------------------------------------------------------------------------------------------- +// Pixels +// --------------------------------------------------------------------------------------------- + +// Scales `source` to `outWidth` x `outHeight` and converts it to straight-alpha RGBA8, which is +// what kitty (f=32) and chafa (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume. Ownership of +// `*outPixels` passes to the caller. +static bool convertToRGBA(IWICImagingFactory* factory, IWICBitmapSource* source, + uint32_t outWidth, uint32_t outHeight, uint8_t** outPixels, const char** error) { + UINT sourceWidth = 0, sourceHeight = 0; + if (FAILED(source->GetSize(&sourceWidth, &sourceHeight))) { + if (error) *error = "failed to query the image dimensions"; return false; } - FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr; - FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr; - FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; FF_AUTO_RELEASE_COM_OBJECT IWICBitmapScaler* scaler = nullptr; FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* premultiplyConverter = nullptr; FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* converter = nullptr; - if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, - IID_IWICImagingFactory, (void**) &factory))) { - if (error) *error = "WIC imaging factory is unavailable"; + IWICBitmapSource* sized = source; + if (outWidth != sourceWidth || outHeight != sourceHeight) { + // Same size: don't resample. ImageMagick clones the image in this case too, + // and resampling would only blur it. + // + // Otherwise scale premultiplied alpha to avoid transparent RGB values bleeding into + // the visible edge pixels. The final output is converted back to straight alpha below. + if (FAILED(factory->CreateFormatConverter(&premultiplyConverter)) || + FAILED(premultiplyConverter->Initialize(source, GUID_WICPixelFormat32bppPBGRA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) || + FAILED(factory->CreateBitmapScaler(&scaler)) || + FAILED(scaler->Initialize(premultiplyConverter, outWidth, outHeight, + (WICBitmapInterpolationMode) 0x4 /* WICBitmapInterpolationModeHighQualityCubic */))) { + if (error) *error = "image scaling failed"; + return false; + } + sized = scaler; + } + + // Normalize to straight-alpha RGBA8: kitty (f=32) and chafa + // (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume exactly this + if (FAILED(factory->CreateFormatConverter(&converter)) || + FAILED(converter->Initialize(sized, GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom))) { + if (error) *error = "pixel format conversion failed"; + return false; + } + + UINT stride = outWidth * 4; + uint8_t* pixels = (uint8_t*) malloc((size_t) stride * outHeight); + if (pixels == nullptr) { + if (error) *error = "out of memory"; + return false; + } + + // prc == nullptr means the whole image; WIC fills the buffer using the stride we pass in + if (FAILED(converter->CopyPixels(nullptr, stride, stride * outHeight, pixels))) { + free(pixels); + if (error) *error = "pixel copy failed"; + return false; + } + + *outPixels = pixels; + return true; +} + +// Fills in the missing dimension, keeping the source aspect ratio (same as the IM path) +static bool resolveTargetSize(FFLogoRequestData* requestData, uint32_t sourceWidth, uint32_t sourceHeight, const char** error) { + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = sourceWidth; + height = sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + + if (width == 0 || height == 0) { + if (error) *error = "invalid target dimensions"; return false; } - // The source path is UTF-8, WIC only accepts UTF-16 + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + return true; +} + +// The source path is UTF-8, WIC only accepts UTF-16 +static bool createDecoder(IWICImagingFactory* factory, IWICBitmapDecoder** out, const char** error) { wchar_t widePath[MAX_PATH + 1]; if (!NT_SUCCESS(RtlUTF8ToUnicodeN(widePath, (ULONG) sizeof(widePath), nullptr, instance.config.logo.source.chars, (ULONG) instance.config.logo.source.length + 1))) { @@ -38,11 +173,35 @@ bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const } if (FAILED(factory->CreateDecoderFromFilename(widePath, nullptr, GENERIC_READ, - WICDecodeMetadataCacheOnDemand, &decoder))) { + WICDecodeMetadataCacheOnDemand, out))) { if (error) *error = "unsupported or unreadable image format"; return false; } + return true; +} + +bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + const char* comError = ffInitCom(); + if (comError) { + if (error) *error = comError; + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; + + if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**) &factory))) { + if (error) *error = "WIC imaging factory is unavailable"; + return false; + } + + if (!createDecoder(factory, &decoder, error)) { + return false; + } + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) if (FAILED(decoder->GetFrame(0, &frame))) { if (error) *error = "failed to get the first frame"; @@ -56,72 +215,357 @@ bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const return false; } - // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) - uint32_t width = requestData->logoPixelWidth; - uint32_t height = requestData->logoPixelHeight; - if (width == 0 && height == 0) { - width = sourceWidth; - height = sourceHeight; - } else if (width == 0) { - width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); - } else if (height == 0) { - height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + if (!resolveTargetSize(requestData, sourceWidth, sourceHeight, error)) { + return false; } - if (width == 0 || height == 0) { - if (error) *error = "invalid target dimensions"; + uint8_t* pixels = nullptr; + if (!convertToRGBA(factory, frame, requestData->logoPixelWidth, requestData->logoPixelHeight, &pixels, error)) { return false; } - requestData->logoPixelWidth = width; - requestData->logoPixelHeight = height; + out->data = pixels; + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} - IWICBitmapSource* source = nullptr; - if (width == sourceWidth && height == sourceHeight) { - // Same size: don't resample. ImageMagick clones the image in this case too, - // and resampling would only blur it - source = frame; - } else { - // Scale premultiplied alpha to avoid transparent RGB values bleeding into - // the visible edge pixels. The final output is converted back to straight - // alpha below, matching FFImageBuffer's RGBA8 contract. - if (FAILED(factory->CreateFormatConverter(&premultiplyConverter)) || - FAILED(premultiplyConverter->Initialize(frame, GUID_WICPixelFormat32bppPBGRA, - WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) || - FAILED(factory->CreateBitmapScaler(&scaler)) || - FAILED(scaler->Initialize(premultiplyConverter, width, height, - (WICBitmapInterpolationMode) 0x4 /* WICBitmapInterpolationModeHighQualityCubic */))) { - if (error) *error = "image scaling failed"; +// --------------------------------------------------------------------------------------------- +// Animation +// +// WIC does not compose GIF frames: it hands out the raw sub-frame plus the metadata describing how +// to place it, so the canvas and the disposal handling are ours. The loop below follows the GIF +// model, which is also what Microsoft's own sample implements: +// +// 1. apply the *previous* frame's disposal to the canvas (0/1 keep, 2 clear its rectangle, +// 3 restore the snapshot taken before it was drawn); +// 2. if the current frame's disposal is 3, snapshot the canvas as it stands; +// 3. draw the current frame's pixels at its own (left, top). +// +// The canvas is scaled once per frame on the way out, never per sub-frame: scaling in sub-frame +// coordinates would introduce edge errors. +// --------------------------------------------------------------------------------------------- + +struct FFWicAnimation { + IWICImagingFactory* factory; + IWICBitmapDecoder* decoder; + IWICMetadataQueryReader* decoderReader; // container level; nullptr for formats without one + + uint32_t canvasWidth; + uint32_t canvasHeight; + uint8_t* canvas; // RGBA8, the composed canvas + uint8_t* snapshot; // the canvas as it was before a disposal=3 frame was drawn + + uint32_t outputWidth; + uint32_t outputHeight; + + uint32_t previousLeft; + uint32_t previousTop; + uint32_t previousWidth; + uint32_t previousHeight; + uint32_t previousDisposal; + uint32_t previousRawDelay; // centiseconds, as the source declares it + bool hasPrevious; + + uint32_t nextIndex; // frames are handed out in order; frame i depends on 0..i-1 + int32_t minGap; // 0, or 10 when every frame's raw delay is <= 0 +}; + +static void clearRect(FFWicAnimation* animation, uint32_t left, uint32_t top, uint32_t width, uint32_t height) { + const size_t rowBytes = (size_t) width * 4; + for (uint32_t y = 0; y < height; ++y) { + memset(animation->canvas + (((size_t) (top + y) * animation->canvasWidth) + left) * 4, 0, rowBytes); + } +} + +// Reads frame `index`'s metadata, applies the previous frame's disposal, snapshots if this frame +// asks for it, and draws the frame onto the canvas. Reports the frame's gap in milliseconds. +static bool composeFrame(FFWicAnimation* animation, uint32_t index, int32_t* delayMs, const char** error) { + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* reader = nullptr; + + if (FAILED(animation->decoder->GetFrame(index, &frame)) || + FAILED(frame->GetMetadataQueryReader(&reader))) { + *error = "failed to read the frame metadata"; + return false; + } + + uint32_t left = 0, top = 0, width = 0, height = 0, disposal = 0, rawDelay = 0; + if (!readUint(reader, L"/imgdesc/Left", &left) || !readUint(reader, L"/imgdesc/Top", &top) || + !readUint(reader, L"/imgdesc/Width", &width) || !readUint(reader, L"/imgdesc/Height", &height)) { + // Not a GIF: there is no placement metadata at all and the frame is the whole canvas. + UINT frameWidth = 0, frameHeight = 0; + if (FAILED(frame->GetSize(&frameWidth, &frameHeight))) { + *error = "failed to read the frame rectangle"; return false; } - source = scaler; + left = 0; + top = 0; + width = frameWidth; + height = frameHeight; } - // Normalize to straight-alpha RGBA8: kitty (f=32) and chafa - // (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume exactly this - if (FAILED(factory->CreateFormatConverter(&converter)) || - FAILED(converter->Initialize(source, GUID_WICPixelFormat32bppRGBA, - WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom))) { - if (error) *error = "pixel format conversion failed"; + if (width == 0 || height == 0 || + left + width > animation->canvasWidth || top + height > animation->canvasHeight) { + *error = "the frame rectangle does not fit the canvas"; return false; } - UINT stride = width * 4; - FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc((size_t) stride * height); - if (pixels == nullptr) { + // Both are absent on anything that is not a GIF, in which case the frame covers the canvas and + // there is nothing to animate. + readUint(reader, L"/grctlext/Disposal", &disposal); + readUint(reader, L"/grctlext/Delay", &rawDelay); + + // kitty's mapping, which the design settled on: the raw delay is in centiseconds, the 100 ms + // floor exists only for sources whose frames are *all* zero, and whatever is left at <= 0 means + // "gapless" rather than 100 ms. Not the "<90 -> 90" floor of the WIC sample, which its own + // comment admits destroys legitimate zero-gap frames. + const int32_t gap = (int32_t) (rawDelay > (uint32_t) animation->minGap ? rawDelay : (uint32_t) animation->minGap) * 10; + *delayMs = gap > 0 ? gap : -1; + + if (animation->hasPrevious) { + if (animation->previousDisposal == 2 && animation->previousRawDelay != 0) { + // No colour table is reachable through WIC - only BackgroundColorIndex, never a palette + // - so "restore to background" can only mean transparent. + // + // A previous frame with a zero delay keeps its pixels instead: it was never displayed + // for any length of time, so erasing it would only flicker. This is the rule the + // reference compositor was validated against. + clearRect(animation, animation->previousLeft, animation->previousTop, + animation->previousWidth, animation->previousHeight); + } else if (animation->previousDisposal == 3) { + memcpy(animation->canvas, animation->snapshot, + (size_t) animation->canvasWidth * animation->canvasHeight * 4); + } + } + + if (disposal == 3) { + const size_t canvasSize = (size_t) animation->canvasWidth * animation->canvasHeight * 4; + if (animation->snapshot == nullptr) { + animation->snapshot = (uint8_t*) malloc(canvasSize); + if (animation->snapshot == nullptr) { + *error = "out of memory"; + return false; + } + } + memcpy(animation->snapshot, animation->canvas, canvasSize); + } + + // Decode the sub-frame at its own size; the canvas is scaled on the way out instead. + FF_AUTO_FREE uint8_t* pixels = nullptr; + if (!convertToRGBA(animation->factory, frame, width, height, &pixels, error)) { + return false; + } + + for (uint32_t y = 0; y < height; ++y) { + const uint8_t* source = pixels + (size_t) y * width * 4; + uint8_t* target = animation->canvas + (((size_t) (top + y) * animation->canvasWidth) + left) * 4; + + for (uint32_t x = 0; x < width; ++x, source += 4, target += 4) { + const uint8_t alpha = source[3]; + if (alpha == 0) { + // GIF transparency is one bit, so "keep" is exact and needs no arithmetic at all. + continue; + } + if (alpha == 255) { + memcpy(target, source, 4); + continue; + } + + // Not a GIF. Composite over, so a source with real alpha still lands sanely; for the + // one-bit alpha above this branch is never taken. + for (uint32_t channel = 0; channel < 3; ++channel) { + target[channel] = (uint8_t) (((uint32_t) source[channel] * alpha + + (uint32_t) target[channel] * (255 - alpha) + 127) / 255); + } + target[3] = (uint8_t) (alpha + (uint32_t) target[3] * (255 - alpha) / 255); + } + } + + animation->previousLeft = left; + animation->previousTop = top; + animation->previousWidth = width; + animation->previousHeight = height; + animation->previousDisposal = disposal; + animation->previousRawDelay = rawDelay; + animation->hasPrevious = true; + return true; +} + +// Scales the composed canvas to the output size and hands it over. +static bool outputCanvas(FFWicAnimation* animation, FFImageFrame* out, const char** error) { + const UINT stride = animation->canvasWidth * 4; + + FF_AUTO_RELEASE_COM_OBJECT IWICBitmap* bitmap = nullptr; + if (FAILED(animation->factory->CreateBitmapFromMemory(animation->canvasWidth, animation->canvasHeight, + GUID_WICPixelFormat32bppRGBA, stride, stride * animation->canvasHeight, animation->canvas, &bitmap))) { + *error = "failed to wrap the composed canvas"; + return false; + } + + return convertToRGBA(animation->factory, bitmap, animation->outputWidth, animation->outputHeight, + &out->data, error); +} + +static bool wicGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) { + FFWicAnimation* session = (FFWicAnimation*) ffImageAnimationGetImpl(animation); + + // The composition of frame i depends on frames 0..i-1, so they are walked in order and the + // intermediate canvases are simply discarded. + if (index < session->nextIndex) { + *error = "animation frames must be taken in order"; + return false; + } + + int32_t delayMs = -1; + for (uint32_t i = session->nextIndex; i <= index; ++i) { + if (!composeFrame(session, i, &delayMs, error)) { + return false; + } + session->nextIndex = i + 1; + } + + if (!outputCanvas(session, out, error)) { + return false; + } + + out->delayMs = delayMs; + return true; +} + +static void wicDestroyAnimation(FFImageAnimation* animation) { + FFWicAnimation* session = (FFWicAnimation*) ffImageAnimationGetImpl(animation); + if (session == nullptr) { + return; + } + + free(session->canvas); + free(session->snapshot); + if (session->decoderReader) session->decoderReader->Release(); + if (session->decoder) session->decoder->Release(); + if (session->factory) session->factory->Release(); + free(session); +} + +// Reads one frame's raw delay, for the all-zero check that decides the 100 ms floor. +static bool readFrameDelay(IWICBitmapDecoder* decoder, uint32_t index, uint32_t* out) { + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* reader = nullptr; + + if (FAILED(decoder->GetFrame(index, &frame)) || FAILED(frame->GetMetadataQueryReader(&reader))) { + return false; + } + + return readUint(reader, L"/grctlext/Delay", out); +} + +bool ffImageAnimationOpenWIC(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) { + const char* comError = ffInitCom(); + if (comError) { + if (error) *error = comError; + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* firstFrame = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* decoderReader = nullptr; + + if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**) &factory))) { + if (error) *error = "WIC imaging factory is unavailable"; + return false; + } + + if (!createDecoder(factory, &decoder, error)) { + return false; + } + + // Cheap: WICDecodeMetadataCacheOnDemand reads only the header, so the frame count - which a + // negative --logo-animation-frame index is resolved against - needs no decoding. + UINT frameCount = 0; + if (FAILED(decoder->GetFrameCount(&frameCount)) || frameCount == 0) { + if (error) *error = "the image source has no frames"; + return false; + } + + if (FAILED(decoder->GetFrame(0, &firstFrame))) { + if (error) *error = "failed to get the first frame"; + return false; + } + + // Absent for every format without a query reader (PNG, JPEG, ...), which is not an error: the + // frame is the canvas then. + decoder->GetMetadataQueryReader(&decoderReader); + + // GetSize() on a frame gives the *sub-frame* rectangle, never the canvas, so the canvas has to + // come from the logical screen descriptor. + uint32_t canvasWidth = 0, canvasHeight = 0; + if (!readUint(decoderReader, L"/logscrdesc/Width", &canvasWidth) || + !readUint(decoderReader, L"/logscrdesc/Height", &canvasHeight)) { + UINT width = 0, height = 0; + if (FAILED(firstFrame->GetSize(&width, &height)) || width == 0 || height == 0) { + if (error) *error = "invalid image dimensions"; + return false; + } + canvasWidth = width; + canvasHeight = height; + } + + if (!resolveTargetSize(requestData, canvasWidth, canvasHeight, error)) { + return false; + } + + // 0 means "loop forever", -1 that the source declares no loop count at all. + int32_t loopCount = -1; + readLoopCount(decoderReader, &loopCount); + + // kitty's rule: the 100 ms floor applies only when every frame's raw delay is <= 0. One frame + // with a delay settles it, so the normal case stops after the first read. + int32_t minGap = 10; + for (UINT i = 0; i < frameCount; ++i) { + uint32_t rawDelay = 0; + if (!readFrameDelay(decoder, i, &rawDelay) || rawDelay > 0) { + minGap = 0; + break; + } + } + + FFWicAnimation* session = (FFWicAnimation*) calloc(1, sizeof(*session)); + if (session == nullptr) { if (error) *error = "out of memory"; return false; } - // prc == nullptr means the whole image; WIC fills the buffer using the stride we pass in - if (FAILED(converter->CopyPixels(nullptr, stride, stride * height, pixels))) { - if (error) *error = "pixel copy failed"; + session->canvas = (uint8_t*) calloc((size_t) canvasWidth * canvasHeight * 4, 1); + if (session->canvas == nullptr) { + free(session); + if (error) *error = "out of memory"; return false; } - out->data = pixels; - out->width = width; - out->height = height; - pixels = nullptr; // Ownership is transferred to `out` + session->factory = factory; + session->decoder = decoder; + session->decoderReader = decoderReader; + session->canvasWidth = canvasWidth; + session->canvasHeight = canvasHeight; + session->outputWidth = requestData->logoPixelWidth; + session->outputHeight = requestData->logoPixelHeight; + session->minGap = minGap; + session->nextIndex = 0; + + FFImageAnimation* animation = ffImageAnimationCreate(frameCount, loopCount, session, wicGetFrame, wicDestroyAnimation); + if (animation == nullptr) { + free(session->canvas); + free(session); + if (error) *error = "out of memory"; + return false; + } + + // The session owns these from here on, so the cleanup guards must not release them. + factory = nullptr; + decoder = nullptr; + decoderReader = nullptr; + *out = animation; return true; } diff --git a/src/options/logo.c b/src/options/logo.c index 08a35cc456..dc64cc607b 100644 --- a/src/options/logo.c +++ b/src/options/logo.c @@ -19,6 +19,7 @@ void ffOptionsInitLogo(FFOptionsLogo* options) { options->preserveAspectRatio = false; options->cache = FF_LOGO_CACHE_ON; options->position = FF_LOGO_POSITION_LEFT; + options->animationFrame = FF_LOGO_ANIMATION_FRAME_FIRST; #if FF_HAVE_CHAFA options->chafaFgOnly = false; @@ -124,6 +125,8 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons { "top", FF_LOGO_POSITION_TOP }, {}, }); + } else if (ffStrEqualsIgnCase(subKey, "animation-frame")) { + options->animationFrame = ffOptionParseInt32(key, value); } else { return false; } @@ -365,6 +368,12 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo } options->position = (FFLogoPosition) value; continue; + } else if (unsafe_yyjson_equals_str(key, "animationFrame")) { + if (!yyjson_is_int(val)) { + return "Property 'logo.animationFrame' must be an integer"; + } + options->animationFrame = (int32_t) yyjson_get_sint(val); + continue; } else if (unsafe_yyjson_equals_str(key, "chafa")) { #if FF_HAVE_CHAFA if (!yyjson_is_obj(val)) { @@ -553,6 +562,8 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { "right", })[options->position]); + yyjson_mut_obj_add_int(doc, obj, "animationFrame", options->animationFrame); + #if FF_HAVE_CHAFA { yyjson_mut_val* chafa = yyjson_mut_obj(doc); diff --git a/src/options/logo.h b/src/options/logo.h index c6e1eb9f62..503afa9927 100644 --- a/src/options/logo.h +++ b/src/options/logo.h @@ -36,6 +36,12 @@ typedef enum FFLogoCacheStrategy: uint8_t { FF_LOGO_CACHE_REGEN, // ignore any existing cached rendering and regenerate it } FFLogoCacheStrategy; +// Which frame of the image source to render. 0 is the only value that prints an animation; the +// others all produce a static image, which keeps the default (1, the first frame) byte-identical +// to the behaviour before animations existed. +#define FF_LOGO_ANIMATION_FRAME_FIRST 1 +#define FF_LOGO_ANIMATION_FRAME_ANIMATE 0 + typedef struct FFOptionsLogo { FFstrbuf source; FFLogoType type; @@ -50,6 +56,10 @@ typedef struct FFOptionsLogo { bool printRemaining; bool preserveAspectRatio; FFLogoCacheStrategy cache; + // 0 = animate, N > 0 = the N-th frame (1-based), N < 0 = the |N|-th frame from the end. + // Kept as given by the user: the cache entry name is built from it before the frame count + // is known, and negative values stay meaningful across runs (see image.c). + int32_t animationFrame; #if FF_HAVE_CHAFA bool chafaFgOnly; diff --git a/tests/base64.c b/tests/base64.c new file mode 100644 index 0000000000..5f05f55de0 --- /dev/null +++ b/tests/base64.c @@ -0,0 +1,153 @@ +#include "common/base64.h" +#include "common/strutil.h" +#include "common/textModifier.h" + +#include +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +// The empty string is a valid case and yields no bytes. +static uint32_t fromHex(const char* hex, uint8_t* out) { + uint32_t length = 0; + for (const char* p = hex; p[0] != '\0' && p[1] != '\0'; p += 2) { + unsigned value = 0; + if (sscanf(p, "%2x", &value) != 1) { + return 0; + } + out[length++] = (uint8_t) value; + } + return length; +} + +typedef struct FFBase64EncodingCase { + const char* name; + const char* hex; + const char* expected; +} FFBase64EncodingCase; + +// The RFC 4648 vectors cover the whole groups. The rest cover the partial group at the end of a +// source whose length is not a multiple of three, which is where the encoder used to go wrong: a +// plain `char` is signed on the supported platforms, so widening a byte with the high bit set into +// the 64 bit accumulator sign extended it, and the six bit fields read back out of that accumulator +// no longer held the byte's own bits. +// +// Only the two byte partial group was affected. One byte left over is shifted into place and never +// OR-ed into, so its sign extension lands above the fields that are read; two bytes left over OR the +// second byte in after shifting it by 8, which drags the sign bits down into them. +static const FFBase64EncodingCase ENCODING_CASES[] = { + // RFC 4648 test vectors + { "rfc4648-empty", "", "" }, + { "rfc4648-f", "66", "Zg==" }, + { "rfc4648-fo", "666f", "Zm8=" }, + { "rfc4648-foo", "666f6f", "Zm9v" }, + { "rfc4648-foob", "666f6f62", "Zm9vYg==" }, + { "rfc4648-fooba", "666f6f6261", "Zm9vYmE=" }, + { "rfc4648-foobar", "666f6f626172", "Zm9vYmFy" }, + + // One byte left over (length % 3 == 1) + { "single-80", "41414180", "QUFBgA==" }, + { "single-b9", "414141b9", "QUFBuQ==" }, + { "single-c6", "414141c6", "QUFBxg==" }, + { "single-ff", "414141ff", "QUFB/w==" }, + + // Two bytes left over (length % 3 == 2), with the high bit on the second byte + { "pair-second-80", "4141414180", "QUFBQYA=" }, + { "pair-second-9c", "414141419c", "QUFBQZw=" }, + { "pair-second-b9", "41414141b9", "QUFBQbk=" }, + { "pair-second-c6", "41414141c6", "QUFBQcY=" }, + { "pair-second-ff", "41414141ff", "QUFBQf8=" }, + + // Two bytes left over (length % 3 == 2), with the high bit on the first byte + { "pair-first-80", "4141418041", "QUFBgEE=" }, + { "pair-first-9c", "4141419c41", "QUFBnEE=" }, + { "pair-first-b9", "414141b941", "QUFBuUE=" }, + { "pair-first-c6", "414141c641", "QUFBxkE=" }, + { "pair-first-ff", "414141ff41", "QUFB/0E=" }, + + // Two bytes left over (length % 3 == 2), with the high bit on both + { "pair-both-80", "4141418080", "QUFBgIA=" }, + { "pair-both-b9", "414141b9b9", "QUFBubk=" }, + { "pair-both-c6", "414141c6c6", "QUFBxsY=" }, + { "pair-both-ff", "414141ffff", "QUFB//8=" }, + + // The shape the kitty animation encoder hands over: a zlib stream for one 100x100 RGBA frame. + // Its last two bytes (0x79 0xb9) are what the sign extension used to mangle. The terminal then + // rejected the frame with a zlib checksum error, so the animation was transmitted correctly but + // never played. + { "kitty-animation-frame", "78daedd1310d00000cc3b0f227dd91a876f9308124490300000000000000000000000000000000000000231dd3d40f3ffcf0c30f3ffcf0c30f3ff0c30ffcf0c30f3ffcf0c30f3ffcf0033ffcc00f0000000000000000000000f876fa5e79b9", "eNrt0TENAAAMw7DyJ92RqHb5MIEkSQMAAAAAAAAAAAAAAAAAAAAAAAAAIx3T1A8//PDDDz/88MMPP/DDD/zwww8//PDDDz/88AM//MAPAAAAAAAAAAAAAAD4dvpeebk=" }, +}; + +static void verifyEncodingCase(const FFBase64EncodingCase* testCase, int lineNo) { + static uint8_t source[512]; + const uint32_t sourceLength = fromHex(testCase->hex, source); + VERIFY(sourceLength <= sizeof(source)); + + static char encoded[1024]; + uint32_t encodedLength = 0; + memset(encoded, 0, sizeof(encoded)); + ffBase64EncodeRaw(sourceLength, (const char*) source, &encodedLength, encoded); + + if (!ffStrEquals(encoded, testCase->expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, testCase->name, testCase->expected, encoded); + exit(1); + } + + // The strbuf wrapper has to agree with the raw function + FF_STRBUF_AUTO_DESTROY input = ffStrbufCreateNS(sourceLength, (const char*) source); + FF_STRBUF_AUTO_DESTROY wrapped = ffBase64EncodeStrbuf(&input); + if (!ffStrbufEqualS(&wrapped, testCase->expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: strbuf wrapper got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, testCase->name, wrapped.chars); + exit(1); + } +} + +// Covers every length, so a break in any of the three code paths (whole groups, one byte left +// over, two bytes left over) shows up at the length that reaches it. The pattern puts the high bit +// on roughly half of the bytes, which is what the partial groups are sensitive to. +static void verifyRoundTrip(void) { + static uint8_t source[512]; + static char encoded[1024]; + static char decoded[512]; + + for (uint32_t length = 0; length <= 400; ++length) { + for (uint32_t i = 0; i < length; ++i) { + source[i] = (uint8_t) (i * 37 + 0x80); + } + + uint32_t encodedLength = 0; + memset(encoded, 0, sizeof(encoded)); + ffBase64EncodeRaw(length, (const char*) source, &encodedLength, encoded); + VERIFY(encodedLength == (length + 2) / 3 * 4); + + uint32_t decodedLength = 0; + memset(decoded, 0, sizeof(decoded)); + VERIFY(ffBase64DecodeRaw(encodedLength, encoded, &decodedLength, decoded)); + VERIFY(decodedLength == length); + VERIFY(memcmp(decoded, source, length) == 0); + } +} + +int main(void) { + for (size_t i = 0; i < sizeof(ENCODING_CASES) / sizeof(ENCODING_CASES[0]); ++i) { + verifyEncodingCase(&ENCODING_CASES[i], __LINE__); + } + + verifyRoundTrip(); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} From 9bac2731651b62d9d23228d06aa4b04830999147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Thu, 17 Sep 2026 00:30:40 +0800 Subject: [PATCH 52/76] NetIO / DiskIO: reduces `waitTime` & improves accurency --- CHANGELOG.md | 7 +++++-- doc/json_schema.json | 4 ++-- presets/all.jsonc | 4 ++-- presets/ci.jsonc | 4 ++-- src/common/impl/commandoption.c | 37 ++++++++++++++++++++------------- src/detection/diskio/diskio.c | 23 ++++++++++---------- src/detection/netio/netio.c | 23 ++++++++++---------- src/detection/top/top.c | 18 ++++++---------- src/modules/diskio/diskio.c | 2 +- src/modules/netio/netio.c | 2 +- 10 files changed, 64 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4816265d6..3416a5a862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,16 @@ Changes: * ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo, Windows / macOS) - * Image logos on Windows and macOS no longer depend on ImageMagick being installed. + * Image logos on Windows and macOS no longer depend on ImageMagick being installed. * Image sources that only ImageMagick could decode, such as SVG, PDF and PostScript, are no longer supported on Windows: neither WIC nor the embedded sixel encoder can decode them. * The `--logo-recache` option has been replaced by `--logo-cache `, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo) * `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss. * `--logo-cache false` ignores the image logo cache completely: nothing is read from it and nothing is written to it. * `--logo-cache regen` does what `--logo-recache true` used to do. * `logo.cache` accepts a boolean, or the string `"regen"`. +* The `waitTime` option of `DiskIO` and `NetIO` now defaults to `250` ms instead of `500`. (DiskIO / NetIO) + * The byte counters are maintained by the kernel as I/O happens, so a shorter sampling window still yields an accurate rate, and both modules now finish about 250 ms sooner. + * Note that the two modules wait concurrently, so enabling both does not cost twice the wait time. Features: * Improved image logo support @@ -37,7 +40,7 @@ Features: * Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS) * The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is tried last. * Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD) -* Modules that were selected on the command line via `--structure` / `-s` now honors module options configured in the JSON config. (CommandOption) +* Modules that were selected on the command line via `--structure` / `-s` now honor module options configured in the JSON config. (CommandOption) Bugfixes: * Fixed Base64 encoding producing wrong output for some inputs. (General) diff --git a/doc/json_schema.json b/doc/json_schema.json index aa472e6afd..d64c5796bb 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -2710,7 +2710,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": { @@ -3611,7 +3611,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": { diff --git a/presets/all.jsonc b/presets/all.jsonc index 73901ca30b..8b5f2d4d65 100644 --- a/presets/all.jsonc +++ b/presets/all.jsonc @@ -99,7 +99,7 @@ "gamepad", "mouse", "keyboard", - "top", + "tpm", "netio", "diskio", { @@ -110,7 +110,7 @@ "type": "weather", "timeout": 1000 }, - "tpm", + "top", "version", "break", "colors" diff --git a/presets/ci.jsonc b/presets/ci.jsonc index c18994df06..96e948f889 100644 --- a/presets/ci.jsonc +++ b/presets/ci.jsonc @@ -101,7 +101,7 @@ "gamepad", "mouse", "keyboard", - "top", + "tpm", "netio", "diskio", { @@ -112,7 +112,7 @@ "type": "weather", "timeout": 1000 }, - "tpm", + "top", "version", "logo", "break", diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c index 0b1bac8dc6..2b11078b39 100644 --- a/src/common/impl/commandoption.c +++ b/src/common/impl/commandoption.c @@ -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; @@ -74,7 +89,7 @@ void ffPrepareCommandOption(FFdata* data) { case 'C': case 'c': FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name) - ffPrepareCPUUsage(); + ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved break; #endif @@ -83,7 +98,7 @@ void ffPrepareCommandOption(FFdata* data) { case 'd': FF_IF_MODULE_MATCH(ffDiskIOModuleInfo.name) { [[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options; - ffInitDiskIOOptions(&options); + initStructureModuleOptions(data, &ffDiskIOModuleInfo, &options); ffPrepareDiskIO(&options); } break; @@ -94,7 +109,7 @@ void ffPrepareCommandOption(FFdata* data) { case 'n': FF_IF_MODULE_MATCH(ffNetIOModuleInfo.name) { [[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options; - ffInitNetIOOptions(&options); + initStructureModuleOptions(data, &ffNetIOModuleInfo, &options); ffPrepareNetIO(&options); } break; @@ -105,7 +120,7 @@ void ffPrepareCommandOption(FFdata* data) { case 'p': FF_IF_MODULE_MATCH(ffPublicIPModuleInfo.name) { [[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options; - ffInitPublicIpOptions(&options); + initStructureModuleOptions(data, &ffPublicIPModuleInfo, &options); ffPreparePublicIp(&options); } break; @@ -116,7 +131,7 @@ void ffPrepareCommandOption(FFdata* data) { case 't': FF_IF_MODULE_MATCH(ffTopModuleInfo.name) { [[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options; - ffInitTopOptions(&options); + initStructureModuleOptions(data, &ffTopModuleInfo, &options); ffPrepareTopProcesses(options.showTypes); } break; @@ -127,7 +142,7 @@ void ffPrepareCommandOption(FFdata* data) { case 'w': FF_IF_MODULE_MATCH(ffWeatherModuleInfo.name) { [[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options; - ffInitWeatherOptions(&options); + initStructureModuleOptions(data, &ffWeatherModuleInfo, &options); ffPrepareWeather(&options); } break; @@ -215,17 +230,11 @@ 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 { - yyjson_val* configModule = findStructureModuleConfig(data, baseInfo->name); - if (configModule != nullptr) { - baseInfo->parseJsonObject(optionBuf, configModule); - baseInfo->printModule(optionBuf); - } else { - baseInfo->printModule(optionBuf); - } + baseInfo->printModule(optionBuf); } baseInfo->destroyOptions(optionBuf); return true; diff --git a/src/detection/diskio/diskio.c b/src/detection/diskio/diskio.c index a2f53439c1..d68570b78a 100644 --- a/src/detection/diskio/diskio.c +++ b/src/detection/diskio/diskio.c @@ -5,7 +5,7 @@ const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options); static FFlist ioCounters1; -static uint64_t time1; +static double time1; void ffPrepareDiskIO(FFDiskIOOptions* options) { if (options->detectTotal) { @@ -16,9 +16,13 @@ void ffPrepareDiskIO(FFDiskIOOptions* options) { return; // Already prepared } + // The options cannot change between this call and `ffDetectDiskIO`: `ffPrepareCommandOption` + // and `parseStructureCommand` both build them through `initStructureModuleOptions`, which + // merges the module object from the JSON config. So the baseline always matches the second + // snapshot and needs no re-validation. ffListInit(&ioCounters1); ffDiskIOGetIoCounters(&ioCounters1, options); - time1 = ffTimeGetNow(); + time1 = ffTimeGetTick(); } const char* ffDetectDiskIO(FFlist* result, FFDiskIOOptions* options) { @@ -33,22 +37,17 @@ const char* ffDetectDiskIO(FFlist* result, FFDiskIOOptions* options) { } if (time1 == 0) { - ffListInit(&ioCounters1); - error = ffDiskIOGetIoCounters(&ioCounters1, options); - if (error) { - return error; - } - time1 = ffTimeGetNow(); + ffPrepareDiskIO(options); } if (ioCounters1.length == 0) { return "No physical disk found"; } - uint64_t time2 = ffTimeGetNow(); - while (time2 - time1 < options->waitTime) { + double time2 = ffTimeGetTick(); + while (time2 - time1 < (double) options->waitTime) { ffTimeSleep((uint32_t) (options->waitTime - (time2 - time1))); - time2 = ffTimeGetNow(); + time2 = ffTimeGetTick(); } error = ffDiskIOGetIoCounters(result, options); @@ -72,7 +71,7 @@ const char* ffDetectDiskIO(FFlist* result, FFDiskIOOptions* options) { uint64_t* prevValue = (uint64_t*) ((uint8_t*) icPrev + off); uint64_t* currValue = (uint64_t*) ((uint8_t*) icCurr + off); uint64_t temp = *currValue; - *currValue = (*currValue - *prevValue) * 1000 / (time2 - time1); // Calculate per second + *currValue = (uint64_t) ((double) (*currValue - *prevValue) * 1000.0 / (time2 - time1)); // Calculate per second // For next function call *prevValue = temp; diff --git a/src/detection/netio/netio.c b/src/detection/netio/netio.c index 3c986a8631..6ebfe14051 100644 --- a/src/detection/netio/netio.c +++ b/src/detection/netio/netio.c @@ -3,7 +3,7 @@ #include "common/time.h" static FFlist ioCounters1; -static uint64_t time1; +static double time1; void ffPrepareNetIO(FFNetIOOptions* options) { if (options->detectTotal) { @@ -14,9 +14,13 @@ void ffPrepareNetIO(FFNetIOOptions* options) { return; // Already prepared } + // The options cannot change between this call and `ffDetectNetIO`: `ffPrepareCommandOption` + // and `parseStructureCommand` both build them through `initStructureModuleOptions`, which + // merges the module object from the JSON config. So the baseline always matches the second + // snapshot and needs no re-validation. ffListInit(&ioCounters1); ffNetIOGetIoCounters(&ioCounters1, options); - time1 = ffTimeGetNow(); + time1 = ffTimeGetTick(); } const char* ffDetectNetIO(FFlist* result, FFNetIOOptions* options) { @@ -31,22 +35,17 @@ const char* ffDetectNetIO(FFlist* result, FFNetIOOptions* options) { } if (time1 == 0) { - ffListInit(&ioCounters1); - error = ffNetIOGetIoCounters(&ioCounters1, options); - if (error) { - return error; - } - time1 = ffTimeGetNow(); + ffPrepareNetIO(options); } if (ioCounters1.length == 0) { return "No network interfaces found"; } - uint64_t time2 = ffTimeGetNow(); - while (time2 - time1 < options->waitTime) { + double time2 = ffTimeGetTick(); + while (time2 - time1 < (double) options->waitTime) { ffTimeSleep((uint32_t) (options->waitTime - (time2 - time1))); - time2 = ffTimeGetNow(); + time2 = ffTimeGetTick(); } error = ffNetIOGetIoCounters(result, options); @@ -70,7 +69,7 @@ const char* ffDetectNetIO(FFlist* result, FFNetIOOptions* options) { uint64_t* prevValue = (uint64_t*) ((uint8_t*) icPrev + off); uint64_t* currValue = (uint64_t*) ((uint8_t*) icCurr + off); uint64_t temp = *currValue; - *currValue = (*currValue - *prevValue) * 1000 / (time2 - time1); // Calculate per second + *currValue = (uint64_t) ((double) (*currValue - *prevValue) * 1000.0 / (time2 - time1)); // Calculate per second *prevValue = temp; } } diff --git a/src/detection/top/top.c b/src/detection/top/top.c index dd4442a7ee..8b50991ecb 100644 --- a/src/detection/top/top.c +++ b/src/detection/top/top.c @@ -4,28 +4,22 @@ static FFlist first; static double startTick; -static FFTopTypes preparedShowTypes = FF_TOP_TYPE_CPU | FF_TOP_TYPE_MEMORY | FF_TOP_TYPE_DISK; void ffPrepareTopProcesses(FFTopTypes showTypes) { if ((showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0) { return; // Memory usage is instantaneous; no baseline snapshot is needed } - if (startTick != 0 && preparedShowTypes == showTypes) { - return; // Already prepared - } - if (startTick != 0) { - // The set of requested types changed; discard the stale baseline - FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, first) { - ffStrbufDestroy(&item->name); - } - ffListDestroy(&first); + return; // Already prepared } + // `showTypes` cannot change between this call and `ffDetectTopProcesses`: `ffPrepareCommandOption` + // and `parseStructureCommand` both build the options through `initStructureModuleOptions`, which + // merges the module object from the JSON config. So the baseline always matches what the second + // snapshot collects and needs no re-validation. ffListInit(&first); startTick = ffTimeGetTick(); - preparedShowTypes = showTypes; ffTopGetProcessSnapshot(&first, showTypes); } @@ -102,7 +96,7 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { ffStrbufInitMove(&item->name, &snap->name); } } else { - if (startTick == 0 || preparedShowTypes != options->showTypes) { + if (startTick == 0) { ffPrepareTopProcesses(options->showTypes); } diff --git a/src/modules/diskio/diskio.c b/src/modules/diskio/diskio.c index fac2d64594..202772606f 100644 --- a/src/modules/diskio/diskio.c +++ b/src/modules/diskio/diskio.c @@ -165,7 +165,7 @@ void ffInitDiskIOOptions(FFDiskIOOptions* options) { ffStrbufInit(&options->namePrefix); options->detectTotal = false; - options->waitTime = 500; + options->waitTime = 250; } void ffDestroyDiskIOOptions(FFDiskIOOptions* options) { diff --git a/src/modules/netio/netio.c b/src/modules/netio/netio.c index ef3da5d6a2..55218ce4d5 100644 --- a/src/modules/netio/netio.c +++ b/src/modules/netio/netio.c @@ -192,7 +192,7 @@ void ffInitNetIOOptions(FFNetIOOptions* options) { #endif ; options->detectTotal = false; - options->waitTime = 500; + options->waitTime = 250; } void ffDestroyNetIOOptions(FFNetIOOptions* options) { From e004c97ee02ca4ae8937677b628216b567d4e4a3 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 17 Sep 2026 13:50:07 +0800 Subject: [PATCH 53/76] CMake: adds option `-DENABLE_IMAGE_LOGO=` --- .github/workflows/build-no-features-test.yml | 2 +- CHANGELOG.md | 12 +++- CMakeLists.txt | 41 ++++++++++--- src/detection/gpu/gpu_apple.m | 4 +- src/logo/logo.c | 26 ++++++-- src/logo/logo.h | 2 + src/options/logo.c | 64 +++++++++++++++++--- 7 files changed, 118 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-no-features-test.yml b/.github/workflows/build-no-features-test.yml index fadefe8b9d..28e2c483c8 100644 --- a/.github/workflows/build-no-features-test.yml +++ b/.github/workflows/build-no-features-test.yml @@ -21,7 +21,7 @@ jobs: run: uname -a - name: configure project - run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_PULSE=OFF -DENABLE_DDCUTIL=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF + run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_PULSE=OFF -DENABLE_DDCUTIL=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF -DENABLE_IMAGE_LOGO=OFF - name: build project run: cmake --build . --target package --verbose -j4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3416a5a862..272cf22109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Unreleased Changes: -* ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo, Windows / macOS) - * Image logos on Windows and macOS no longer depend on ImageMagick being installed. - * Image sources that only ImageMagick could decode, such as SVG, PDF and PostScript, are no longer supported on Windows: neither WIC nor the embedded sixel encoder can decode them. +* ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo) +* ImageMagick 6 support is deprecated. It's only kept for old Debian & Ubuntu releases which have no ImageMagick 7 available. + * Intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible. + * The `--logo-recache` option has been replaced by `--logo-cache `, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo) * `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss. * `--logo-cache false` ignores the image logo cache completely: nothing is read from it and nothing is written to it. @@ -29,6 +30,11 @@ Features: * The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single frame GIF falls back to a still image. A build with none of those, or with ImageMagick 6, reports an error instead of quietly showing a still image. * A terminal that supports the kitty graphics protocol but not its animation frames, as Konsole does not, shows the first frame. + * Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo) + * Image logos are the only consumer of ImageMagick, of chafa and of the embedded libsixel encoder, so none of the three is looked for at configure time, and no image decoding source is compiled in. + * The `sixel`, `kitty`, `kitty-direct`, `kitty-icat`, `iterm` and `chafa` logo types are rejected with an error, on the command line and in the JSON config alike, and the `auto` logo type never tries an image. + * `--logo-type raw` keeps working: it writes a pre-rendered byte stream through unchanged and needs no decoder, so a logo can still be displayed by converting the image externally. + * This is intended to be used to reduce binary size on embedded systems (such as OpenWrt) only. * Added CPU name and frequency detection support on SPARC. (CPU, Linux) * Added package detection support for CRUX. (Packages, Linux) * Exposed in custom format as `{crux}`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 54fa295c53..88a7ef7842 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,10 +88,6 @@ 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 ANDROID OR SunOS OR Haiku OR GNU" OFF) -cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "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 and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF) -cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" 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) @@ -101,6 +97,14 @@ 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) +if(ENABLE_IMAGE_LOGO) + cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF) + cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "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 and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF) + cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" OFF) +endif() + 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) @@ -501,10 +505,6 @@ 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/image/sixel.c src/logo/logo.c src/modules/modules.c src/options/display.c @@ -512,6 +512,15 @@ set(LIBFASTFETCH_SRC 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 + ) +endif() + foreach(FF_MODULE_DIR ${FF_MODULE_DIRS}) list(APPEND LIBFASTFETCH_SRC src/modules/${FF_MODULE_DIR}/${FF_MODULE_DIR}.c @@ -1023,8 +1032,12 @@ elseif(APPLE) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_apple.c src/detection/camera/camera_apple.m - src/logo/image/imageio.c ) + 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 @@ -1117,8 +1130,12 @@ elseif(WIN32) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_windows.c src/detection/camera/camera_windows.cpp - src/logo/image/wic.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 @@ -1846,6 +1863,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() diff --git a/src/detection/gpu/gpu_apple.m b/src/detection/gpu/gpu_apple.m index 9596dbe76d..d00410c8a8 100644 --- a/src/detection/gpu/gpu_apple.m +++ b/src/detection/gpu/gpu_apple.m @@ -46,6 +46,7 @@ continue; } +#pragma clang diagnostic ignored "-Wdeprecated-declarations" #ifndef MAC_OS_X_VERSION_10_15 if ([device supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily2_v1]) { ffStrbufSetStatic(&gpu->platformApi, "Metal Feature Set 2"); @@ -53,14 +54,11 @@ ffStrbufSetStatic(&gpu->platformApi, "Metal Feature Set 1"); } #else // MAC_OS_X_VERSION_10_15 - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wunguarded-availability-new" if ([device supportsFamily:MTLGPUFamilyMetal4]) { ffStrbufSetStatic(&gpu->platformApi, "Metal 4"); } else if ([device supportsFamily:MTLGPUFamilyMetal3]) { ffStrbufSetStatic(&gpu->platformApi, "Metal 3"); } - #pragma clang diagnostic pop else if ([device supportsFamily:MTLGPUFamilyCommon3]) { ffStrbufSetStatic(&gpu->platformApi, "Metal Common 3"); } else if ([device supportsFamily:MTLGPUFamilyCommon2]) { diff --git a/src/logo/logo.c b/src/logo/logo.c index 12af2251ac..e0ba24cd2d 100644 --- a/src/logo/logo.c +++ b/src/logo/logo.c @@ -518,6 +518,7 @@ static bool logoPrintFileIfExists(bool doColorReplacement, bool raw) { return true; } +#if FF_HAVE_IMAGE_LOGO static bool logoPrintImageIfExists(FFLogoType logo, bool printError) { if (!ffLogoPrintImageIfExists(logo, printError)) { return false; @@ -526,6 +527,7 @@ static bool logoPrintImageIfExists(FFLogoType logo, bool printError) { logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false); return true; } +#endif static bool logoTryKnownType(void) { FFOptionsLogo* options = &instance.config.logo; @@ -593,7 +595,16 @@ static bool logoTryKnownType(void) { return logoPrintFileIfExists(false, true); } +#if FF_HAVE_IMAGE_LOGO return logoPrintImageIfExists(options->type, instance.config.display.showErrors); +#else + // No image logo type can be selected in this build (see ffOptionsParseLogoCommandLine), and + // FF_LOGO_TYPE_IMAGE_RAW was handled above, so nothing printable is left for this source. + if (instance.config.display.showErrors) { + fprintf(stderr, "Logo: Unsupported logo type for source: %s\n", options->source.chars); + } + return false; +#endif } void ffLogoPrint(void) { @@ -638,8 +649,10 @@ void ffLogoPrint(void) { } } +#if FF_HAVE_IMAGE_LOGO + // Try to load the logo as an image. if (!ffStrbufEndsWithIgnCaseS(&options->source, ".txt")) { -#if !FF_MODULE_DISABLE_TERMINAL + #if !FF_MODULE_DISABLE_TERMINAL const FFTerminalResult* terminal = ffDetectTerminal(); bool supportsIterm2 = ffStrbufEqualS(&terminal->prettyName, "iTerm"); @@ -655,21 +668,22 @@ void ffLogoPrint(void) { ffStrbufIgnCaseEqualS(&terminal->processName, "wezterm") || ffStrbufIgnCaseEqualS(&terminal->processName, "wayst") || ffStrbufIgnCaseEqualS(&terminal->processName, "ghostty") || - #ifdef __APPLE__ + #ifdef __APPLE__ ffStrbufIgnCaseEqualS(&terminal->processName, "WarpTerminal") || - #else + #else ffStrbufIgnCaseEqualS(&terminal->processName, "warp") || - #endif + #endif false; -#else + #else bool supportsKitty = false; -#endif + #endif // Try to load the logo as an image. If it succeeds, print it and return. if (logoPrintImageIfExists(supportsKitty ? FF_LOGO_TYPE_IMAGE_KITTY : FF_LOGO_TYPE_IMAGE_CHAFA, false)) { return; } } +#endif // Try to load the logo as a file. If it succeeds, print it and return. if (logoPrintFileIfExists(true, false)) { diff --git a/src/logo/logo.h b/src/logo/logo.h index 324b590caa..a1cf36ec99 100644 --- a/src/logo/logo.h +++ b/src/logo/logo.h @@ -39,5 +39,7 @@ const FFlogo* ffLogoGetBuiltinDetected(FFLogoSize size); extern const FFlogo* ffLogoBuiltins[]; extern const FFlogo ffLogoUnknown; +#if FF_HAVE_IMAGE_LOGO // image/image.c bool ffLogoPrintImageIfExists(FFLogoType type, bool printError); +#endif diff --git a/src/options/logo.c b/src/options/logo.c index dc64cc607b..f4045569c2 100644 --- a/src/options/logo.c +++ b/src/options/logo.c @@ -3,6 +3,42 @@ #include "common/jsonconfig.h" #include "common/strutil.h" +#if !FF_HAVE_IMAGE_LOGO +// The image logo types are still accepted by the parsers below in a build that can not render them, +// so that the user is told which type is unavailable instead of being told the type does not exist. +// The one that is deliberately left out is `raw`: it writes a pre-rendered byte stream through +// untouched and needs no decoder, so a build without image logos can still display one. +static bool logoTypeIsImage(FFLogoType type) { + switch (type) { + case FF_LOGO_TYPE_IMAGE_SIXEL: + case FF_LOGO_TYPE_IMAGE_KITTY: + case FF_LOGO_TYPE_IMAGE_KITTY_DIRECT: + case FF_LOGO_TYPE_IMAGE_KITTY_ICAT: + case FF_LOGO_TYPE_IMAGE_ITERM: + case FF_LOGO_TYPE_IMAGE_CHAFA: + return true; + default: + return false; + } +} +#endif + +// --sixel, --kitty, --kitty-direct, --kitty-icat and --iterm. They keep a branch of their own in +// every build so that the error a build without image logos gives names the flag that was used, +// rather than it being rejected as an unknown option. --chafa is not among them: it is gated on +// FF_HAVE_CHAFA, which a build without image logos turns off as well, and it keeps the message it +// has always given in that case. +static void logoParseImageFlag(FFOptionsLogo* options, const char* key, const char* value, FFLogoType type) { +#if FF_HAVE_IMAGE_LOGO + ffOptionParseString(key, value, &options->source); + options->type = type; +#else + FF_UNUSED(options, key, value, type); + fputs("Error: Fastfetch was built without image logo support\n", stderr); + exit(477); +#endif +} + void ffOptionsInitLogo(FFOptionsLogo* options) { ffStrbufInit(&options->source); options->type = FF_LOGO_TYPE_AUTO; @@ -71,6 +107,12 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons { "none", FF_LOGO_TYPE_NONE }, {}, }); +#if !FF_HAVE_IMAGE_LOGO + if (logoTypeIsImage(options->type)) { + fputs("Error: Fastfetch was built without image logo support\n", stderr); + exit(477); + } +#endif } else if (ffStrStartsWithIgnCase(subKey, "color-") && subKey[6] != '\0' && subKey[7] == '\0') // matches "--logo-color-*" { // Map the number to an array index, so that '1' -> 0, '2' -> 1, etc. @@ -151,20 +193,15 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons return false; } } else if (ffStrEqualsIgnCase(key, "--sixel")) { - ffOptionParseString(key, value, &options->source); - options->type = FF_LOGO_TYPE_IMAGE_SIXEL; + logoParseImageFlag(options, key, value, FF_LOGO_TYPE_IMAGE_SIXEL); } else if (ffStrEqualsIgnCase(key, "--kitty")) { - ffOptionParseString(key, value, &options->source); - options->type = FF_LOGO_TYPE_IMAGE_KITTY; + logoParseImageFlag(options, key, value, FF_LOGO_TYPE_IMAGE_KITTY); } else if (ffStrEqualsIgnCase(key, "--kitty-direct")) { - ffOptionParseString(key, value, &options->source); - options->type = FF_LOGO_TYPE_IMAGE_KITTY_DIRECT; + logoParseImageFlag(options, key, value, FF_LOGO_TYPE_IMAGE_KITTY_DIRECT); } else if (ffStrEqualsIgnCase(key, "--kitty-icat")) { - ffOptionParseString(key, value, &options->source); - options->type = FF_LOGO_TYPE_IMAGE_KITTY_ICAT; + logoParseImageFlag(options, key, value, FF_LOGO_TYPE_IMAGE_KITTY_ICAT); } else if (ffStrEqualsIgnCase(key, "--iterm")) { - ffOptionParseString(key, value, &options->source); - options->type = FF_LOGO_TYPE_IMAGE_ITERM; + logoParseImageFlag(options, key, value, FF_LOGO_TYPE_IMAGE_ITERM); } else if (ffStrEqualsIgnCase(key, "--raw")) { ffOptionParseString(key, value, &options->source); options->type = FF_LOGO_TYPE_IMAGE_RAW; @@ -278,6 +315,11 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo if (error) { return error; } +#if !FF_HAVE_IMAGE_LOGO + if (logoTypeIsImage((FFLogoType) value)) { + return "Image logo types are not supported because Fastfetch was built without image logo support"; + } +#endif options->type = (FFLogoType) value; continue; } else if (unsafe_yyjson_equals_str(key, "source")) { @@ -482,6 +524,7 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { case FF_LOGO_TYPE_COMMAND_RAW: yyjson_mut_obj_add_str(doc, obj, "type", "command-raw"); break; +#if FF_HAVE_IMAGE_LOGO case FF_LOGO_TYPE_IMAGE_SIXEL: yyjson_mut_obj_add_str(doc, obj, "type", "sixel"); break; @@ -500,6 +543,7 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { case FF_LOGO_TYPE_IMAGE_CHAFA: yyjson_mut_obj_add_str(doc, obj, "type", "chafa"); break; +#endif case FF_LOGO_TYPE_IMAGE_RAW: yyjson_mut_obj_add_str(doc, obj, "type", "raw"); break; From 59e33150dd6863f12b1b41b92e722039b16ddde1 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 17 Sep 2026 14:01:13 +0800 Subject: [PATCH 54/76] CPU (Android): updates the marketing name of Dimensity 9600 Pro --- src/detection/cpu/cpu_linux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detection/cpu/cpu_linux.c b/src/detection/cpu/cpu_linux.c index 152827294a..3cfac542db 100644 --- a/src/detection/cpu/cpu_linux.c +++ b/src/detection/cpu/cpu_linux.c @@ -321,7 +321,7 @@ static void detectMediaTek(FFCPUResult* cpu) { switch (code) // The SOC code of MTK Dimensity series is full of mess { case 6995: - name = "9600"; + name = "9600 Pro"; break; case 6993: name = "9500"; From 1b7cc1cd306c79b3ad44cdb0e2c9d0c4e7544173 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 17 Sep 2026 14:02:29 +0800 Subject: [PATCH 55/76] CPU (Linux): adds SoC name of MacBook Neo --- src/detection/cpu/cpu.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/detection/cpu/cpu.c b/src/detection/cpu/cpu.c index 75db6f92cb..896bbbc405 100644 --- a/src/detection/cpu/cpu.c +++ b/src/detection/cpu/cpu.c @@ -31,7 +31,7 @@ const char* ffDetectCPU(const FFCPUOptions* options, FFCPUResult* cpu) { } const char* ffCPUAppleCodeToName(uint32_t code) { - // https://github.com/AsahiLinux/docs/wiki/Codenames + // https://github.com/AsahiLinux/docs/blob/main/docs/hw/soc/soc-codenames.md switch (code) { case 8103: return "Apple M1"; @@ -62,6 +62,8 @@ const char* ffCPUAppleCodeToName(uint32_t code) { return "Apple M4 Pro"; case 6041: return "Apple M4 Max"; + case 8140: + return "Apple A18 Pro"; case 8142: return "Apple M5"; case 6050: From 43988a8d4196890f0dfa22a7012fbbf0b8887f6d Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 17 Sep 2026 14:37:17 +0800 Subject: [PATCH 56/76] Release: v2.69.0 --- CHANGELOG.md | 31 +++++++++++++++++-------------- CMakeLists.txt | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 272cf22109..6ed7523a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -# Unreleased +# 2.69.0 Changes: * ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo) -* ImageMagick 6 support is deprecated. It's only kept for old Debian & Ubuntu releases which have no ImageMagick 7 available. - * Intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible. +* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available. + * It is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible. * The `--logo-recache` option has been replaced by `--logo-cache `, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo) * `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss. @@ -24,17 +24,17 @@ Features: * Image logo cache entries are now validated against the modification time of the source image. (Logo) * Editing an image logo in place now invalidates its cached rendering. * Cache entries written by older versions are not reused, as they carry no modification time. - * Image logos can now be animated, when the terminal and the image protocol support it. (Logo) + * Image logos can now be animated when the terminal and the image protocol support it. (Logo) * `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved. * `--logo-animation-frame ` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option. * The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. - * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single frame GIF falls back to a still image. A build with none of those, or with ImageMagick 6, reports an error instead of quietly showing a still image. - * A terminal that supports the kitty graphics protocol but not its animation frames, as Konsole does not, shows the first frame. + * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image. + * A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame. * Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo) - * Image logos are the only consumer of ImageMagick, of chafa and of the embedded libsixel encoder, so none of the three is looked for at configure time, and no image decoding source is compiled in. - * The `sixel`, `kitty`, `kitty-direct`, `kitty-icat`, `iterm` and `chafa` logo types are rejected with an error, on the command line and in the JSON config alike, and the `auto` logo type never tries an image. - * `--logo-type raw` keeps working: it writes a pre-rendered byte stream through unchanged and needs no decoder, so a logo can still be displayed by converting the image externally. - * This is intended to be used to reduce binary size on embedded systems (such as OpenWrt) only. + * Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in. + * The `sixel`, `kitty`, `kitty-direct`, `kitty-icat`, `iterm` and `chafa` logo types are rejected with an error, both on the command line and in the JSON config, and the `auto` logo type never tries an image. + * `--logo-type raw` keeps working: it passes a pre-rendered byte stream through unchanged and needs no decoder, so a logo can still be displayed by converting the image externally. + * This is intended only to reduce binary size on embedded systems (such as OpenWrt). * Added CPU name and frequency detection support on SPARC. (CPU, Linux) * Added package detection support for CRUX. (Packages, Linux) * Exposed in custom format as `{crux}`. @@ -44,17 +44,20 @@ Features: * Improved Packages detection on Windows (Packages, Windows) * `winget list` is now invoked with `--source winget`, so only packages installed by winget itself are counted, and the slow msstore HTTP round trips are skipped. * Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS) - * The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is tried last. + * The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is used only as a last resort. * Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD) * Modules that were selected on the command line via `--structure` / `-s` now honor module options configured in the JSON config. (CommandOption) +* Improved reliability of fastfetch's built-in HTTP client. (PublicIP, Weather) + * It now supports custom ports and can properly handle chunked transfer encoding. + * It is designed for minimal resource usage and fast performance. It does not support full HTTP features like HTTPS. Users can always use the `Command` module with `curl` to achieve similar functionality. Bugfixes: -* Fixed Base64 encoding producing wrong output for some inputs. (General) -* Fixed image logos not working when ImageMagick is built without a quantum depth suffix in its library name, as on FreeBSD. (Logo, FreeBSD) +* Fixed Base64 encoding producing incorrect output for some inputs. (General) +* Fixed image logos not working when ImageMagick is built without a quantum depth suffix in its library name, as is the case on FreeBSD. (Logo, FreeBSD) * Fixed TerminalFont detection on Windows ignoring Windows Terminal JSON fragment files. (#2573, TerminalFont, Windows) * Fixed 64-bit values being truncated by `strtoul` on platforms where `unsigned long` is 32-bit. (Swap / PhysicalDisk / PhysicalMemory / GPU) * Fixed read-only SQLite databases failing with `SQLITE_READONLY` when the database directory is not writable. (Packages) - * This fixes PKG package count detection on FreeBSD + * This fixes PKG package count detection on FreeBSD. * Fixed `{#keys}` and `{#title}` in module format strings not honoring the `brightColor` display option. (Format) * Fixed `paddingTop` and `paddingLeft` being ignored by the `kitty-icat` image logo type. (Logo) * Some internal cleanups and optimizations. diff --git a/CMakeLists.txt b/CMakeLists.txt index 88a7ef7842..9cf4b05cc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23 project(fastfetch - VERSION 2.68.1 + VERSION 2.69.0 LANGUAGES C DESCRIPTION "Fast neofetch-like system information tool" HOMEPAGE_URL "https://github.com/fastfetch-cli/fastfetch" From c23c98a85a4eeb1fa0ab2f05b637823e494abf59 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Thu, 17 Sep 2026 16:39:19 +0800 Subject: [PATCH 57/76] Logo (Image): supports transparent background of sixel logos --- src/3rdparty/sixel/README.md | 99 +++++++++++++++++++++++++++++------- src/3rdparty/sixel/repo.json | 7 ++- src/3rdparty/sixel/tosixel.c | 10 +++- src/logo/image/sixel.c | 81 +++++++++++++++++++++++++++-- 4 files changed, 174 insertions(+), 23 deletions(-) diff --git a/src/3rdparty/sixel/README.md b/src/3rdparty/sixel/README.md index 7550359a95..8b9c85af7a 100644 --- a/src/3rdparty/sixel/README.md +++ b/src/3rdparty/sixel/README.md @@ -1,30 +1,71 @@ # 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 only**. +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 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. +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. -## Why Windows only +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`, and these sources are **not compiled at all** on Linux / macOS / -BSD. +is gated on `WIN32 OR APPLE`, and these sources are **not compiled at all** on +Linux / the BSDs. -Consequence: sixel bytes differ between Windows (libsixel) and the rest +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` / @@ -48,8 +89,8 @@ 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, ImageMagick elsewhere), so libsixel only ever - sees an RGBA8 buffer here. + 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. @@ -59,20 +100,21 @@ Everything else in the upstream tree, in particular: **`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 `config.h` below. + `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 / 255 KB**, down from 319 files / 11.3 MB. Note that 8.0 MB of +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 / 255 KB. +`src/` + `include/` went from 50 files / 1.17 MB to 20 files / 256 KB. ## Local modifications -Two deliberate divergences from upstream. Re-apply both when re-syncing. +Three deliberate divergences from upstream. Re-apply all three when re-syncing. 1. **`sixel.h` — `SIXELAPI` is empty.** Upstream defines it as `__declspec(dllexport)` on Windows. This subset is compiled into `libfastfetch` @@ -92,6 +134,24 @@ Two deliberate divergences from upstream. Re-apply both when re-syncing. `-Werror=implicit-function-declaration`, so a missing `` / `` 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`. + 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 @@ -111,14 +171,17 @@ done cp /tmp/libsixel/include/sixel.h . cp /tmp/libsixel/LICENSE /tmp/libsixel/LICENSE.sixel /tmp/libsixel/LICENSE.pnmcolormap . -# 2. re-apply the two local modifications listed above +# 2. re-apply the three local modifications listed above # 3. verify ``` -Verification (must be 0 diagnostics, and the encoder must emit a DCS-wrapped stream): +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 -CC=/c/msys64/clang64/bin/cc.exe +# 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" diff --git a/src/3rdparty/sixel/repo.json b/src/3rdparty/sixel/repo.json index 481ed232f9..12900c0f72 100644 --- a/src/3rdparty/sixel/repo.json +++ b/src/3rdparty/sixel/repo.json @@ -2,5 +2,10 @@ "home": "https://github.com/saitoha/libsixel", "license": "MIT ( embed in source )", "version": "1.8.7-r2", - "author": "Hayaki Saito" + "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" + ] } diff --git a/src/3rdparty/sixel/tosixel.c b/src/3rdparty/sixel/tosixel.c index f3a23731a9..84fe3eb2c6 100644 --- a/src/3rdparty/sixel/tosixel.c +++ b/src/3rdparty/sixel/tosixel.c @@ -307,7 +307,15 @@ sixel_encode_header(int width, int height, sixel_output_t *output) { SIXELSTATUS status = SIXEL_FALSE; int nwrite; - int p[3] = {0, 0, 0}; + + /* fastfetch: P2 = 1 asks for a transparent background, so a pixel we leave unset keeps + * whatever the terminal already had there. Upstream leaves it at 0, which tells the terminal + * to fill unset pixels with colour-table entry 0 instead -- 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. Measured on Windows Terminal, which implements the VT340 semantics; + * ImageMagick's SIXEL coder emits "0;1;0" here and chafa emits the same. + * The trimming below drops the trailing zero, so the envelope is "0;1" -- P3 defaults to 0. */ + int p[3] = {0, 1, 0}; int pcount = 3; int use_raster_attributes = 1; diff --git a/src/logo/image/sixel.c b/src/logo/image/sixel.c index 3b0c0ae83c..24f400ab69 100644 --- a/src/logo/image/sixel.c +++ b/src/logo/image/sixel.c @@ -2,16 +2,89 @@ #ifdef FF_HAVE_SIXEL - #include // src/3rdparty/sixel/sixel.h + #include + #include + // The transparent path needs two things the public header does not declare: the palette mapping + // -- which is where libsixel applies the Floyd-Steinberg dithering, so doing it by hand would + // band the gradients -- and the palette size the encoder's keycolor handling is built around. + // The vendored copy is pinned to the exact revision this build compiles, so there is no + // version skew to guard against. + #include "dither.h" static int sixelWriteCallback(char* data, int size, void* priv) { ffStrbufAppendNS((FFstrbuf*) priv, (uint32_t) size, data); return 1; // non-zero means "keep going" } +// Only a fully transparent pixel is left out. A partially transparent one can not be represented -- +// a sixel has no way to blend it with whatever the terminal has behind it -- and painting it in its +// unblended colour is closer to the source than dropping it would be. +static bool hasTransparentPixels(const FFImageBuffer* buffer) { + const uint8_t* pixels = buffer->data; + const uint32_t pixelCount = buffer->width * buffer->height; + for (uint32_t i = 0; i < pixelCount; ++i) { + if (pixels[i * 4 + 3] == 0) { + return true; + } + } + + return false; +} + +// Encodes an image with transparent pixels as an indexed image whose transparent pixels carry a +// keycolor: that entry is left out of the palette definition and its pixels are left out of the +// pixel data, so nothing is drawn there and the terminal background shows through. +static SIXELSTATUS sixelEncodeTransparent(sixel_dither_t* dither, const FFImageBuffer* buffer, sixel_output_t* output) { + const int width = (int) buffer->width; + const int height = (int) buffer->height; + + // Map the pixels onto the palette through libsixel's own lookup, then hand the result back as + // an indexed image. This is the same mapping the opaque path gets from sixel_encode. + sixel_index_t* indices = sixel_dither_apply_palette(dither, buffer->data, width, height); + if (indices == nullptr) { + return SIXEL_RUNTIME_ERROR; + } + + // The keycolor is the first index past the quantized palette, so it is never a colour that a + // pixel was mapped to. Asking for one colour less than the maximum is what guarantees the + // index exists. + const int keycolor = sixel_dither_get_num_of_palette_colors(dither); + const uint8_t* pixels = buffer->data; + for (uint32_t i = 0; i < (uint32_t) width * (uint32_t) height; ++i) { + if (pixels[i * 4 + 3] == 0) { + indices[i] = (sixel_index_t) keycolor; + } + } + + // The encoder sizes its per-colour map from ncolors and skips the keycolor inside that range, + // so the keycolor has to be counted in it. Its own palette entry is never read: leaving the + // definition out is exactly what the keycolor means. + int ncolors = keycolor + 1; + if (ncolors < 3) { + // Two colours mean "the terminal's default black and white palette, no definitions needed" + // to the encoder, which would be wrong here: one of the two is the keycolor, not a colour. + // The padding entry is written out so no uninitialised palette byte reaches the output. + ncolors = 3; + memset(dither->palette + keycolor * 3, 0, (size_t) (ncolors - keycolor) * 3); + } + dither->ncolors = ncolors; + + sixel_dither_set_transparent(dither, keycolor); + // PAL8 hands the indices over as they are, instead of mapping the pixels a second time + sixel_dither_set_pixelformat(dither, SIXEL_PIXELFORMAT_PAL8); + + SIXELSTATUS status = sixel_encode(indices, width, height, 1, dither, output); + + sixel_allocator_free(dither->allocator, indices); + return status; +} + bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error) { + // A source without fully transparent pixels takes the opaque path, unchanged + const bool transparent = hasTransparentPixels(buffer); + sixel_dither_t* dither = nullptr; - if (sixel_dither_new(&dither, 256, nullptr) != SIXEL_OK) { + if (sixel_dither_new(&dither, transparent ? SIXEL_PALETTE_MAX - 1 : SIXEL_PALETTE_MAX, nullptr) != SIXEL_OK) { if (error) { *error = "sixel_dither_new() failed"; } @@ -37,7 +110,9 @@ bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** e } // The depth parameter is unused by libsixel; the DCS envelope is emitted by default - SIXELSTATUS status = sixel_encode(buffer->data, (int) buffer->width, (int) buffer->height, 4, dither, output); + SIXELSTATUS status = transparent + ? sixelEncodeTransparent(dither, buffer, output) + : sixel_encode(buffer->data, (int) buffer->width, (int) buffer->height, 4, dither, output); sixel_output_unref(output); sixel_dither_unref(dither); From f8fd708423094979e4b242a061aecd1f46d2f87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Thu, 17 Sep 2026 19:19:25 +0800 Subject: [PATCH 58/76] Global: addresses Copilot's review comments --- doc/json_schema.json | 5 ++++ src/common/endian.h | 7 ++++- src/common/impl/FFPlatform_unix.c | 36 +++++++++++++++++++---- src/detection/top/top_apple.c | 14 +++++++-- src/detection/wallpaper/wallpaper_apple.m | 5 ++++ src/logo/image/im6.c | 23 +++++++++++++++ 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/doc/json_schema.json b/doc/json_schema.json index d64c5796bb..03ba42ecea 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -790,6 +790,11 @@ ], "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, diff --git a/src/common/endian.h b/src/common/endian.h index 2410911ee4..78de3aa3b6 100644 --- a/src/common/endian.h +++ b/src/common/endian.h @@ -2,7 +2,12 @@ #include -#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), \ diff --git a/src/common/impl/FFPlatform_unix.c b/src/common/impl/FFPlatform_unix.c index 2c0ab9db34..0c9020691a 100644 --- a/src/common/impl/FFPlatform_unix.c +++ b/src/common/impl/FFPlatform_unix.c @@ -102,14 +102,40 @@ static void getExePath(FFPlatform* platform) { 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 ~10% - FF_AUTO_FREE struct kinfo_file* kf = (struct kinfo_file*) malloc(fileSize); + 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; - do { - fileMib[5] = (int) (fileSize / sizeof(struct kinfo_file)); + // 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); - } while (rv == -1 && errno == ENOMEM); + if (rv == 0 || errno != ENOMEM || retries-- == 0) { + 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)); diff --git a/src/detection/top/top_apple.c b/src/detection/top/top_apple.c index b37793e867..5d5c9208e8 100644 --- a/src/detection/top/top_apple.c +++ b/src/detection/top/top_apple.c @@ -10,8 +10,14 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { if (npids <= 0) { return "proc_listallpids(nullptr, 0) failed"; } - FF_AUTO_FREE pid_t* pids = malloc((uint32_t) (npids + npids / 8 + 1) * sizeof(pid_t)); - npids = proc_listallpids(pids, npids); + // `proc_listallpids` returns the number of pids, but it wants the size of the buffer in bytes: + // passing the count back would only let the kernel fill a quarter of it. + const int pidCapacity = npids + npids / 8 + 1; + FF_AUTO_FREE pid_t* pids = malloc((size_t) pidCapacity * sizeof(pid_t)); + if (pids == nullptr) { + return "malloc() failed"; + } + npids = proc_listallpids(pids, pidCapacity * (int) sizeof(pid_t)); if (npids <= 0) { return "proc_listallpids(pids, bufferSize) failed"; } @@ -41,6 +47,10 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { item->startTime = proc.pbsd.pbi_start_tvsec * 1000u + proc.pbsd.pbi_start_tvusec / 1000u; // convert to ms item->threads = (uint32_t) proc.ptinfo.pti_threadnum; + // FF_LIST_ADD does not zero the element, and top.c subtracts both counters for every + // process it samples, whether or not they were collected. + item->bytesRead = 0; + item->bytesWritten = 0; if (showTypes & FF_TOP_TYPE_DISK) { struct rusage_info_v2 rusage; if (proc_pid_rusage(pid, RUSAGE_INFO_V2, (rusage_info_t*) &rusage) == 0) { diff --git a/src/detection/wallpaper/wallpaper_apple.m b/src/detection/wallpaper/wallpaper_apple.m index 1b4f6e3d59..9e9d863567 100644 --- a/src/detection/wallpaper/wallpaper_apple.m +++ b/src/detection/wallpaper/wallpaper_apple.m @@ -118,6 +118,11 @@ if (@available(macOS 14.0, *)) { error = detectFromPlist(result); + if (error) { + // The plist is the authoritative source on Sonoma and later, but when it can not be read + // at all, NSWorkspace can still resolve a user-picked static image. + error = detectFromNSWorkspace(result); + } } else { #ifdef FF_HAVE_SQLITE3 error = detectFromSQLite(result); diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c index 047f519056..8d5698e3c1 100644 --- a/src/logo/image/im6.c +++ b/src/logo/image/im6.c @@ -3,6 +3,7 @@ #include "image.h" #include "common/library.h" #include "common/mallocHelper.h" + #include "common/strutil.h" #include #include @@ -98,6 +99,17 @@ static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const ch ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + // The raw pixel coders write image->depth bits per sample, not 8: a 1-bit grayscale source comes + // back as columns*4/8 bytes per row and a 16-bit one as columns*8, while the RGBA caller hands + // the blob on as RGBA8 and derives its length from width*height*4. Pin the depth for the raw + // formats only -- the SIXEL coder quantises on its own, so it keeps the source depth and its + // output stays byte identical. + // ImageMagick 6 leaves no choice about where to pin it: AcquireQuantumInfo takes the depth from + // image->depth and never looks at image_info->depth. + if (ffStrEquals(magick, "RGBA")) { + image->depth = 8; + } + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); if (blob == nullptr || length == 0) { goto cleanup; @@ -142,6 +154,17 @@ bool ffImageCreateIM6(FFLogoRequestData* requestData, FFImageBuffer* out, const return false; } + // FFImageBuffer carries no length, so every consumer derives it from width*height*4. Refuse any + // other size rather than let them read past the blob -- the raw coder's depth scaling used to + // produce one (see the depth pin in im6EncodeImage). + if (length != (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4) { + if (error) { + *error = "Image Magick did not return an RGBA8 buffer"; + } + free(blob); + return false; + } + out->data = blob; out->width = requestData->logoPixelWidth; out->height = requestData->logoPixelHeight; From 6ff7f2772b96f16997cb664aeb2ddbea5d09af6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Thu, 17 Sep 2026 20:11:25 +0800 Subject: [PATCH 59/76] Tests: adds more UTs --- CMakeLists.txt | 64 ++++++++++++ tests/endian-bigendian.c | 44 ++++++++ tests/endian.c | 96 +++++++++++++++++ tests/frequency.c | 90 ++++++++++++++++ tests/parsing.c | 190 ++++++++++++++++++++++++++++++++++ tests/percent.c | 216 +++++++++++++++++++++++++++++++++++++++ tests/properties.c | 138 +++++++++++++++++++++++++ tests/size.c | 142 +++++++++++++++++++++++++ tests/strutil.c | 164 ++++++++++++++++++++++++++++- tests/temps.c | 177 ++++++++++++++++++++++++++++++++ 10 files changed, 1320 insertions(+), 1 deletion(-) create mode 100644 tests/endian-bigendian.c create mode 100644 tests/endian.c create mode 100644 tests/frequency.c create mode 100644 tests/parsing.c create mode 100644 tests/percent.c create mode 100644 tests/properties.c create mode 100644 tests/size.c create mode 100644 tests/temps.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cf4b05cc6..597c752e6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2239,6 +2239,62 @@ if (BUILD_TESTS) 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-strbuf COMMAND fastfetch-test-strbuf) add_test(NAME test-list COMMAND fastfetch-test-list) @@ -2248,6 +2304,14 @@ if (BUILD_TESTS) 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() ################## diff --git a/tests/endian-bigendian.c b/tests/endian-bigendian.c new file mode 100644 index 0000000000..39056bedf6 --- /dev/null +++ b/tests/endian-bigendian.c @@ -0,0 +1,44 @@ +// This translation unit pretends to be compiled on a big endian host, so that the endianness +// detection in `common/endian.h` can be exercised on a little endian machine. +// +// The selection is a preprocessor decision, so it can be pinned with `_Static_assert` instead of by +// running anything -- which matters, because the alternative would be to observe a byte swap at run +// time on hardware that really is little endian, and that cannot work. +// +// The case this guards: GCC does not define `__BIG_ENDIAN__` at all, it only documents +// `__BYTE_ORDER__` and the `__ORDER_*_ENDIAN__` values. A header that tests `#if __BIG_ENDIAN__` +// therefore evaluates it as 0 on s390x and silently picks the little endian branch, byte swapping +// values that were already big endian. `__BIG_ENDIAN__` is undefined here for exactly that reason: +// a header that only looks at its presence, or at its value, fails the assertions below. +// +// System headers are pulled in before the macros are faked so that they are not affected. + +#include +#include + +#include "common/textModifier.h" + +// What a big endian compiler reports ... +#undef __BYTE_ORDER__ +#define __BYTE_ORDER__ __ORDER_BIG_ENDIAN__ +// ... and what GCC does not report on such a machine, even though it is one. +#undef __BIG_ENDIAN__ + +#include "common/endian.h" + +// On a big endian host the byte order of the machine and the byte order of the data agree, so +// reading a big endian value is a no-op ... +_Static_assert(FF_READ_BE((uint16_t) 0x1122) == (uint16_t) 0x1122, "FF_READ_BE must be the identity on a big endian host"); +_Static_assert(FF_READ_BE(0x11223344u) == 0x11223344u, "FF_READ_BE must be the identity on a big endian host"); +_Static_assert(FF_READ_BE(0x1122334455667788ull) == 0x1122334455667788ull, "FF_READ_BE must be the identity on a big endian host"); + +// ... and reading a little endian one swaps. +_Static_assert(FF_READ_LE((uint16_t) 0x1122) == (uint16_t) 0x2211, "FF_READ_LE must swap on a big endian host"); +_Static_assert(FF_READ_LE(0x11223344u) == 0x44332211u, "FF_READ_LE must swap on a big endian host"); +_Static_assert(FF_READ_LE(0x1122334455667788ull) == 0x8877665544332211ull, "FF_READ_LE must swap on a big endian host"); + +int main(void) { + // Everything this test asserts is checked at compile time, so reaching this line is the result. + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/endian.c b/tests/endian.c new file mode 100644 index 0000000000..29294b4186 --- /dev/null +++ b/tests/endian.c @@ -0,0 +1,96 @@ +#include "common/endian.h" +#include "common/textModifier.h" + +#include +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +// The macros take a value, so the only way to test them is to lay out the bytes of a known number +// in memory and read them back through the macro. The expectations below do not depend on the +// endianness of the machine running the test: +// +// * copying the little endian byte sequence of 0x11223344 (44 33 22 11) into a `uint32_t` yields +// 0x11223344 on a little endian machine and 0x44332211 on a big endian one. `FF_READ_LE` has to +// turn both of them into 0x11223344, which means it is the identity on one machine and a byte +// swap on the other. +// * `FF_READ_BE` is the mirror image. +// +// On a little endian machine this only pins that the two macros are the identity / swap pair; the +// selection itself (a compiler that does not define `__BIG_ENDIAN__` at all must still be detected +// as big endian from `__BYTE_ORDER__`) can only be exercised by a big endian build, or by a cross +// target such as `clang --target=s390x-linux-gnu`. +static void verify16(void) { + const uint8_t littleEndianBytes[2] = { 0x22, 0x11 }; + const uint8_t bigEndianBytes[2] = { 0x11, 0x22 }; + + uint16_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == (uint16_t) 0x1122); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == (uint16_t) 0x1122); +} + +static void verify32(void) { + const uint8_t littleEndianBytes[4] = { 0x44, 0x33, 0x22, 0x11 }; + const uint8_t bigEndianBytes[4] = { 0x11, 0x22, 0x33, 0x44 }; + + uint32_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == 0x11223344u); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == 0x11223344u); +} + +static void verify64(void) { + const uint8_t littleEndianBytes[8] = { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }; + const uint8_t bigEndianBytes[8] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }; + + uint64_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == 0x1122334455667788ull); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == 0x1122334455667788ull); +} + +int main(void) { + verify16(); + verify32(); + verify64(); + + // A byte swap has to stay inside the width of the value it was given. A value whose bytes are + // all equal reads the same through either macro, on either kind of machine. + { + uint16_t narrow = 0x0F0Fu; + VERIFY(FF_READ_LE(narrow) == (uint16_t) 0x0F0F); + VERIFY(FF_READ_BE(narrow) == (uint16_t) 0x0F0F); + + uint32_t wide = 0x0F0F0F0Fu; + VERIFY(FF_READ_LE(wide) == 0x0F0F0F0Fu); + VERIFY(FF_READ_BE(wide) == 0x0F0F0F0Fu); + + uint64_t widest = 0x0F0F0F0F0F0F0F0Full; + VERIFY(FF_READ_LE(widest) == 0x0F0F0F0F0F0F0F0Full); + VERIFY(FF_READ_BE(widest) == 0x0F0F0F0F0F0F0F0Full); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/frequency.c b/tests/frequency.c new file mode 100644 index 0000000000..60f5a3c9ce --- /dev/null +++ b/tests/frequency.c @@ -0,0 +1,90 @@ +#include "common/frequency.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static void verify(uint32_t mhz, bool expectedResult, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + const bool returned = ffFreqAppendNum(mhz, &result); + + if (returned != expectedResult || !ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffFreqAppendNum(%u): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, mhz, expectedResult ? "true" : "false", expected, returned ? "true" : "false", result.chars); + exit(1); + } +} + +#define VERIFY_FREQ(mhz, expectedResult, expected) verify((mhz), (expectedResult), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + + // An unknown frequency prints nothing at all + { + VERIFY_FREQ(0, false, ""); + } + + // With `freqNdigits >= 0` the value is printed in GHz, with that many digits after the point + { + VERIFY_FREQ(1000, true, "1.00 GHz"); + VERIFY_FREQ(3600, true, "3.60 GHz"); + VERIFY_FREQ(5000, true, "5.00 GHz"); + VERIFY_FREQ(100, true, "0.10 GHz"); + VERIFY_FREQ(1, true, "0.00 GHz"); // rounds down to zero, but is still reported + VERIFY_FREQ(4294000, true, "4294.00 GHz"); // the whole `uint32_t` range is usable + } + + // `freqNdigits = 0` rounds to whole GHz + { + options->freqNdigits = 0; + VERIFY_FREQ(1000, true, "1 GHz"); + VERIFY_FREQ(3600, true, "4 GHz"); // rounded, not truncated + VERIFY_FREQ(1400, true, "1 GHz"); + VERIFY_FREQ(0, false, ""); + options->freqNdigits = 2; + } + + // A negative `freqNdigits` selects the integer MHz form instead + { + options->freqNdigits = -1; + VERIFY_FREQ(3600, true, "3600 MHz"); + VERIFY_FREQ(1000, true, "1000 MHz"); + VERIFY_FREQ(1, true, "1 MHz"); + VERIFY_FREQ(0, false, ""); + options->freqNdigits = 2; + } + + // Only `never` removes the space between the number and the unit + { + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_FREQ(3600, true, "3.60GHz"); + options->freqNdigits = -1; + VERIFY_FREQ(3600, true, "3600MHz"); + options->freqNdigits = 2; + + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_FREQ(3600, true, "3.60 GHz"); + + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_FREQ(3600, true, "3.60 GHz"); + } + + // The result is appended to the buffer instead of replacing it + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("CPU: "); + if (!ffFreqAppendNum(3600, &result) || !ffStrbufEqualS(&result, "CPU: 3.60 GHz")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffFreqAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/parsing.c b/tests/parsing.c new file mode 100644 index 0000000000..dbf56723c9 --- /dev/null +++ b/tests/parsing.c @@ -0,0 +1,190 @@ +#include "common/parsing.h" +#include "common/textModifier.h" + +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +static void verifyVersionCompare(uint32_t major1, uint32_t minor1, uint32_t patch1, uint32_t major2, uint32_t minor2, uint32_t patch2, int8_t expected, int lineNo) { + const FFVersion first = { major1, minor1, patch1 }; + const FFVersion second = { major2, minor2, patch2 }; + + const int8_t forward = ffVersionCompare(&first, &second); + const int8_t backward = ffVersionCompare(&second, &first); + + // Only the sign is meaningful to callers, but the comparison is documented to be antisymmetric, + // so a caller that negates the result to get "greater" must not be surprised. + if (forward != expected || backward != -expected) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffVersionCompare(%u.%u.%u, %u.%u.%u): expected %d/%d, got %d/%d\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major1, minor1, patch1, major2, minor2, patch2, expected, -expected, forward, backward); + exit(1); + } +} + +#define VERIFY_COMPARE(...) verifyVersionCompare(__VA_ARGS__, __LINE__) + +static void verifyPretty(uint32_t major, uint32_t minor, uint32_t patch, const char* expected, int lineNo) { + const FFVersion version = { major, minor, patch }; + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffVersionToPretty(&version, &result); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffVersionToPretty(%u.%u.%u): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major, minor, patch, expected, result.chars); + exit(1); + } +} + +#define VERIFY_PRETTY(...) verifyPretty(__VA_ARGS__, __LINE__) + +static void verifySemver(const char* major, const char* minor, const char* patch, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY majorBuf = ffStrbufCreateS(major); + FF_STRBUF_AUTO_DESTROY minorBuf = ffStrbufCreateS(minor); + FF_STRBUF_AUTO_DESTROY patchBuf = ffStrbufCreateS(patch); + + ffParseSemver(&result, &majorBuf, &minorBuf, &patchBuf); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParseSemver(\"%s\", \"%s\", \"%s\"): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major, minor, patch, expected, result.chars); + exit(1); + } +} + +#define VERIFY_SEMVER(...) verifySemver(__VA_ARGS__, __LINE__) + +static void verifyGtk(const char* gtk2, const char* gtk3, const char* gtk4, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY gtk2Buf = ffStrbufCreateS(gtk2); + FF_STRBUF_AUTO_DESTROY gtk3Buf = ffStrbufCreateS(gtk3); + FF_STRBUF_AUTO_DESTROY gtk4Buf = ffStrbufCreateS(gtk4); + + ffParseGTK(&result, >k2Buf, >k3Buf, >k4Buf); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParseGTK(\"%s\", \"%s\", \"%s\"): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, gtk2, gtk3, gtk4, expected, result.chars); + exit(1); + } +} + +#define VERIFY_GTK(...) verifyGtk(__VA_ARGS__, __LINE__) + +int main(void) { + // ffVersionCompare: the major field decides first, then minor, then patch. Equal versions + // compare equal even when they come from different sources. + { + VERIFY_COMPARE(0, 0, 0, 0, 0, 0, 0); + VERIFY_COMPARE(1, 2, 3, 1, 2, 3, 0); + VERIFY_COMPARE(UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, 0); + + VERIFY_COMPARE(2, 0, 0, 1, 99, 99, 1); + VERIFY_COMPARE(1, 99, 99, 2, 0, 0, -1); + + VERIFY_COMPARE(1, 2, 0, 1, 1, 99, 1); + VERIFY_COMPARE(1, 1, 99, 1, 2, 0, -1); + + VERIFY_COMPARE(1, 2, 3, 1, 2, 2, 1); + VERIFY_COMPARE(1, 2, 2, 1, 2, 3, -1); + + // A larger patch must not be able to outvote a smaller minor + VERIFY_COMPARE(1, 2, 0, 1, 1, UINT32_MAX, 1); + VERIFY_COMPARE(1, 1, UINT32_MAX, 1, 2, 0, -1); + + // ... nor a larger minor a smaller major + VERIFY_COMPARE(2, 0, 0, 1, UINT32_MAX, UINT32_MAX, 1); + VERIFY_COMPARE(1, UINT32_MAX, UINT32_MAX, 2, 0, 0, -1); + } + + // ffVersionToPretty: a zero version prints as nothing at all, and a field is only printed when + // it or a later field is non-zero. + { + VERIFY_PRETTY(0, 0, 0, ""); + VERIFY_PRETTY(1, 0, 0, "1"); + VERIFY_PRETTY(0, 1, 0, "0.1"); + VERIFY_PRETTY(0, 0, 1, "0.0.1"); + VERIFY_PRETTY(1, 2, 0, "1.2"); + VERIFY_PRETTY(1, 0, 3, "1.0.3"); + VERIFY_PRETTY(1, 2, 3, "1.2.3"); + VERIFY_PRETTY(10, 20, 30, "10.20.30"); + VERIFY_PRETTY(UINT32_MAX, 0, 0, "4294967295"); + VERIFY_PRETTY(0, 0, UINT32_MAX, "0.0.4294967295"); + } + + // ffParseSemver: joins whichever parts were detected. A missing major is reported as 1, which is + // how "2.3" from a source that only reports the last two components stays unambiguous. + { + VERIFY_SEMVER("1", "2", "3", "1.2.3"); + VERIFY_SEMVER("1", "2", "", "1.2"); + VERIFY_SEMVER("1", "", "", "1"); + VERIFY_SEMVER("1", "", "3", "1.0.3"); + VERIFY_SEMVER("", "2", "3", "1.2.3"); + VERIFY_SEMVER("", "2", "", "1.2"); + VERIFY_SEMVER("", "", "3", "1.0.3"); + VERIFY_SEMVER("", "", "", ""); + + // A zero part is a value, not a missing part + VERIFY_SEMVER("0", "0", "0", "0.0.0"); + VERIFY_SEMVER("0", "", "", "0"); + } + + // ffParseSemver appends instead of replacing, so a caller can prefix its own text + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("v"); + FF_STRBUF_AUTO_DESTROY majorBuf = ffStrbufCreateS("1"); + FF_STRBUF_AUTO_DESTROY minorBuf = ffStrbufCreateS("2"); + FF_STRBUF_AUTO_DESTROY patchBuf = ffStrbufCreateS("3"); + ffParseSemver(&result, &majorBuf, &minorBuf, &patchBuf); + VERIFY(ffStrbufEqualS(&result, "v1.2.3")); + } + + // ffParseGTK: the three versions are folded into one line, and every combination of present and + // equal versions has its own suffix. + { + // All three present + VERIFY_GTK("1.0", "1.0", "1.0", "1.0 [GTK2/3/4]"); + VERIFY_GTK("2.0", "2.0", "3.0", "2.0 [GTK2/3], 3.0 [GTK4]"); + VERIFY_GTK("2.0", "3.0", "3.0", "2.0 [GTK2], 3.0 [GTK3/4]"); + VERIFY_GTK("2.0", "3.0", "4.0", "2.0 [GTK2], 3.0 [GTK3], 4.0 [GTK4]"); + + // GTK2 and GTK4 equal while GTK3 differs is not special cased: it falls back to the plain + // three-way listing, so the same version is printed twice with different suffixes. + VERIFY_GTK("2.0", "3.0", "2.0", "2.0 [GTK2], 3.0 [GTK3], 2.0 [GTK4]"); + + // The comparison is case insensitive, but the value printed is the one from the buffer that + // the branch selected -- here the third one, not the first. + VERIFY_GTK("ABC", "abc", "aBc", "aBc [GTK2/3/4]"); + VERIFY_GTK("2.0", "2.0", "3.0", "2.0 [GTK2/3], 3.0 [GTK4]"); + + // Two present + VERIFY_GTK("1.0", "1.0", "", "1.0 [GTK2/3]"); + VERIFY_GTK("2.0", "3.0", "", "2.0 [GTK2], 3.0 [GTK3]"); + VERIFY_GTK("1.0", "", "1.0", "1.0 [GTK2/4]"); + VERIFY_GTK("2.0", "", "4.0", "2.0 [GTK2], 4.0 [GTK4]"); + VERIFY_GTK("", "1.0", "1.0", "1.0 [GTK3/4]"); + VERIFY_GTK("", "3.0", "4.0", "3.0 [GTK3], 4.0 [GTK4]"); + + // One present + VERIFY_GTK("2.0", "", "", "2.0 [GTK2]"); + VERIFY_GTK("", "3.0", "", "3.0 [GTK3]"); + VERIFY_GTK("", "", "4.0", "4.0 [GTK4]"); + + // None present + VERIFY_GTK("", "", "", ""); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/percent.c b/tests/percent.c new file mode 100644 index 0000000000..e697a854dd --- /dev/null +++ b/tests/percent.c @@ -0,0 +1,216 @@ +#include "common/option.h" +#include "common/percent.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static FFModuleArgs module; + +static void verifyNum(double percent, FFPercentageModuleConfig config, bool parentheses, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffPercentAppendNum(&result, percent, config, parentheses, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffPercentAppendNum(%f, {%u, %u, %u}, %s): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, percent, config.green, config.yellow, (unsigned) config.type, parentheses ? "true" : "false", expected, result.chars); + exit(1); + } +} + +#define VERIFY_NUM(percent, config, parentheses, expected) verifyNum((percent), (config), (parentheses), (expected), __LINE__) + +static void verifyBar(double percent, FFPercentageModuleConfig config, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffPercentAppendBar(&result, percent, config, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffPercentAppendBar(%f, {%u, %u, %u}): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, percent, config.green, config.yellow, (unsigned) config.type, expected, result.chars); + exit(1); + } +} + +#define VERIFY_BAR(percent, config, expected) verifyBar((percent), (config), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + ffOptionInitModuleArg(&module, ""); + + // Pin the threshold colors: the yellow and red defaults depend on whether the terminal is + // detected as a light theme, which is not something a test should depend on. + ffStrbufSetS(&options->percentColorGreen, "32"); + ffStrbufSetS(&options->percentColorYellow, "33"); + ffStrbufSetS(&options->percentColorRed, "31"); + + // The plain number. `pipe` disables the color, which is what the first group of cases checks. + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT | FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "0%"); + VERIFY_NUM(50, config, false, "50%"); + VERIFY_NUM(100, config, false, "100%"); + VERIFY_NUM(99.9, config, false, "100%"); // the default is zero digits, so this rounds + + // An unset percentage prints as a bare dash, without a percent sign + VERIFY_NUM(-DBL_MAX, config, false, "-"); + + // Parentheses wrap the whole thing, color included + VERIFY_NUM(0, config, true, "(0%)"); + VERIFY_NUM(-DBL_MAX, config, true, "(-)"); + } + + // percentNdigits + { + options->pipe = true; + options->percentNdigits = 2; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, "0.00%"); + VERIFY_NUM(50, config, false, "50.00%"); + VERIFY_NUM(33.333, config, false, "33.33%"); + VERIFY_NUM(100, config, false, "100.00%"); + VERIFY_NUM(-DBL_MAX, config, false, "-"); + + options->percentNdigits = 0; + } + + // percentWidth pads on the left, and the percent sign is not part of the padded field + { + options->pipe = true; + options->percentWidth = 5; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, " 0%"); + VERIFY_NUM(50, config, false, " 50%"); + VERIFY_NUM(100, config, false, " 100%"); + + options->percentWidth = 0; + } + + // Only `always` adds a space before the percent sign + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_NUM(50, config, false, "50 %"); + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_NUM(50, config, false, "50%"); + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_NUM(50, config, false, "50%"); + } + + // The color is picked by comparing against the thresholds, and each threshold belongs to the + // lower band: exactly `green` is green, exactly `yellow` is yellow. + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "\e[32m0%\e[m"); + VERIFY_NUM(50, config, false, "\e[32m50%\e[m"); + VERIFY_NUM(51, config, false, "\e[33m51%\e[m"); + VERIFY_NUM(79, config, false, "\e[33m79%\e[m"); + VERIFY_NUM(80, config, false, "\e[33m80%\e[m"); + VERIFY_NUM(81, config, false, "\e[31m81%\e[m"); + VERIFY_NUM(100, config, false, "\e[31m100%\e[m"); + + VERIFY_NUM(-DBL_MAX, config, false, "\e[90m-\e[m"); + VERIFY_NUM(-DBL_MAX, config, true, "(\e[90m-\e[m)"); + } + + // When `green > yellow` the bands are inverted, so a low value is the alarming one + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 80, .yellow = 50, .type = FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "\e[31m0%\e[m"); + VERIFY_NUM(49, config, false, "\e[31m49%\e[m"); + VERIFY_NUM(50, config, false, "\e[33m50%\e[m"); + VERIFY_NUM(79, config, false, "\e[33m79%\e[m"); + VERIFY_NUM(80, config, false, "\e[32m80%\e[m"); + VERIFY_NUM(100, config, false, "\e[32m100%\e[m"); + } + + // Without the color flag the number is never colored, even when colors are enabled + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, "0%"); + VERIFY_NUM(100, config, false, "100%"); + } + + // A zero `type` means "use the global default" instead of "no flags" + { + options->pipe = false; + FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = 0 }; + + options->percentType = FF_PERCENTAGE_TYPE_NUM_BIT; + VERIFY_NUM(100, config, false, "100%"); + + options->percentType = FF_PERCENTAGE_TYPE_NUM_BIT | FF_PERCENTAGE_TYPE_NUM_COLOR_BIT; + VERIFY_NUM(100, config, false, "\e[31m100%\e[m"); + } + + // The bar fills `percent` of the width, rounding to the nearest block. Colors are left to the + // `pipe` free cases above; the block characters and the borders are what is checked here. + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = 0 }; + + options->barWidth = 10; + VERIFY_BAR(0, config, "[ ---------- ]"); + VERIFY_BAR(50, config, "[ ■■■■■----- ]"); + VERIFY_BAR(100, config, "[ ■■■■■■■■■■ ]"); + + // An unset percentage fills the bar with the "total" character, same as zero + VERIFY_BAR(-DBL_MAX, config, "[ ---------- ]"); + + // The half block is rounded up at exactly 0.5 + VERIFY_BAR(4, config, "[ ---------- ]"); + VERIFY_BAR(5, config, "[ ■--------- ]"); + VERIFY_BAR(14, config, "[ ■--------- ]"); + VERIFY_BAR(15, config, "[ ■■-------- ]"); + + options->barWidth = 4; + VERIFY_BAR(0, config, "[ ---- ]"); + VERIFY_BAR(25, config, "[ ■--- ]"); + VERIFY_BAR(50, config, "[ ■■-- ]"); + VERIFY_BAR(100, config, "[ ■■■■ ]"); + + // The borders are configurable and may be removed entirely + ffStrbufSetS(&options->barBorderLeft, ""); + ffStrbufSetS(&options->barBorderRight, ""); + VERIFY_BAR(50, config, "■■--"); + + ffStrbufSetS(&options->barBorderLeft, "<"); + ffStrbufSetS(&options->barBorderRight, ">"); + VERIFY_BAR(50, config, "<■■-->"); + + ffStrbufSetS(&options->barBorderLeft, "[ "); + ffStrbufSetS(&options->barBorderRight, " ]"); + + // The bar characters themselves are configurable + ffStrbufSetS(&options->barCharElapsed, "#"); + ffStrbufSetS(&options->barCharTotal, "."); + VERIFY_BAR(50, config, "[ ##.. ]"); + ffStrbufSetS(&options->barCharElapsed, "■"); + ffStrbufSetS(&options->barCharTotal, "-"); + + options->barWidth = 10; + } + + ffOptionDestroyModuleArg(&module); + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/properties.c b/tests/properties.c new file mode 100644 index 0000000000..e8f95ec05f --- /dev/null +++ b/tests/properties.c @@ -0,0 +1,138 @@ +#include "common/properties.h" +#include "common/textModifier.h" + +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +static void verifyLine(const char* line, const char* start, bool expectedResult, const char* expectedValue, int lineNo) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const char* cursor = line; + const bool result = ffParsePropLinePointer(&cursor, start, &buffer); + + if (result != expectedResult || (result && !ffStrbufEqualS(&buffer, expectedValue))) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParsePropLine(\"%s\", \"%s\"): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, line, start, expectedResult ? "true" : "false", expectedResult ? expectedValue : "", + result ? "true" : "false", result ? buffer.chars : ""); + exit(1); + } +} + +#define VERIFY_LINE(...) verifyLine(__VA_ARGS__, __LINE__) + +static void verifyLines(const char* lines, const char* start, bool expectedResult, const char* expectedValue, int lineNo) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const bool result = ffParsePropLines(lines, start, &buffer); + + if (result != expectedResult || (result && !ffStrbufEqualS(&buffer, expectedValue))) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParsePropLines(\"%s\", \"%s\"): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, lines, start, expectedResult ? "true" : "false", expectedResult ? expectedValue : "", + result ? "true" : "false", result ? buffer.chars : ""); + exit(1); + } +} + +#define VERIFY_LINES(...) verifyLines(__VA_ARGS__, __LINE__) + +int main(void) { + // The plain case: `key: value`, with the value running to the end of the line. + { + VERIFY_LINE("Name: foo", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\n", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\nBar: baz", "Name:", true, "foo"); // stops at the newline + VERIFY_LINE("Name:foo", "Name:", true, "foo"); + VERIFY_LINE("Name: ", "Name:", true, ""); // found, but the value is empty + VERIFY_LINE("Name:", "Name:", true, ""); + VERIFY_LINE("Name: a:b", "Name:", true, "a:b"); // only the first colon is the separator + VERIFY_LINE("Name: foo bar", "Name:", true, "foo bar"); // spaces inside the value are kept + } + + // Any amount of whitespace in the format matches any amount of whitespace in the line, including + // none -- the two sides are independent. + { + VERIFY_LINE("Name: foo", "Name:", true, "foo"); + VERIFY_LINE("Name:foo", "Name: ", true, "foo"); + VERIFY_LINE("Name: foo", "Name: ", true, "foo"); + VERIFY_LINE("Name:\tfoo", "Name: ", true, "foo"); + VERIFY_LINE("Name: foo", "Name:\t", true, "foo"); + VERIFY_LINE(" \t Name: foo", "Name:", true, "foo"); // leading whitespace of the line is skipped + } + + // A mismatch anywhere in the key makes the whole line fail, and the key must match in full. + { + VERIFY_LINE("Name: foo", "Value:", false, ""); + VERIFY_LINE("name: foo", "Name:", true, "foo"); // the key is compared case insensitively + VERIFY_LINE("NAME: foo", "name:", true, "foo"); + VERIFY_LINE("nAmE: foo", "NAME:", true, "foo"); + VERIFY_LINE("NameX: foo", "Name:", false, ""); + VERIFY_LINE("Name :foo", "Name:", false, ""); + VERIFY_LINE("Nam", "Name:", false, ""); // the line ends before the key does + VERIFY_LINE("", "Name:", false, ""); + VERIFY_LINE("\n", "Name:", false, ""); + VERIFY_LINE("Name", "Name:", false, ""); + } + + // Trailing spaces are trimmed, but nothing else is: a trailing tab survives. + { + VERIFY_LINE("Name: foo ", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\t", "Name:", true, "foo\t"); + VERIFY_LINE("Name: foo \n", "Name:", true, "foo"); + } + + // A quoted value ends at its closing quote, so trailing text is not part of the value. + { + VERIFY_LINE("Name: \"foo bar\"", "Name:", true, "foo bar"); + VERIFY_LINE("Name: 'foo bar'", "Name:", true, "foo bar"); + VERIFY_LINE("Name: \"foo\" trailing", "Name:", true, "foo"); + VERIFY_LINE("Name: \"\"", "Name:", true, ""); + VERIFY_LINE("Name: \"foo", "Name:", true, "foo"); // unterminated quote: the line end closes it + + // The trim still applies, and it only removes the right hand side + VERIFY_LINE("Name: \" foo \"", "Name:", true, " foo"); + } + + // A key ending in `>` switches the parser to XML mode, where the value ends at `<` instead of at + // the end of the line. + { + VERIFY_LINE("foo", "", true, "foo"); + VERIFY_LINE("", "", true, ""); + VERIFY_LINE("foo", "", true, "foo"); // no closing tag + VERIFY_LINE("foo bar", "", true, "foo bar"); + VERIFY_LINE(" foo", "", true, "foo"); + VERIFY_LINE("foo\n", "", true, "foo"); + } + + // ffParsePropLines walks the lines until the key is found, and the first match wins -- including + // when that match has an empty value. + { + VERIFY_LINES("A: 1\nName: foo\nB: 2", "Name:", true, "foo"); + VERIFY_LINES("A: 1\nName: foo", "Name:", true, "foo"); + VERIFY_LINES("Name: foo", "Name:", true, "foo"); + VERIFY_LINES("\n\nName: foo", "Name:", true, "foo"); + VERIFY_LINES("Name: foo\nName: bar", "Name:", true, "foo"); + VERIFY_LINES("Name:\nName: bar", "Name:", true, ""); + VERIFY_LINES("A: 1\nB: 2", "Name:", false, ""); + VERIFY_LINES("A: 1\nB: 2\n", "Name:", false, ""); + VERIFY_LINES("", "Name:", false, ""); + } + + // The value is appended to the buffer, so a caller can collect several keys into one string. + { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS("pre"); + VERIFY(ffParsePropLines("Name: foo", "Name:", &buffer)); + VERIFY(ffStrbufEqualS(&buffer, "prefoo")); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/size.c b/tests/size.c new file mode 100644 index 0000000000..0288829e1b --- /dev/null +++ b/tests/size.c @@ -0,0 +1,142 @@ +#include "common/size.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static void verify(uint64_t bytes, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffSizeAppendNum(bytes, &result); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffSizeAppendNum(%llu): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, (unsigned long long) bytes, expected, result.chars); + exit(1); + } +} + +#define VERIFY_SIZE(bytes, expected) verify((bytes), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + + // IEC (the default): 1024 based, with the `KiB` style suffixes + { + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1, "1 B"); + VERIFY_SIZE(999, "999 B"); + VERIFY_SIZE(1023, "1023 B"); // one byte short of the first prefix + VERIFY_SIZE(1024, "1.00 KiB"); + VERIFY_SIZE(1025, "1.00 KiB"); // below the rounding precision + VERIFY_SIZE(1536, "1.50 KiB"); + VERIFY_SIZE(1024ULL * 1024, "1.00 MiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1.00 GiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024, "1.00 TiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024, "1.00 PiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024 * 1024, "1.00 EiB"); + + // The largest 64 bit value is 16 EiB; `ZiB` and `YiB` are listed but can never be reached + // through an IEC conversion of a `uint64_t`. + VERIFY_SIZE(UINT64_MAX, "16.00 EiB"); + } + + // SI: 1000 based, with the `kB` style suffixes + { + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_SI; + + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(999, "999 B"); + VERIFY_SIZE(1000, "1.00 kB"); + VERIFY_SIZE(1500, "1.50 kB"); + VERIFY_SIZE(1000000, "1.00 MB"); + VERIFY_SIZE(1000000000ULL, "1.00 GB"); + VERIFY_SIZE(1000000000000ULL, "1.00 TB"); + VERIFY_SIZE(1000000000000000ULL, "1.00 PB"); + VERIFY_SIZE(1000000000000000000ULL, "1.00 EB"); + VERIFY_SIZE(UINT64_MAX, "18.45 EB"); + } + + // JEDEC: 1024 based, but with the `KB` style suffixes. The suffix list stops at `TB`, so the + // loop has to stop there as well instead of reading past the end. + { + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_JEDEC; + + VERIFY_SIZE(1023, "1023 B"); + VERIFY_SIZE(1024, "1.00 KB"); + VERIFY_SIZE(1024ULL * 1024, "1.00 MB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1.00 GB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024, "1.00 TB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024, "1024.00 TB"); // no `PB` in JEDEC + VERIFY_SIZE(UINT64_MAX, "16777216.00 TB"); + } + + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_IEC; + + // sizeNdigits: the number of digits after the decimal point. It applies only once a prefix is + // used -- a plain byte count is always printed as an integer. + { + options->sizeNdigits = 0; + VERIFY_SIZE(1024, "1 KiB"); + VERIFY_SIZE(1536, "2 KiB"); // rounded, not truncated + VERIFY_SIZE(512, "512 B"); + VERIFY_SIZE(1023, "1023 B"); + + options->sizeNdigits = 4; + VERIFY_SIZE(1024, "1.0000 KiB"); + VERIFY_SIZE(1536, "1.5000 KiB"); + VERIFY_SIZE(512, "512 B"); // still an integer + VERIFY_SIZE(0, "0 B"); + + options->sizeNdigits = 2; + } + + // sizeMaxPrefix: the index of the largest suffix that may be used. `0` disables the conversion + // entirely. + { + options->sizeMaxPrefix = 0; + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1024, "1024 B"); + VERIFY_SIZE(UINT64_MAX, "18446744073709551615 B"); + + options->sizeMaxPrefix = 1; + VERIFY_SIZE(1024, "1.00 KiB"); + VERIFY_SIZE(1024ULL * 1024, "1024.00 KiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1048576.00 KiB"); + + options->sizeMaxPrefix = 8; + } + + // sizeSpaceBeforeUnit: only `never` removes the space, `default` and `always` both keep it + { + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_SIZE(0, "0B"); + VERIFY_SIZE(1024, "1.00KiB"); + VERIFY_SIZE(1536, "1.50KiB"); + + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1024, "1.00 KiB"); + + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_SIZE(1024, "1.00 KiB"); + } + + // The result is appended to the buffer instead of replacing it + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("Size: "); + ffSizeAppendNum(1024, &result); + if (!ffStrbufEqualS(&result, "Size: 1.00 KiB")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffSizeAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/strutil.c b/tests/strutil.c index 3d065eee67..1b06ff5b74 100644 --- a/tests/strutil.c +++ b/tests/strutil.c @@ -17,6 +17,168 @@ static void verify(bool expression, const char* expressionStr, int lineNo) { #define VERIFY(expression) verify((expression), #expression, __LINE__) int main(void) { + // The plain C string helpers. These have nothing to do with wcwidth, so they run in every build. + { + VERIFY(!ffStrSet(nullptr)); + VERIFY(!ffStrSet("")); + VERIFY(!ffStrSet(" ")); + VERIFY(!ffStrSet(" \t \n")); + VERIFY(ffStrSet("a")); + VERIFY(ffStrSet(" a")); + VERIFY(ffStrSet("a ")); + } + + { + VERIFY(ffStrStartsWith("", "")); + VERIFY(ffStrStartsWith("abc", "")); + VERIFY(ffStrStartsWith("abc", "a")); + VERIFY(ffStrStartsWith("abc", "abc")); + VERIFY(!ffStrStartsWith("abc", "abcd")); // the needle is longer than the haystack + VERIFY(!ffStrStartsWith("abc", "b")); + VERIFY(!ffStrStartsWith("", "a")); + VERIFY(!ffStrStartsWith("Abc", "abc")); + + VERIFY(ffStrStartsWithIgnCase("Abc", "abc")); + VERIFY(ffStrStartsWithIgnCase("ABC", "abc")); + VERIFY(ffStrStartsWithIgnCase("abc", "")); + VERIFY(!ffStrStartsWithIgnCase("Abc", "abd")); + } + + { + VERIFY(ffStrEndsWith("", "")); + VERIFY(ffStrEndsWith("abc", "")); + VERIFY(ffStrEndsWith("abc", "c")); + VERIFY(ffStrEndsWith("abc", "abc")); + VERIFY(!ffStrEndsWith("abc", "abcd")); // the needle is longer than the haystack + VERIFY(!ffStrEndsWith("abc", "b")); + VERIFY(!ffStrEndsWith("", "a")); + VERIFY(!ffStrEndsWith("abc", "Abc")); + + VERIFY(ffStrEndsWithIgnCase("abc", "BC")); + VERIFY(ffStrEndsWithIgnCase("abc", "")); + VERIFY(!ffStrEndsWithIgnCase("abc", "BD")); + } + + { + VERIFY(ffStrEquals("", "")); + VERIFY(ffStrEquals("abc", "abc")); + VERIFY(!ffStrEquals("abc", "abcd")); + VERIFY(!ffStrEquals("abc", "ABC")); + + VERIFY(ffStrEqualsIgnCase("abc", "ABC")); + VERIFY(ffStrEqualsIgnCase("", "")); + VERIFY(!ffStrEqualsIgnCase("abc", "abd")); + + VERIFY(ffStrContains("abc", "")); + VERIFY(ffStrContains("abc", "b")); + VERIFY(ffStrContains("abc", "abc")); + VERIFY(!ffStrContains("abc", "d")); + + VERIFY(ffStrContainsIgnCase("abc", "B")); + VERIFY(!ffStrContainsIgnCase("abc", "D")); + // `strcasestr` reports an empty needle as a match, but the `StrStrIA` it is aliased to on + // Windows reports no match, so on Windows this disagrees with `ffStrContains`. +#ifndef _WIN32 + VERIFY(ffStrContainsIgnCase("abc", "")); +#endif + + VERIFY(ffStrContainsC("abc", 'b')); + VERIFY(!ffStrContainsC("abc", 'd')); + VERIFY(ffStrContainsC("abc", '\0')); // `strchr` reports the terminator as a match + } + + { + VERIFY(ffCharIsEnglishAlphabet('a')); + VERIFY(ffCharIsEnglishAlphabet('z')); + VERIFY(ffCharIsEnglishAlphabet('A')); + VERIFY(ffCharIsEnglishAlphabet('Z')); + VERIFY(!ffCharIsEnglishAlphabet('0')); + VERIFY(!ffCharIsEnglishAlphabet('_')); + VERIFY(!ffCharIsEnglishAlphabet(' ')); + + VERIFY(ffCharIsDigit('0')); + VERIFY(ffCharIsDigit('9')); + VERIFY(!ffCharIsDigit('/')); // one below '0' + VERIFY(!ffCharIsDigit(':')); // one above '9' + VERIFY(!ffCharIsDigit('a')); + + VERIFY(ffCharIsHexDigit('0')); + VERIFY(ffCharIsHexDigit('9')); + VERIFY(ffCharIsHexDigit('a')); + VERIFY(ffCharIsHexDigit('F')); + VERIFY(!ffCharIsHexDigit('g')); + VERIFY(!ffCharIsHexDigit('G')); + VERIFY(!ffCharIsHexDigit('/')); + VERIFY(!ffCharIsHexDigit('@')); + } + + { + VERIFY(ffHexCharToInt('0') == 0); + VERIFY(ffHexCharToInt('9') == 9); + VERIFY(ffHexCharToInt('a') == 10); + VERIFY(ffHexCharToInt('f') == 15); + VERIFY(ffHexCharToInt('A') == 10); + VERIFY(ffHexCharToInt('F') == 15); + VERIFY(ffHexCharToInt('g') == -1); + VERIFY(ffHexCharToInt('G') == -1); + VERIFY(ffHexCharToInt('/') == -1); // one below '0' + VERIFY(ffHexCharToInt(':') == -1); // one above '9' + VERIFY(ffHexCharToInt('@') == -1); // one below 'A' + VERIFY(ffHexCharToInt('`') == -1); // one below 'a' + VERIFY(ffHexCharToInt(' ') == -1); + } + + // ffStrCopy never writes more than `dstBufSiz` bytes and always terminates. It returns a pointer + // to the end of the copy, so consecutive calls can be chained. + { + char buffer[8]; + + memset(buffer, 'X', sizeof(buffer)); + char* end = ffStrCopy(buffer, "abc", sizeof(buffer)); + VERIFY(ffStrEquals(buffer, "abc")); + VERIFY(end == buffer + 3); + + memset(buffer, 'X', sizeof(buffer)); + end = ffStrCopy(buffer, "abcdef", sizeof(buffer)); + VERIFY(ffStrEquals(buffer, "abcdef")); + VERIFY(end == buffer + 6); + + // Filling the buffer exactly still leaves room for the terminator + char exact[4]; + memset(exact, 'X', sizeof(exact)); + end = ffStrCopy(exact, "abc", sizeof(exact)); + VERIFY(ffStrEquals(exact, "abc")); + VERIFY(end == exact + 3); + + // One byte short: the copy is truncated to `dstBufSiz - 1` bytes + char truncated[3]; + memset(truncated, 'X', sizeof(truncated)); + end = ffStrCopy(truncated, "abcdef", sizeof(truncated)); + VERIFY(ffStrEquals(truncated, "ab")); + VERIFY(end == truncated + 2); + + // A one byte buffer holds nothing but the terminator + char single[1]; + single[0] = 'X'; + end = ffStrCopy(single, "abc", sizeof(single)); + VERIFY(single[0] == '\0'); + VERIFY(end == single); + + // A zero sized buffer is left completely alone + char untouched[1] = { 'X' }; + end = ffStrCopy(untouched, "abc", 0); + VERIFY(untouched[0] == 'X'); + VERIFY(end == untouched); + + VERIFY(ffStrCopy(nullptr, "abc", 8) == nullptr); + } + + // FF_STR stringifies its argument + { + VERIFY(ffStrEquals(FF_STR(1), "1")); + VERIFY(ffStrEquals(FF_STR(123), "123")); + } + #if FF_ENABLE_WCWIDTH { uint8_t width = 255; @@ -102,7 +264,7 @@ int main(void) { puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); #else - puts("\033[33mTests skipped because wcwidth support is disabled." FASTFETCH_TEXT_MODIFIER_RESET); + puts("\033[33mwcwidth tests skipped because wcwidth support is disabled." FASTFETCH_TEXT_MODIFIER_RESET); #endif return 0; } diff --git a/tests/temps.c b/tests/temps.c new file mode 100644 index 0000000000..84978bff44 --- /dev/null +++ b/tests/temps.c @@ -0,0 +1,177 @@ +#include "common/option.h" +#include "common/temps.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static FFModuleArgs module; + +static void verify(double celsius, FFColorRangeConfig config, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffTempsAppendNum(celsius, &result, config, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffTempsAppendNum(%f, {%u, %u}): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, celsius, config.green, config.yellow, expected, result.chars); + exit(1); + } +} + +#define VERIFY_TEMP(celsius, config, expected) verify((celsius), (config), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + ffOptionInitModuleArg(&module, ""); + + // Pin the threshold colors: the yellow and red defaults depend on whether the terminal is + // detected as a light theme, which is not something a test should depend on. + ffStrbufSetS(&options->tempColorGreen, "32"); + ffStrbufSetS(&options->tempColorYellow, "33"); + ffStrbufSetS(&options->tempColorRed, "31"); + + const FFColorRangeConfig config = { .green = 50, .yellow = 80 }; + + // An unknown temperature prints nothing at all, not even the unit + { + options->pipe = true; + VERIFY_TEMP(-DBL_MAX, config, ""); + } + + // The default unit is Celsius, with one digit after the point + { + options->pipe = true; + VERIFY_TEMP(0, config, "0.0°C"); + VERIFY_TEMP(25.5, config, "25.5°C"); + VERIFY_TEMP(-10, config, "-10.0°C"); + VERIFY_TEMP(-273.15, config, "-273.1°C"); + } + + // tempNdigits + { + options->pipe = true; + options->tempNdigits = 0; + VERIFY_TEMP(25.4, config, "25°C"); + VERIFY_TEMP(25.6, config, "26°C"); + VERIFY_TEMP(0, config, "0°C"); + + options->tempNdigits = 2; + VERIFY_TEMP(25.5, config, "25.50°C"); + + options->tempNdigits = 1; + } + + // Celsius and Fahrenheit only take a space before the unit when it is forced on, while Kelvin + // takes one by default. That asymmetry is deliberate and easy to break. + { + options->pipe = true; + options->tempNdigits = 2; // so that 25.5 + 273.15 prints exactly + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_TEMP(25.5, config, "25.50°C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65 K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_TEMP(25.5, config, "25.50°C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_TEMP(25.5, config, "25.50 °C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65 K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + options->tempNdigits = 1; + } + + // Fahrenheit and Kelvin are conversions of the Celsius value that the thresholds were compared + // against, so the unit changes what is printed but never which band is picked. + { + options->pipe = true; + + options->tempUnit = FF_TEMPERATURE_UNIT_FAHRENHEIT; + VERIFY_TEMP(0, config, "32.0°F"); + VERIFY_TEMP(100, config, "212.0°F"); + VERIFY_TEMP(37, config, "98.6°F"); + VERIFY_TEMP(-40, config, "-40.0°F"); // the one point where the two scales meet + + options->tempNdigits = 2; + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(0, config, "273.15 K"); + VERIFY_TEMP(100, config, "373.15 K"); + VERIFY_TEMP(-273.15, config, "0.00 K"); + options->tempNdigits = 1; + + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + } + + // Colors, with the same band rules as the percentage module: the threshold itself belongs to the + // lower band, and the reset sequence is always emitted when colors are on. + { + options->pipe = false; + + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m"); + VERIFY_TEMP(50, config, "\e[32m50.0°C\e[m"); + VERIFY_TEMP(51, config, "\e[33m51.0°C\e[m"); + VERIFY_TEMP(80, config, "\e[33m80.0°C\e[m"); + VERIFY_TEMP(81, config, "\e[31m81.0°C\e[m"); + VERIFY_TEMP(100, config, "\e[31m100.0°C\e[m"); + + // The color is chosen from the Celsius value, so a display unit that is not Celsius does not + // move the threshold + options->tempUnit = FF_TEMPERATURE_UNIT_FAHRENHEIT; + VERIFY_TEMP(81, config, "\e[31m177.8°F\e[m"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + // An unknown temperature has no color at all + VERIFY_TEMP(-DBL_MAX, config, ""); + + // When `green > yellow` the bands are inverted, so a low value is the alarming one + const FFColorRangeConfig inverted = { .green = 80, .yellow = 50 }; + VERIFY_TEMP(0, inverted, "\e[31m0.0°C\e[m"); + VERIFY_TEMP(49, inverted, "\e[31m49.0°C\e[m"); + VERIFY_TEMP(50, inverted, "\e[33m50.0°C\e[m"); + VERIFY_TEMP(79, inverted, "\e[33m79.0°C\e[m"); + VERIFY_TEMP(80, inverted, "\e[32m80.0°C\e[m"); + VERIFY_TEMP(100, inverted, "\e[32m100.0°C\e[m"); + } + + // The color is restored to the module's output color, so the rest of the line keeps its own + { + options->pipe = false; + ffStrbufSetS(&module.outputColor, "94"); + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m\e[94m"); + ffStrbufClear(&module.outputColor); + + // ... and it falls back to the global output color when the module has none + ffStrbufSetS(&options->colorOutput, "95"); + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m\e[95m"); + ffStrbufClear(&options->colorOutput); + } + + // The result is appended to the buffer instead of replacing it + { + options->pipe = true; + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("Temp: "); + ffTempsAppendNum(25.5, &result, config, &module); + if (!ffStrbufEqualS(&result, "Temp: 25.5°C")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffTempsAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionDestroyModuleArg(&module); + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} From 97ac94eb44fb641ce93fee4ef0ddff7dd2e8245e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Thu, 17 Sep 2026 21:00:36 +0800 Subject: [PATCH 60/76] Common: adds more gcc constrants --- src/common/FFlist.h | 33 ++-- src/common/FFstrbuf.h | 294 +++++++++++++++------------- src/common/base64.h | 9 +- src/common/debug.h | 2 +- src/common/duration.h | 2 +- src/common/frequency.h | 4 +- src/common/impl/FFstrbuf.c | 21 +- src/common/impl/base64.c | 3 +- src/common/impl/library.c | 4 +- src/common/impl/networking_common.c | 9 +- src/common/impl/option.c | 2 - src/common/io.h | 15 +- src/common/jsonconfig.h | 19 +- src/common/library.h | 11 +- src/common/mallocHelper.h | 3 +- src/common/networking.h | 17 +- src/common/option.h | 26 ++- src/common/parsing.h | 9 +- src/common/path.h | 6 +- src/common/percent.h | 7 +- src/common/printing.h | 11 +- src/common/properties.h | 34 ++-- src/common/size.h | 2 +- src/common/smbios.h | 14 +- src/common/strutil.h | 38 +++- src/common/sysctl.h | 9 +- src/common/temps.h | 5 +- src/common/thread.h | 19 +- src/common/wcwidth.h | 5 +- 29 files changed, 346 insertions(+), 287 deletions(-) diff --git a/src/common/FFlist.h b/src/common/FFlist.h index 630b0734b2..be98668bb7 100644 --- a/src/common/FFlist.h +++ b/src/common/FFlist.h @@ -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); diff --git a/src/common/FFstrbuf.h b/src/common/FFstrbuf.h index 0f4f405a6a..879508a072 100644 --- a/src/common/FFstrbuf.h +++ b/src/common/FFstrbuf.h @@ -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 } diff --git a/src/common/base64.h b/src/common/base64.h index d17da5c70f..52a60130fc 100644 --- a/src/common/base64.h +++ b/src/common/base64.h @@ -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); diff --git a/src/common/debug.h b/src/common/debug.h index ee7f4be04a..ea7c27f4ca 100644 --- a/src/common/debug.h +++ b/src/common/debug.h @@ -3,7 +3,7 @@ #include "fastfetch.h" #include "common/time.h" -static inline const char* ffFindFileName(const char* file) { +[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline const char* ffFindFileName(const char* file) { const char* lastSlash = __builtin_strrchr(file, '/'); #ifdef _WIN32 if (lastSlash == nullptr) { diff --git a/src/common/duration.h b/src/common/duration.h index 30733c946a..2336eb86e5 100644 --- a/src/common/duration.h +++ b/src/common/duration.h @@ -2,4 +2,4 @@ #include "fastfetch.h" -void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result); +[[gnu::nonnull(2)]] void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result); diff --git a/src/common/frequency.h b/src/common/frequency.h index 7a3bdab02e..9ffe51b7ba 100644 --- a/src/common/frequency.h +++ b/src/common/frequency.h @@ -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); diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c index 20693c1ecf..494e487c88 100644 --- a/src/common/impl/FFstrbuf.c +++ b/src/common/impl/FFstrbuf.c @@ -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)); @@ -842,8 +827,6 @@ bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLeng } bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) { - assert(strbuf); - if (strbuf->length < 4) { return false; } diff --git a/src/common/impl/base64.c b/src/common/impl/base64.c index 50d7d47183..6db4db5f62 100644 --- a/src/common/impl/base64.c +++ b/src/common/impl/base64.c @@ -66,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(); } @@ -113,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; } diff --git a/src/common/impl/library.c b/src/common/impl/library.c index 83a315d974..757890d62a 100644 --- a/src/common/impl/library.c +++ b/src/common/impl/library.c @@ -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) { diff --git a/src/common/impl/networking_common.c b/src/common/impl/networking_common.c index cc6f5e24b5..949cc9ce0f 100644 --- a/src/common/impl/networking_common.c +++ b/src/common/impl/networking_common.c @@ -64,7 +64,8 @@ static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) { // Decompress gzip content bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) { - assert(headerEnd != nullptr && *headerEnd == '\r'); + // `headerEnd` itself is non-null per the `nonnull(2)` contract; what it points to is not expressible as an attribute + assert(*headerEnd == '\r'); // Calculate header size uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars); @@ -192,8 +193,6 @@ bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) { #endif // FF_HAVE_ZLIB const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen) { - assert(headers != nullptr && valueLen != nullptr); - uint32_t nameLen = (uint32_t) strlen(name); uint32_t pos = 0; @@ -225,8 +224,6 @@ const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, cons } FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen) { - assert(value != nullptr); - // The value is a comma-separated list of transfer codings (RFC 9112 6.1) uint32_t codingCount = 0; const char* coding = nullptr; @@ -267,8 +264,6 @@ FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value } int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* consumed) { - assert(body != nullptr && consumed != nullptr); - uint32_t pos = 0; for (;;) { diff --git a/src/common/impl/option.c b/src/common/impl/option.c index 24b4c7acca..fb27abe715 100644 --- a/src/common/impl/option.c +++ b/src/common/impl/option.c @@ -7,8 +7,6 @@ // Return start position of the inner key if the argument key belongs to the module specified, nullptr otherwise const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName) { - assert(argumentKey && moduleName); - const char* subKey = argumentKey; if (!(subKey[0] == '-' && subKey[1] == '-')) { return nullptr; diff --git a/src/common/io.h b/src/common/io.h index eba05683fa..c4a12fb3bf 100644 --- a/src/common/io.h +++ b/src/common/io.h @@ -36,8 +36,8 @@ typedef int FFNativeFD; #endif // Only O_RDONLY is supported -HANDLE openat(HANDLE dfd, const char* fileName, int oflag); -HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory); +[[gnu::nonnull(2)]] HANDLE openat(HANDLE dfd, const char* fileName, int oflag); +[[gnu::nonnull(2)]] HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory); #endif static inline bool ffIsValidNativeFD(FFNativeFD fd) { @@ -51,8 +51,6 @@ static inline bool ffIsValidNativeFD(FFNativeFD fd) { [[gnu::always_inline, gnu::nonnull(1)]] static inline void wrapClose(FFNativeFD* pfd) { - assert(pfd); - if (ffIsValidNativeFD(*pfd)) { #ifndef _WIN32 close(*pfd); @@ -235,6 +233,7 @@ typedef enum FFPathType: uint8_t { [[gnu::format(scanf, 3, 4), gnu::nonnull(1, 3)]] const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...); // Not thread safe! +// Returns the previous state, which callers routinely discard (see ffUnsuppressIO), so not `nodiscard` bool ffSuppressIO(bool suppress); static inline void ffUnsuppressIO(bool* suppressed) { @@ -247,10 +246,9 @@ static inline void ffUnsuppressIO(bool* suppressed) { #define FF_SUPPRESS_IO() [[maybe_unused, gnu::cleanup(ffUnsuppressIO)]] bool io_suppressed__ = ffSuppressIO(true) -void ffListFilesRecursively(const char* path, bool pretty); +[[gnu::nonnull(1)]] void ffListFilesRecursively(const char* path, bool pretty); [[gnu::nonnull(1), gnu::always_inline]] static inline void wrapFclose(FILE** pfile) { - assert(pfile); if (*pfile) { fclose(*pfile); } @@ -260,14 +258,12 @@ void ffListFilesRecursively(const char* path, bool pretty); [[gnu::nonnull(1), gnu::always_inline]] #ifndef _WIN32 static inline void wrapClosedir(DIR** pdir) { - assert(pdir); if (*pdir) { closedir(*pdir); } } #else static inline void wrapClosedir(HANDLE* pdir) { - assert(pdir); if (*pdir) { FindClose(*pdir); } @@ -290,7 +286,8 @@ static inline void wrapClosedir(HANDLE* pdir) { } FFNativeFD ffGetNullFD(void); -bool ffRemoveFile(const char* fileName); +// Returns whether the file was removed; callers that only want it gone discard that, so not `nodiscard` +[[gnu::nonnull(1)]] bool ffRemoveFile(const char* fileName); // Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read. // The representation is uniform across platforms so that a value derived from it means the // same thing everywhere, which matters for callers that store it as a cache key. diff --git a/src/common/jsonconfig.h b/src/common/jsonconfig.h index a78aafd160..9790739dbc 100644 --- a/src/common/jsonconfig.h +++ b/src/common/jsonconfig.h @@ -3,25 +3,30 @@ #include "common/ffdata.h" #include "common/option.h" -bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs); -const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]); +// `key` / `val` may be null when the JSON object has no such member; `moduleArgs` is always real +[[gnu::nonnull(3)]] bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs); +// `val` may be null (yyjson predicates tolerate it); `result` and `pairs` may not. +// Returns an error string, null on success, so it is `nodiscard`. +[[gnu::nonnull(2, 3), nodiscard]] const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]); -yyjson_api_inline yyjson_mut_val* yyjson_mut_strbuf(yyjson_mut_doc* doc, const FFstrbuf* buf) { +// The three helpers below are routinely called as statements, to add a key to a document, so their +// result is not `nodiscard`. +[[gnu::nonnull(1, 2)]] yyjson_api_inline yyjson_mut_val* yyjson_mut_strbuf(yyjson_mut_doc* doc, const FFstrbuf* buf) { return yyjson_mut_strncpy(doc, buf->chars, buf->length); } -yyjson_api_inline bool yyjson_mut_obj_add_strbuf(yyjson_mut_doc* doc, +[[gnu::nonnull(1, 2, 3, 4)]] yyjson_api_inline bool yyjson_mut_obj_add_strbuf(yyjson_mut_doc* doc, yyjson_mut_val* obj, const char* _key, const FFstrbuf* buf) { return yyjson_mut_obj_add_strncpy(doc, obj, _key, buf->chars, buf->length); } -yyjson_api_inline bool yyjson_mut_arr_add_strbuf(yyjson_mut_doc* doc, +[[gnu::nonnull(1, 2, 3)]] yyjson_api_inline bool yyjson_mut_arr_add_strbuf(yyjson_mut_doc* doc, yyjson_mut_val* obj, const FFstrbuf* buf) { return yyjson_mut_arr_add_strncpy(doc, obj, buf->chars, buf->length); } -void ffPrintJsonConfig(FFdata* data, bool prepare); -void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs); +[[gnu::nonnull(1)]] void ffPrintJsonConfig(FFdata* data, bool prepare); +[[gnu::nonnull(1, 2, 3)]] void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs); diff --git a/src/common/library.h b/src/common/library.h index fc5ddbe956..a1b4d772a7 100644 --- a/src/common/library.h +++ b/src/common/library.h @@ -21,8 +21,7 @@ int dlclose(void* handle); #define FF_LIBRARY_EXTENSION ".so" #endif -static inline void ffLibraryUnload(void** handle) { - assert(handle); +[[gnu::nonnull(1)]] static inline void ffLibraryUnload(void** handle) { if (*handle) { dlclose(*handle); } @@ -66,8 +65,8 @@ static inline void ffLibraryUnload(void** handle) { #define FF_LIBRARY_LOAD_SYMBOL_PTR(library, varName, symbolName, returnValue) \ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName)->ff##symbolName, symbolName, returnValue); -void* ffLibraryLoadSingle(const char* path, int maxVersion); -void* ffLibraryLoadMulti(const char* path, int maxVersion, ...); +[[gnu::nonnull(1), nodiscard]] void* ffLibraryLoadSingle(const char* path, int maxVersion); +[[gnu::nonnull(1), nodiscard]] void* ffLibraryLoadMulti(const char* path, int maxVersion, ...); #else @@ -109,10 +108,10 @@ void* ffLibraryLoadMulti(const char* path, int maxVersion, ...); #endif #if _WIN32 -void* ffLibraryGetModule(const wchar_t* libraryFileName); +[[gnu::nonnull(1), nodiscard]] void* ffLibraryGetModule(const wchar_t* libraryFileName); #endif // Return false to stop iterating, true to continue typedef bool (*FFLibraryIterateCallback)(const char* name, void* userData); // Iterate over all loaded dynamic libraries. Returns true on success, false on failure. -bool ffLibraryIterateDynamicLibs(FFLibraryIterateCallback callback, void* userData); +[[nodiscard]] bool ffLibraryIterateDynamicLibs(FFLibraryIterateCallback callback, void* userData); diff --git a/src/common/mallocHelper.h b/src/common/mallocHelper.h index e0c1829481..e3d8321ea4 100644 --- a/src/common/mallocHelper.h +++ b/src/common/mallocHelper.h @@ -15,7 +15,6 @@ [[gnu::always_inline, gnu::nonnull(1)]] static inline void ffWrapFree(const void* pPtr) { - assert(pPtr); if (*(void**) pPtr) { free(*(void**) pPtr); } @@ -24,8 +23,8 @@ static inline void ffWrapFree(const void* pPtr) { #define FF_AUTO_FREE [[gnu::cleanup(ffWrapFree)]] // ptr MUST be a malloc'ed pointer +[[gnu::nonnull(1), nodiscard]] static inline size_t ffMallocUsableSize(const void* ptr) { - assert(ptr); #if FF_HAVE_MALLOC_USABLE_SIZE return malloc_usable_size((void*) ptr); #elif FF_HAVE_MALLOC_SIZE diff --git a/src/common/networking.h b/src/common/networking.h index f7dbba422b..55949027d6 100644 --- a/src/common/networking.h +++ b/src/common/networking.h @@ -29,14 +29,16 @@ typedef struct FFNetworkingState { bool tfo; // if true, TCP Fast Open will be attempted first, and fallback to traditional connection if it fails } FFNetworkingState; -const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers); -const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer); +// `headers` is optional and may be null; the other pointer arguments are dereferenced unconditionally. +// Both functions report failure through their return value, which must be checked. +[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers); +[[gnu::nonnull(1, 2), nodiscard]] const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer); // Case-insensitive header lookup restricted to the header block [0, headerEnd). // Restricting the range matters because the body may already share the same buffer. // Returns a pointer to the first character of the value; `valueLen` receives its // length excluding the terminating CRLF. Returns nullptr when the header is absent. -const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen); +[[gnu::nonnull(1, 3, 4), gnu::pure, nodiscard]] const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen); // Checks whether a `Transfer-Encoding: chunked` body has been received in full, so that // framing does not have to rely on the server closing the connection. @@ -44,11 +46,12 @@ const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, cons // 0 when more data is needed, and -1 when the body is malformed. // The caller must not wait for a fixed amount of data (e.g. `MSG_WAITALL`) while the // response length is still unknown, otherwise this check never gets to run. -int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* consumed); +// Not `pure`: the `strtoul` it calls writes `errno`. +[[gnu::nonnull(1, 3), nodiscard]] int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* consumed); // Decodes a `Transfer-Encoding: chunked` body in place and rewrites the response with a // `Content-Length` header in place of `Transfer-Encoding`. -bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd); +[[gnu::nonnull(1), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd); // Result of parsing a `Transfer-Encoding` header value typedef enum FFNetworkingTransferEncoding { @@ -62,9 +65,9 @@ typedef enum FFNetworkingTransferEncoding { // and any other coding (e.g. `gzip, chunked`) leaves the payload encoded, which this // client cannot decode. Only a lone `chunked` is accepted; everything else is reported // as unsupported so that the caller fails the response instead of returning garbage. -FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen); +[[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen); #ifdef FF_HAVE_ZLIB const char* ffNetworkingLoadZlibLibrary(void); -bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd); +[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd); #endif diff --git a/src/common/option.h b/src/common/option.h index 0afaa449d1..ba3940a942 100644 --- a/src/common/option.h +++ b/src/common/option.h @@ -89,19 +89,25 @@ typedef struct FFKeyValuePair { int value; } FFKeyValuePair; -const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName); -void ffOptionParseString(const char* argumentKey, const char* value, FFstrbuf* buffer); -[[nodiscard]] uint32_t ffOptionParseUInt32(const char* argumentKey, const char* value); -[[nodiscard]] int32_t ffOptionParseInt32(const char* argumentKey, const char* value); -[[nodiscard]] int ffOptionParseEnum(const char* argumentKey, const char* requestedKey, FFKeyValuePair pairs[]); -[[nodiscard]] bool ffOptionParseBoolean(const char* str); -void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer); -static inline void ffOptionParseColor(const char* value, FFstrbuf* buffer) { +// `moduleName` and `argumentKey` are both dereferenced unconditionally; reads no global state, so `pure` +[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName); +// `value` is optional (null exits with a usage error), `argumentKey` and `buffer` are not +[[gnu::nonnull(1, 3)]] void ffOptionParseString(const char* argumentKey, const char* value, FFstrbuf* buffer); +[[gnu::nonnull(1), nodiscard]] uint32_t ffOptionParseUInt32(const char* argumentKey, const char* value); +[[gnu::nonnull(1), nodiscard]] int32_t ffOptionParseInt32(const char* argumentKey, const char* value); +// `requestedKey` is optional (null exits with a usage error); `pairs` is walked until its null key +[[gnu::nonnull(1, 3), nodiscard]] int ffOptionParseEnum(const char* argumentKey, const char* requestedKey, FFKeyValuePair pairs[]); +// `str` is optional: a null or empty value is reported as `true` +[[gnu::pure, nodiscard]] bool ffOptionParseBoolean(const char* str); +// `value` is optional (a null or empty value is a no-op); `buffer` is not +[[gnu::nonnull(2)]] void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer); +[[gnu::nonnull(2)]] static inline void ffOptionParseColor(const char* value, FFstrbuf* buffer) { ffStrbufClear(buffer); ffOptionParseColorNoClear(value, buffer); } -static inline void ffOptionInitModuleArg(FFModuleArgs* args, const char* icon) { +// `icon` is optional, `args` is not +[[gnu::nonnull(1)]] static inline void ffOptionInitModuleArg(FFModuleArgs* args, const char* icon) { ffStrbufInit(&args->key); ffStrbufInit(&args->keyColor); ffStrbufInitStatic(&args->keyIcon, icon); @@ -110,7 +116,7 @@ static inline void ffOptionInitModuleArg(FFModuleArgs* args, const char* icon) { args->keyWidth = 0; } -static inline void ffOptionDestroyModuleArg(FFModuleArgs* args) { +[[gnu::nonnull(1)]] static inline void ffOptionDestroyModuleArg(FFModuleArgs* args) { ffStrbufDestroy(&args->key); ffStrbufDestroy(&args->keyColor); ffStrbufDestroy(&args->keyIcon); diff --git a/src/common/parsing.h b/src/common/parsing.h index c3a4bee93a..cb64466208 100644 --- a/src/common/parsing.h +++ b/src/common/parsing.h @@ -17,8 +17,9 @@ typedef struct FFColorRangeConfig { #define FF_VERSION_INIT ((FFVersion) { 0 }) -void ffParseSemver(FFstrbuf* buffer, const FFstrbuf* major, const FFstrbuf* minor, const FFstrbuf* patch); -void ffParseGTK(FFstrbuf* buffer, const FFstrbuf* gtk2, const FFstrbuf* gtk3, const FFstrbuf* gtk4); +// Every argument is dereferenced unconditionally +[[gnu::nonnull(1, 2, 3, 4)]] void ffParseSemver(FFstrbuf* buffer, const FFstrbuf* major, const FFstrbuf* minor, const FFstrbuf* patch); +[[gnu::nonnull(1, 2, 3, 4)]] void ffParseGTK(FFstrbuf* buffer, const FFstrbuf* gtk2, const FFstrbuf* gtk3, const FFstrbuf* gtk4); -void ffVersionToPretty(const FFVersion* version, FFstrbuf* pretty); -int8_t ffVersionCompare(const FFVersion* version1, const FFVersion* version2); +[[gnu::nonnull(1, 2)]] void ffVersionToPretty(const FFVersion* version, FFstrbuf* pretty); +[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] int8_t ffVersionCompare(const FFVersion* version1, const FFVersion* version2); diff --git a/src/common/path.h b/src/common/path.h index 0d292d8689..dafcc8d5d6 100644 --- a/src/common/path.h +++ b/src/common/path.h @@ -4,8 +4,8 @@ #include "common/strutil.h" #include "fastfetch_config.h" -const char* ffFindExecutableInPath(const char* name, FFstrbuf* result); -static inline bool ffIsAbsolutePath(const char* path) { +[[gnu::nonnull(1, 2), nodiscard]] const char* ffFindExecutableInPath(const char* name, FFstrbuf* result); +[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffIsAbsolutePath(const char* path) { #ifdef _WIN32 return (ffCharIsEnglishAlphabet(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) // drive letter path || (path[0] == '\\' && path[1] == '\\'); // UNC path @@ -34,4 +34,4 @@ ssize_t readlink(const char* path, char* buf, size_t bufsiz); #define FF_PATH_PKG_BASE "/usr/pkg" #else #define FF_PATH_PKG_BASE FASTFETCH_TARGET_DIR_USR -#endif \ No newline at end of file +#endif diff --git a/src/common/percent.h b/src/common/percent.h index 5e71dc7d78..a71a62267f 100644 --- a/src/common/percent.h +++ b/src/common/percent.h @@ -30,13 +30,14 @@ typedef struct FFPercentageModuleConfig { // [yellow, green): print yellow // [0, yellow): print red -void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module); -void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module); +// `buffer` and `module` are dereferenced unconditionally; `module` carries the key/color formatting +[[gnu::nonnull(1, 4)]] void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module); +[[gnu::nonnull(1, 5)]] void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module); typedef struct yyjson_val yyjson_val; typedef struct yyjson_mut_doc yyjson_mut_doc; typedef struct yyjson_mut_val yyjson_mut_val; bool ffPercentParseCommandOptions(const char* key, const char* subkey, const char* value, FFPercentageModuleConfig* config); bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageModuleConfig* config); -void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config); +[[gnu::nonnull(1, 2)]] void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config); const char* ffPercentParseTypeJsonConfig(yyjson_val* value, FFPercentageTypeFlags* result); diff --git a/src/common/printing.h b/src/common/printing.h index 29f8c12279..fd1f0a44d4 100644 --- a/src/common/printing.h +++ b/src/common/printing.h @@ -11,10 +11,17 @@ typedef enum FFPrintType: uint8_t { FF_PRINT_TYPE_NO_CUSTOM_OUTPUT_FORMAT = 1 << 3, // reserved } FFPrintType; +// Both `moduleName` and `moduleArgs` are optional and explicitly handled: a null `moduleName` is what +// `--set-keyless` relies on, and a null `moduleArgs` (used by ffPrintError callers) skips the custom +// key entirely. So neither takes `nonnull`. void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType); +// Same for `moduleArgs`; `numArgs` / `arguments` are only read when `moduleArgs` is non-null, so +// `arguments` is not `nonnull(6)` either. At least one caller discards the result, so not `nodiscard`. bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments); #define FF_PRINT_FORMAT_CHECKED(moduleName, moduleIndex, moduleArgs, printType, arguments) \ ffPrintFormat((moduleName), (moduleIndex), (moduleArgs), (printType), (sizeof(arguments) / sizeof(*arguments)), (arguments)); -[[gnu::format(printf, 5, 6)]] void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...); -void ffPrintColor(const FFstrbuf* colorValue); +// `moduleName` / `moduleArgs` are forwarded to ffPrintLogoAndKey, which accepts null for both; +// only `message` is dereferenced here (by `vprintf`). +[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...); +[[gnu::nonnull(1)]] void ffPrintColor(const FFstrbuf* colorValue); void ffPrintCharTimes(char c, uint32_t times); diff --git a/src/common/properties.h b/src/common/properties.h index 789a757c33..4388072407 100644 --- a/src/common/properties.h +++ b/src/common/properties.h @@ -7,41 +7,45 @@ typedef struct FFpropquery { FFstrbuf* buffer; } FFpropquery; -bool ffParsePropLines(const char* lines, const char* start, FFstrbuf* buffer); -bool ffParsePropFileValues(const char* filename, uint32_t numQueries, FFpropquery* queries); -bool ffParsePropFileHomeValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries); -bool ffParsePropFileListValues(const FFlist* list, const char* relativeFile, uint32_t numQueries, FFpropquery* queries); - -bool ffParsePropLinePointer(const char** line, const char* start, FFstrbuf* buffer); - -static inline bool ffParsePropLine(const char* line, const char* start, FFstrbuf* buffer) { +// These report "was the file/property found", which callers routinely discard, so they are not +// `nodiscard`. +// +// `queries` is only walked when `numQueries > 0`, so it is intentionally not `nonnull(3)`. +[[gnu::nonnull(1, 2, 3)]] bool ffParsePropLines(const char* lines, const char* start, FFstrbuf* buffer); +[[gnu::nonnull(1)]] bool ffParsePropFileValues(const char* filename, uint32_t numQueries, FFpropquery* queries); +[[gnu::nonnull(1)]] bool ffParsePropFileHomeValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries); +[[gnu::nonnull(1, 2)]] bool ffParsePropFileListValues(const FFlist* list, const char* relativeFile, uint32_t numQueries, FFpropquery* queries); + +[[gnu::nonnull(1, 2, 3)]] bool ffParsePropLinePointer(const char** line, const char* start, FFstrbuf* buffer); + +[[gnu::nonnull(1, 2, 3)]] static inline bool ffParsePropLine(const char* line, const char* start, FFstrbuf* buffer) { return ffParsePropLinePointer(&line, start, buffer); } -static inline bool ffParsePropFile(const char* filename, const char* start, FFstrbuf* buffer) { +[[gnu::nonnull(1, 2, 3)]] static inline bool ffParsePropFile(const char* filename, const char* start, FFstrbuf* buffer) { return ffParsePropFileValues(filename, 1, (FFpropquery[]) { { start, buffer } }); } -static inline bool ffParsePropFileHome(const char* relativeFile, const char* start, FFstrbuf* buffer) { +[[gnu::nonnull(1, 2, 3)]] static inline bool ffParsePropFileHome(const char* relativeFile, const char* start, FFstrbuf* buffer) { return ffParsePropFileHomeValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } }); } -static inline bool ffParsePropFileList(const FFlist* list, const char* relativeFile, const char* start, FFstrbuf* buffer) { +[[gnu::nonnull(1, 2, 3, 4)]] static inline bool ffParsePropFileList(const FFlist* list, const char* relativeFile, const char* start, FFstrbuf* buffer) { return ffParsePropFileListValues(list, relativeFile, 1, (FFpropquery[]) { { start, buffer } }); } -static inline bool ffParsePropFileConfigValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { +[[gnu::nonnull(1)]] static inline bool ffParsePropFileConfigValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { return ffParsePropFileListValues(&instance.state.platform.configDirs, relativeFile, numQueries, queries); } -static inline bool ffParsePropFileConfig(const char* relativeFile, const char* start, FFstrbuf* buffer) { +[[gnu::nonnull(1, 2, 3)]] static inline bool ffParsePropFileConfig(const char* relativeFile, const char* start, FFstrbuf* buffer) { return ffParsePropFileConfigValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } }); } -static inline bool ffParsePropFileDataValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { +[[gnu::nonnull(1)]] static inline bool ffParsePropFileDataValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { return ffParsePropFileListValues(&instance.state.platform.dataDirs, relativeFile, numQueries, queries); } -static inline bool ffParsePropFileData(const char* relativeFile, const char* start, FFstrbuf* buffer) { +[[gnu::nonnull(1, 2, 3)]] static inline bool ffParsePropFileData(const char* relativeFile, const char* start, FFstrbuf* buffer) { return ffParsePropFileDataValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } }); } diff --git a/src/common/size.h b/src/common/size.h index 7f1da020c8..a78f3a9bf7 100644 --- a/src/common/size.h +++ b/src/common/size.h @@ -2,4 +2,4 @@ #include "fastfetch.h" -void ffSizeAppendNum(uint64_t bytes, FFstrbuf* result); +[[gnu::nonnull(2)]] void ffSizeAppendNum(uint64_t bytes, FFstrbuf* result); diff --git a/src/common/smbios.h b/src/common/smbios.h index 15c0b0ce6e..cd5418f198 100644 --- a/src/common/smbios.h +++ b/src/common/smbios.h @@ -2,8 +2,9 @@ #include "common/FFstrbuf.h" -bool ffIsSmbiosValueSet(FFstrbuf* value); -static inline void ffCleanUpSmbiosValue(FFstrbuf* value) { +// Not `pure`: it trims the trailing spaces of `value` before reporting whether it is set +[[gnu::nonnull(1), nodiscard]] bool ffIsSmbiosValueSet(FFstrbuf* value); +[[gnu::nonnull(1)]] static inline void ffCleanUpSmbiosValue(FFstrbuf* value) { if (!ffIsSmbiosValueSet(value)) { ffStrbufClear(value); } @@ -82,7 +83,8 @@ typedef struct [[gnu::packed]] FFSmbiosHeader { } FFSmbiosHeader; static_assert(sizeof(FFSmbiosHeader) == 4, "FFSmbiosHeader should be 4 bytes"); -static inline const char* ffSmbiosLocateString(const char* start, uint8_t index /* start from 1 */) { +// `start` points into the unformatted string section and is walked with `strlen`, so it may not be null +[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline const char* ffSmbiosLocateString(const char* start, uint8_t index /* start from 1 */) { if (index == 0 || *start == '\0') { return nullptr; } @@ -94,9 +96,9 @@ static inline const char* ffSmbiosLocateString(const char* start, uint8_t index typedef const FFSmbiosHeader* FFSmbiosHeaderTable[FF_SMBIOS_TYPE__MAX]; -const FFSmbiosHeader* ffSmbiosNextEntry(const FFSmbiosHeader* header); -const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable(void); +[[gnu::nonnull(1), gnu::pure, nodiscard]] const FFSmbiosHeader* ffSmbiosNextEntry(const FFSmbiosHeader* header); +[[nodiscard]] const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable(void); #ifdef __linux__ -bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer); +[[gnu::nonnull(1, 2, 3), nodiscard]] bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer); #endif diff --git a/src/common/strutil.h b/src/common/strutil.h index a9c43c4957..ee30dbc41c 100644 --- a/src/common/strutil.h +++ b/src/common/strutil.h @@ -14,6 +14,15 @@ __stdcall char* StrStrIA(const char* lpFirst, const char* lpSrch); #define FF_STR_INDIR(x) #x #define FF_STR(x) FF_STR_INDIR(x) +// Everything below is a leaf predicate over the bytes it is given: it reads memory through its +// arguments and nothing else, so `gnu::pure` is accurate. `gnu::const` is deliberately not used -- +// these are already `always_inline`, so it would buy no optimization, and it would become a lie the +// day one of them is rewritten around a lookup table. +// +// The `gnu::nonnull` indexes name the arguments that are dereferenced unconditionally. `ffStrSet` +// and `ffStrCopy` are not listed: both handle a null pointer explicitly. + +[[nodiscard, gnu::pure]] static inline bool ffStrSet(const char* str) { if (str == nullptr) { return false; @@ -26,21 +35,22 @@ static inline bool ffStrSet(const char* str) { return *str != '\0'; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrStartsWithIgnCase(const char* str, const char* compareTo) { return strncasecmp(str, compareTo, strlen(compareTo)) == 0; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrEqualsIgnCase(const char* str, const char* compareTo) { return strcasecmp(str, compareTo) == 0; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrStartsWith(const char* str, const char* compareTo) { return strncmp(str, compareTo, strlen(compareTo)) == 0; } +[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrEndsWith(const char* str, const char* compareTo) { size_t strLength = strlen(str); size_t compareToLength = strlen(compareTo); @@ -50,6 +60,7 @@ static inline bool ffStrEndsWith(const char* str, const char* compareTo) { return memcmp(str + strLength - compareToLength, compareTo, compareToLength) == 0; } +[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrEndsWithIgnCase(const char* str, const char* compareTo) { size_t strLength = strlen(str); size_t compareToLength = strlen(compareTo); @@ -59,32 +70,32 @@ static inline bool ffStrEndsWithIgnCase(const char* str, const char* compareTo) return strncasecmp(str + strLength - compareToLength, compareTo, compareToLength) == 0; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrEquals(const char* str, const char* compareTo) { return strcmp(str, compareTo) == 0; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrContains(const char* str, const char* compareTo) { return strstr(str, compareTo) != nullptr; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrContainsIgnCase(const char* str, const char* compareTo) { return strcasestr(str, compareTo) != nullptr; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrContainsC(const char* str, char compareTo) { return strchr(str, compareTo) != nullptr; } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::pure, nodiscard]] static inline bool ffCharIsEnglishAlphabet(char c) { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); } -[[gnu::always_inline]] +[[gnu::always_inline, gnu::pure, nodiscard]] static inline bool ffCharIsDigit(char c) { return '0' <= c && c <= '9'; } @@ -92,15 +103,19 @@ static inline bool ffCharIsDigit(char c) { // Parse one UTF-8 character, returning consumed byte count and display width. // Invalid / incomplete sequence falls back to one-byte width=1. // If the Unicode codepoint is non-printable, width becomes 0. +// Not `pure`: it stores the width through `width` when that is not null. +[[gnu::nonnull(1)]] uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width); +[[gnu::nonnull(1), gnu::pure, nodiscard]] uint32_t ffUtf8StrWidth(const char* str, uint32_t length); -[[gnu::always_inline]] +[[gnu::always_inline, gnu::pure, nodiscard]] static inline bool ffCharIsHexDigit(char c) { return ffCharIsDigit(c) || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F'); } +[[gnu::always_inline, gnu::pure, nodiscard]] static inline int8_t ffHexCharToInt(char c) { if (ffCharIsDigit(c)) { return (int8_t) (c - '0'); @@ -114,6 +129,9 @@ static inline int8_t ffHexCharToInt(char c) { } // Copies at most (dstBufSiz - 1) bytes from src to dst; dst is always null-terminated +// Returns a pointer to the end of the copy, which callers are free to ignore, so not `nodiscard`. +// Not `pure`: it writes through `dst`. `dst` may be null, `src` may not. +[[gnu::nonnull(2)]] static inline char* ffStrCopy(char* __restrict__ dst, const char* __restrict__ src, size_t dstBufSiz) { if (__builtin_expect(dst == nullptr, false) || dstBufSiz == 0) { return dst; diff --git a/src/common/sysctl.h b/src/common/sysctl.h index 9d12df6fcb..b40c3883c1 100644 --- a/src/common/sysctl.h +++ b/src/common/sysctl.h @@ -5,12 +5,13 @@ #include #include +// `result` is always written to, and `propName` is always read by the sysctl wrapper. #ifdef __OpenBSD__ -const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result); +[[gnu::nonnull(3)]] const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result); [[nodiscard]] int ffSysctlGetInt(int mib1, int mib2, int defaultValue); [[nodiscard]] int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue); #else -const char* ffSysctlGetString(const char* propName, FFstrbuf* result); -[[nodiscard]] int ffSysctlGetInt(const char* propName, int defaultValue); -[[nodiscard]] int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue); +[[gnu::nonnull(1, 2)]] const char* ffSysctlGetString(const char* propName, FFstrbuf* result); +[[gnu::nonnull(1), nodiscard]] int ffSysctlGetInt(const char* propName, int defaultValue); +[[gnu::nonnull(1), nodiscard]] int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue); #endif diff --git a/src/common/temps.h b/src/common/temps.h index f2f8eda89e..4ddb3f096d 100644 --- a/src/common/temps.h +++ b/src/common/temps.h @@ -3,7 +3,8 @@ #include "common/parsing.h" #include "common/option.h" -void ffTempsAppendNum(double celsius, FFstrbuf* buffer, FFColorRangeConfig config, const FFModuleArgs* module); +// `buffer` and `module` are dereferenced unconditionally; `module` carries the key/color formatting +[[gnu::nonnull(2, 4)]] void ffTempsAppendNum(double celsius, FFstrbuf* buffer, FFColorRangeConfig config, const FFModuleArgs* module); bool ffTempsParseCommandOptions(const char* key, const char* subkey, const char* value, bool* useTemp, FFColorRangeConfig* config); bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, FFColorRangeConfig* config); -void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config); +[[gnu::nonnull(1, 2)]] void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config); diff --git a/src/common/thread.h b/src/common/thread.h index c8212c2550..43b72ba436 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -11,13 +11,14 @@ #define FF_THREAD_MUTEX_INITIALIZER SRWLOCK_INIT typedef SRWLOCK FFThreadMutex; typedef HANDLE FFThreadType; -static inline void ffThreadMutexLock(FFThreadMutex* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexLock(FFThreadMutex* mutex) { AcquireSRWLockExclusive(mutex); } -static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) { ReleaseSRWLockExclusive(mutex); } -static inline FFThreadType ffThreadCreate(unsigned(__stdcall* func)(void*), void* data) { +// `data` is the thread argument and may be null; `func` may not +[[gnu::nonnull(1)]] static inline FFThreadType ffThreadCreate(unsigned(__stdcall* func)(void*), void* data) { return (FFThreadType) _beginthreadex(nullptr, 0, func, data, 0, nullptr); } #define FF_THREAD_ENTRY_DECL_WRAPPER(fn, paramType) \ @@ -56,23 +57,24 @@ typedef pthread_t FFThreadType; #include #define FF_THREAD_MUTEX_INITIALIZER OS_UNFAIR_LOCK_INIT typedef os_unfair_lock FFThreadMutex; -static inline void ffThreadMutexLock(os_unfair_lock* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexLock(os_unfair_lock* mutex) { os_unfair_lock_lock(mutex); } -static inline void ffThreadMutexUnlock(os_unfair_lock* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexUnlock(os_unfair_lock* mutex) { os_unfair_lock_unlock(mutex); } #else #define FF_THREAD_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER typedef pthread_mutex_t FFThreadMutex; -static inline void ffThreadMutexLock(FFThreadMutex* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexLock(FFThreadMutex* mutex) { pthread_mutex_lock(mutex); } -static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) { +[[gnu::nonnull(1)]] static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) { pthread_mutex_unlock(mutex); } #endif -static inline FFThreadType ffThreadCreate(void* (*func)(void*), void* data) { +// `data` is the thread argument and may be null; `func` may not +[[gnu::nonnull(1)]] static inline FFThreadType ffThreadCreate(void* (*func)(void*), void* data) { FFThreadType newThread = 0; pthread_create(&newThread, nullptr, func, data); return newThread; @@ -115,6 +117,7 @@ static inline uintptr_t ffThreadGetCurrentId() { #else // FF_HAVE_THREADS #define FF_THREAD_MUTEX_INITIALIZER 0 typedef char FFThreadMutex; +// The stubs deliberately ignore their argument, so no `nonnull` here static inline void ffThreadMutexLock([[maybe_unused]] FFThreadMutex* mutex) {} static inline void ffThreadMutexUnlock([[maybe_unused]] FFThreadMutex* mutex) {} #define FF_THREAD_ENTRY_DECL_WRAPPER(fn, paramType) diff --git a/src/common/wcwidth.h b/src/common/wcwidth.h index 8d093da520..ada6279f4e 100644 --- a/src/common/wcwidth.h +++ b/src/common/wcwidth.h @@ -3,9 +3,10 @@ #include #if FF_ENABLE_WCWIDTH -int mk_wcwidth(uint32_t wc); +// A pure table lookup: the width depends only on the code point +[[gnu::pure, nodiscard]] int mk_wcwidth(uint32_t wc); #else -static inline int mk_wcwidth(uint32_t wc) { +[[gnu::pure, nodiscard]] static inline int mk_wcwidth(uint32_t wc) { (void) wc; return 1; } From 70d2dd37a65648e5fcbdfddd385f949f853baca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 01:10:18 +0800 Subject: [PATCH 61/76] Image (Logo): uses native image decoder on Android Codec (Android): uses `availability` feature test --- CHANGELOG.md | 11 +- CMakeLists.txt | 20 +- src/common/androidApi.h | 16 ++ src/detection/codec/codec_android.c | 47 ++-- src/logo/image/aid.c | 397 ++++++++++++++++++++++++++++ src/logo/image/image.c | 24 +- src/logo/image/image.h | 7 +- 7 files changed, 480 insertions(+), 42 deletions(-) create mode 100644 src/common/androidApi.h create mode 100644 src/logo/image/aid.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed7523a70..64df0391bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # 2.69.0 Changes: -* ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo) +* ImageMagick is no longer used for image logos on Windows, macOS and Android, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS, AImageDecoder on Android). (Logo) * ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available. * It is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible. @@ -17,9 +17,9 @@ Changes: Features: * Improved image logo support * Backend rewritten - * Added a native image decoding backend on Windows (WIC) and macOS (ImageIO). - * Added an embedded libsixel encoder, used to produce sixel output on Windows and macOS. It is reported by `fastfetch --list-features` as "Embedded sixel". - * Enabled chafa image output on Windows and macOS independently of ImageMagick. + * Added a native image decoding backend on Windows (WIC), macOS (ImageIO) and Android (AImageDecoder). + * Added an embedded libsixel encoder, used to produce sixel output on Windows, macOS and Android. It is reported by `fastfetch --list-features` as "Embedded sixel". + * Enabled chafa image output on Windows, macOS and Android independently of ImageMagick. * As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal. * Image logo cache entries are now validated against the modification time of the source image. (Logo) * Editing an image logo in place now invalidates its cached rendering. @@ -28,7 +28,8 @@ Features: * `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved. * `--logo-animation-frame ` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option. * The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. - * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image. + * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, AImageDecoder on Android, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image. + * On Android an animation needs Android 12 (API 31), which is where AImageDecoder gained the ability to decode past the first frame. AImageDecoder composes the frames itself; its API does not expose a repeat count, so an animation is reported as looping forever. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image. * A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame. * Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo) * Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in. diff --git a/CMakeLists.txt b/CMakeLists.txt index 597c752e6d..c5affebc3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,10 +99,10 @@ cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON) if(ENABLE_IMAGE_LOGO) - cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF) + cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF) cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "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 and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF) - cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" 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 "WIN32 OR APPLE OR ANDROID" OFF) + cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF) endif() option(ENABLE_ZLIB "Enable zlib" ON) @@ -695,6 +695,7 @@ elseif(ANDROID) src/detection/de/de_linux.c src/detection/wmtheme/wmtheme_linux.c src/detection/camera/camera_android.c + src/logo/image/aid.c ) elseif(FreeBSD) list(APPEND LIBFASTFETCH_SRC @@ -2000,6 +2001,8 @@ elseif(GNU) elseif(ANDROID) target_link_libraries(libfastfetch PRIVATE "m" + PRIVATE jnigraphics + PRIVATE mediandk ) if(ENABLE_WORDEXP) # https://github.com/termux/termux-packages/pull/7056 @@ -2017,6 +2020,17 @@ elseif(ANDROID) endif() endif() endif() + + # Both APIs (AImageDecoder, AMediaCodec) are newer than the API level this build targets -- + # AImageDecoder is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28 -- while fastfetch + # keeps running on devices below those levels. The NDK headers mark the entry points unavailable, so + # they have to be weak references: otherwise the executable would fail to load on an older device. + # Guarding every use with __builtin_available() is what makes that safe, and turning the guard + # diagnostic into an error is what keeps a use from slipping past it. + set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/src/logo/image/aid.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" diff --git a/src/common/androidApi.h b/src/common/androidApi.h new file mode 100644 index 0000000000..32f164fd5d --- /dev/null +++ b/src/common/androidApi.h @@ -0,0 +1,16 @@ +#pragma once + +// 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_API_AT_LEAST. Unguarded, the call would jump to a null pointer. +// * Annotating a helper with FF_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_REQUIRES_API(x) [[clang::availability(android, introduced = x)]] +#define FF_API_AT_LEAST(x) __builtin_available(android x, *) diff --git a/src/detection/codec/codec_android.c b/src/detection/codec/codec_android.c index 15782bee4e..54b39b9a6b 100644 --- a/src/detection/codec/codec_android.c +++ b/src/detection/codec/codec_android.c @@ -1,9 +1,7 @@ #include "codec.h" -#include "common/library.h" +#include "common/androidApi.h" #include "common/strutil.h" -#undef __INTRODUCED_IN -#define __INTRODUCED_IN(...) #include static const struct { @@ -35,33 +33,25 @@ static bool ffCodecIsLikelySoftware(const char* codecName) { ffStrStartsWith(codecName, "OMX.PV."); } -static bool ffCodecIsHardwareAccelerated( - AMediaCodec* codec, - typeof(&AMediaCodec_getName) ffAMediaCodec_getName, - typeof(&AMediaCodec_releaseName) ffAMediaCodec_releaseName) { +// Only the name query is newer than the API level this build targets: AMediaCodec_getName and +// AMediaCodec_releaseName are API 28, while creating and deleting a codec is API 21. +FF_REQUIRES_API(28) static bool ffCodecIsHardwareAccelerated(AMediaCodec* codec) { if (!codec) { return false; } char* codecName = nullptr; - media_status_t status = ffAMediaCodec_getName(codec, &codecName); + media_status_t status = AMediaCodec_getName(codec, &codecName); if (status != AMEDIA_OK || !codecName) { return false; } bool isHardware = !ffCodecIsLikelySoftware(codecName); - ffAMediaCodec_releaseName(codec, codecName); + AMediaCodec_releaseName(codec, codecName); return isHardware; } -const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { - FF_LIBRARY_LOAD_MESSAGE(mediandk, "libmediandk.so", 0) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createDecoderByType) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createEncoderByType) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_delete) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_getName) - FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_releaseName) - +FF_REQUIRES_API(28) static const char* ffDetectCodecNativeImpl(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { FFCodecType decoders = FF_CODEC_TYPE_NONE; FFCodecType encoders = FF_CODEC_TYPE_NONE; @@ -70,22 +60,22 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o FFCodecType type = FF_CODEC_MIME_TO_TYPE[i].type; if ((options->showType & FF_CODEC_SHOW_TYPE_DECODER) && !(decoders & type)) { - AMediaCodec* decoder = ffAMediaCodec_createDecoderByType(mime); + AMediaCodec* decoder = AMediaCodec_createDecoderByType(mime); if (decoder) { - if (ffCodecIsHardwareAccelerated(decoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) { + if (ffCodecIsHardwareAccelerated(decoder)) { decoders |= type; } - ffAMediaCodec_delete(decoder); + AMediaCodec_delete(decoder); } } if ((options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && !(encoders & type)) { - AMediaCodec* encoder = ffAMediaCodec_createEncoderByType(mime); + AMediaCodec* encoder = AMediaCodec_createEncoderByType(mime); if (encoder) { - if (ffCodecIsHardwareAccelerated(encoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) { + if (ffCodecIsHardwareAccelerated(encoder)) { encoders |= type; } - ffAMediaCodec_delete(encoder); + AMediaCodec_delete(encoder); } } } @@ -100,3 +90,14 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o return nullptr; } + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { + if (FF_API_AT_LEAST(28)) { + return ffDetectCodecNativeImpl(options, result); + } + + // Reading the codec name is the only way to tell a hardware codec from a software one. Without + // it there is nothing to report, and listing the codecs as if they were accelerated would be a + // guess, so say why instead. + return "AMediaCodec_getName() requires Android 9 (API 28)"; +} diff --git a/src/logo/image/aid.c b/src/logo/image/aid.c new file mode 100644 index 0000000000..4e7ae343a2 --- /dev/null +++ b/src/logo/image/aid.c @@ -0,0 +1,397 @@ + +#include "image.h" +#include "common/androidApi.h" +#include "common/io.h" +#include "common/mallocHelper.h" + +#include +#include + +#include +#include +#include + +static inline bool androidImageDecoderError(const char** error, const char* message) { + if (error) { + *error = message; + } + return false; +} + +// AImageDecoder decodes to premultiplied alpha, and it can not be asked for straight alpha once a +// target size is in effect: AImageDecoder_setUnpremultipliedRequired fails with +// ANDROID_IMAGE_DECODER_INVALID_CONVERSION, documented as "Unpremultiplied is not possible due to +// an existing scale set by AImageDecoder_setTargetSize". Premultiplied is the right domain to +// scale in anyway -- interpolating straight alpha averages the colour of fully transparent pixels +// into the edges next to them, which shows up as dark fringes -- so the decoder is left alone and +// this runs afterwards, exactly as the ImageIO backend does. +static void unPremultiplyRGBA(uint8_t* data, size_t pixelCount) { + for (size_t i = 0; i < pixelCount; ++i) { + uint8_t* const p = data + i * 4; + const uint8_t a = p[3]; + if (a == 0) { + p[0] = p[1] = p[2] = 0; + } else if (a < 255) { + p[0] = (uint8_t) ((p[0] * 255 + a / 2) / a); + p[1] = (uint8_t) ((p[1] * 255 + a / 2) / a); + p[2] = (uint8_t) ((p[2] * 255 + a / 2) / a); + } + } +} + +// Copy one decoded frame into a tightly packed, straight alpha buffer of its own. +// +// A copy rather than a hand-over: the animation path has to keep the decoder's own buffer intact, +// because AImageDecoder blends each frame into the one that is already there. The still path only +// needs the packed result, and going through here keeps the two identical. +// +// `premultiplied` is what the decoder reports about the pixels it produced, so a source that was +// decoded straight is copied and nothing else. +static uint8_t* androidPackFrame(const uint8_t* src, size_t stride, uint32_t width, uint32_t height, bool premultiplied) { + const size_t packedStride = (size_t) width * 4; + uint8_t* pixels = (uint8_t*) malloc(packedStride * height); + if (pixels == nullptr) { + return nullptr; + } + + for (uint32_t y = 0; y < height; ++y) { + memcpy(pixels + (size_t) y * packedStride, src + (size_t) y * stride, packedStride); + } + + if (premultiplied) { + unPremultiplyRGBA(pixels, (size_t) width * height); + } + return pixels; +} + +// Everything the still and the animation path share: read the source size, resolve the requested +// pixel size, ask for RGBA8888 and let the decoder do the scaling. +FF_REQUIRES_API(30) static bool androidResolveDecoder(AImageDecoder* decoder, FFLogoRequestData* requestData, size_t* outStride, bool* outPremultiplied, const char** error) { + const AImageDecoderHeaderInfo* header = AImageDecoder_getHeaderInfo(decoder); + // Both are int32_t, and anything <= 0 is not a usable source + const int32_t sourceWidth = AImageDecoderHeaderInfo_getWidth(header); + const int32_t sourceHeight = AImageDecoderHeaderInfo_getHeight(header); + if (sourceWidth <= 0 || sourceHeight <= 0) { + return androidImageDecoderError(error, "invalid image dimensions"); + } + + // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = (uint32_t) sourceWidth; + height = (uint32_t) sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + if (width == 0 || height == 0) { + return androidImageDecoderError(error, "invalid target dimensions"); + } + + if (AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to select the RGBA8888 output format"); + } + + // A source that is already the requested size needs no scaling at all, and leaving the target + // size unset buys one more thing: AImageDecoder refuses straight alpha only *because* of a + // scale, so unscaled output can be decoded straight and no conversion follows. That is also + // the more accurate of the two, having no integer rounding to recover from. + if (width == (uint32_t) sourceWidth && height == (uint32_t) sourceHeight) { + *outPremultiplied = AImageDecoder_setUnpremultipliedRequired(decoder, true) != ANDROID_IMAGE_DECODER_SUCCESS; + } else { + // Scaling is part of the decode: the decoder samples the source down (or up) as it goes, so + // there is neither a full-size intermediate nor a separate resize step. + if (AImageDecoder_setTargetSize(decoder, (int32_t) width, (int32_t) height) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to scale the image"); + } + *outPremultiplied = true; + } + + const size_t stride = AImageDecoder_getMinimumStride(decoder); + if (stride < (size_t) width * 4 || stride > SIZE_MAX / height) { + return androidImageDecoderError(error, "invalid target dimensions"); + } + + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + *outStride = stride; + return true; +} + +bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + // AImageDecoder is API 30, and fastfetch still runs on devices below that, so this is a real + // run-time check and not a compile-time constant. See common/androidApi.h for why. + if (FF_API_AT_LEAST(30)) { + FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return androidImageDecoderError(error, "failed to open the image source"); + } + + AImageDecoder* decoder = nullptr; + if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "unsupported or unreadable image format"); + } + + size_t stride = 0; + bool premultiplied = true; + if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) { + AImageDecoder_delete(decoder); + return false; + } + + const uint32_t width = requestData->logoPixelWidth; + const uint32_t height = requestData->logoPixelHeight; + const size_t packedStride = (size_t) width * 4; + const size_t bufferSize = stride * height; + + FF_AUTO_FREE uint8_t* decoded = (uint8_t*) malloc(bufferSize); + if (decoded == nullptr) { + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + + const int result = AImageDecoder_decodeImage(decoder, decoded, stride, bufferSize); + // The decoder reads from the fd while decoding, so it outlives the decode but not the fd + AImageDecoder_delete(decoder); + if (result != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to decode the image"); + } + + // Without row padding the decoder's buffer is already laid out the way the contract wants, + // so it is finished in place and handed over rather than copied. + if (stride == packedStride) { + if (premultiplied) { + unPremultiplyRGBA(decoded, (size_t) width * height); + } + out->data = decoded; + decoded = nullptr; // Ownership is transferred to `out` + } else { + out->data = androidPackFrame(decoded, stride, width, height, premultiplied); + if (out->data == nullptr) { + return androidImageDecoderError(error, "out of memory"); + } + } + + out->width = width; + out->height = height; + return true; + } else { + return androidImageDecoderError(error, "AImageDecoder requires Android 11 (API 30) or newer"); + } +} + +// --------------------------------------------------------------------------------------------- +// Animation +// +// AImageDecoder composes the frames itself. Given the same buffer for every call, it decodes the +// part of the canvas a frame actually covers and blends it over what is already there, and it +// restores the buffer for a DISPOSE_OP_PREVIOUS frame on its own. So unlike the Windows backend, +// there is no canvas to maintain here: the session keeps the decoder's buffer and hands out a copy +// of it per frame. +// +// Two things follow from the decoder only ever walking forwards: +// +// * The frame count and every gap have to be collected up front by advancing through the whole +// animation and rewinding, which is what the session contract asks for anyway -- a negative +// --logo-animation-frame has to be resolved before the first frame is taken. +// * Reaching a frame that is not the next one means rewinding and walking forward again, which is +// slower than refusing it but keeps the iterator usable after the last frame. +// +// The loop count is the one thing AImageDecoder does not expose: it has no API for it at any +// level, so the animation is reported as looping forever, which is what an animated GIF with no +// NETSCAPE block does in every viewer anyway. +// --------------------------------------------------------------------------------------------- + +typedef struct FFAndroidAnimation { + AImageDecoder* decoder; + uint8_t* canvas; // the decoder's buffer; every frame is blended into what is already in it + size_t stride; + size_t size; + uint32_t width; + uint32_t height; + uint32_t nextIndex; // the frame the decoder is positioned on + bool premultiplied; // whether the decoder's frames need un-premultiplying + int32_t minGap; + int32_t* delaysCs; // one per frame, in centiseconds +} FFAndroidAnimation; + +FF_REQUIRES_API(31) static bool androidAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) { + FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation); + + // Frames come out of the decoder in order, so anything before the current one means starting + // over. AImageDecoder_rewind needs an animated source; it is reached here only once a frame + // has already been taken, which means the source is one. + if (index < session->nextIndex) { + if (AImageDecoder_rewind(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to rewind the animation"); + } + session->nextIndex = 0; + } + + // Skipped frames still have to be decoded: each frame is blended into the buffer that the + // following one builds on, so the buffer would be wrong without them. + while (session->nextIndex < index) { + if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to decode an animation frame"); + } + if (AImageDecoder_advanceFrame(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to advance to the next animation frame"); + } + ++session->nextIndex; + } + + if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "failed to decode the animation frame"); + } + + uint8_t* frame = androidPackFrame(session->canvas, session->stride, session->width, session->height, session->premultiplied); + if (frame == nullptr) { + return androidImageDecoderError(error, "out of memory"); + } + + // Position on the following frame. Past the last one this reports FINISHED, which is expected + // and harmless: the frame just handed out is complete, and anything else the caller asks for + // restarts above. + AImageDecoder_advanceFrame(session->decoder); + ++session->nextIndex; + + // The same mapping the ImageIO backend settled on: the raw gap is in centiseconds, a floor of + // 100 ms applies only when every frame is zero, and what is left at <= 0 means gapless. + const int32_t gap = (session->delaysCs[index] > session->minGap ? session->delaysCs[index] : session->minGap) * 10; + out->data = frame; + out->delayMs = gap > 0 ? gap : -1; + return true; +} + +FF_REQUIRES_API(31) static void androidAnimationDestroy(FFImageAnimation* animation) { + FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation); + if (session == nullptr) { + return; + } + + AImageDecoder_delete(session->decoder); + free(session->canvas); + free(session->delaysCs); + free(session); +} + +bool ffImageAnimationOpenAID(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) { + // Decoding past the first frame needs API 31: AImageDecoder_advanceFrame and + // AImageDecoderFrameInfo are both introduced there, and AImageDecoder_decodeImage only + // documents decoding "all of the frames" from that level on. + if (FF_API_AT_LEAST(31)) { + FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return androidImageDecoderError(error, "failed to open the image source"); + } + + AImageDecoder* decoder = nullptr; + if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + return androidImageDecoderError(error, "unsupported or unreadable image format"); + } + + size_t stride = 0; + bool premultiplied = true; + if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) { + AImageDecoder_delete(decoder); + return false; + } + + AImageDecoderFrameInfo* frameInfo = AImageDecoderFrameInfo_create(); + if (frameInfo == nullptr) { + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + + // Walk the animation once, recording every gap on the way, so that both the frame count + // and each frame's timing are known before the first frame is decoded. + uint32_t capacity = 16; + uint32_t frameCount = 0; + int32_t* delays = (int32_t*) malloc(capacity * sizeof(*delays)); + if (delays == nullptr) { + AImageDecoderFrameInfo_delete(frameInfo); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + + int32_t minGap = 10; + for (;;) { + if (frameCount == capacity) { + const uint32_t grown = capacity * 2; + int32_t* resized = (int32_t*) realloc(delays, grown * sizeof(*delays)); + if (resized == nullptr) { + free(delays); + AImageDecoderFrameInfo_delete(frameInfo); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + delays = resized; + capacity = grown; + } + + int64_t nanos = 0; + if (AImageDecoder_getFrameInfo(decoder, frameInfo) == ANDROID_IMAGE_DECODER_SUCCESS) { + nanos = AImageDecoderFrameInfo_getDuration(frameInfo); + } + const int32_t cs = nanos > 0 ? (int32_t) (nanos / 10000000) : 0; + delays[frameCount++] = cs; + if (cs > 0) { + minGap = 0; + } + + if (AImageDecoder_advanceFrame(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + break; + } + } + AImageDecoderFrameInfo_delete(frameInfo); + + // A single frame source never advanced, so it is still positioned on its only frame and + // has nothing to rewind. Everything else stopped in the finished state. + if (frameCount > 1 && AImageDecoder_rewind(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) { + free(delays); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "failed to rewind the animation"); + } + + FFAndroidAnimation* session = (FFAndroidAnimation*) calloc(1, sizeof(*session)); + if (session == nullptr) { + free(delays); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + session->decoder = decoder; + session->stride = stride; + session->size = stride * requestData->logoPixelHeight; + session->width = requestData->logoPixelWidth; + session->height = requestData->logoPixelHeight; + session->premultiplied = premultiplied; + session->minGap = minGap; + session->delaysCs = delays; + + session->canvas = (uint8_t*) calloc(1, session->size); + if (session->canvas == nullptr) { + free(session->delaysCs); + free(session); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + + // 0: AImageDecoder has no API for the loop count, and looping forever is what a viewer + // does with an animation that does not declare one. + FFImageAnimation* animation = ffImageAnimationCreate(frameCount, 0, session, androidAnimationGetFrame, androidAnimationDestroy); + if (animation == nullptr) { + free(session->canvas); + free(session->delaysCs); + free(session); + AImageDecoder_delete(decoder); + return androidImageDecoderError(error, "out of memory"); + } + + *out = animation; + return true; + } else { + return androidImageDecoderError(error, "animation support requires Android 12 (API 31) or newer"); + } +} diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 5bd440aa74..24ae2da166 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -447,7 +447,7 @@ static bool printImageKittyDirect(bool printError) { return true; } -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) || defined(FF_HAVE_SIXEL) #define FF_KITTY_MAX_CHUNK_SIZE 4096 @@ -807,6 +807,8 @@ bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const cha return ffImageCreateWIC(requestData, out, error); #elif defined(__APPLE__) return ffImageCreateImageIO(requestData, out, error); + #elif defined(__ANDROID__) + return ffImageCreateAID(requestData, out, error); #else #ifdef FF_HAVE_IMAGEMAGICK7 if (ffImageCreateIM7(requestData, out, error)) { @@ -879,6 +881,8 @@ bool ffImageAnimationOpen(FFLogoRequestData* requestData, FFImageAnimation** out return ffImageAnimationOpenWIC(requestData, out, error); #elif defined(__APPLE__) return ffImageAnimationOpenImageIO(requestData, out, error); + #elif defined(__ANDROID__) + return ffImageAnimationOpenAID(requestData, out, error); #elif defined(FF_HAVE_IMAGEMAGICK7) return ffImageAnimationOpenIM7(requestData, out, error); #else @@ -915,10 +919,10 @@ void ffImageAnimationClose(FFImageAnimation* animation) { } bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { - // Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded - // libsixel encoder takes over. Other platforms let ImageMagick encode straight from the - // decoded image without an RGBA round trip. - #if defined(_WIN32) || defined(__APPLE__) + // Windows (WIC), macOS (ImageIO) and Android (AImageDecoder) decode and resize to RGBA first, + // then the embedded libsixel encoder takes over. Other platforms let ImageMagick encode + // straight from the decoded image without an RGBA round trip. + #if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) #ifdef FF_HAVE_SIXEL FFImageBuffer buffer = {}; if (!ffImageCreate(requestData, &buffer, error)) { @@ -950,10 +954,10 @@ bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const cha } bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) { - // Windows and macOS hand the pixels to the embedded encoder. ImageMagick has no way to encode - // pixels it was not given an Image for, so its SIXEL coder is reached through a ConstituteImage - // round trip instead. - #if defined(_WIN32) || defined(__APPLE__) + // Windows, macOS and Android hand the pixels to the embedded encoder. ImageMagick has no way to + // encode pixels it was not given an Image for, so its SIXEL coder is reached through a + // ConstituteImage round trip instead. + #if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) #ifdef FF_HAVE_SIXEL return ffSixelEncode(buffer, out, error); #else @@ -1780,7 +1784,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { } #endif -#if !defined(_WIN32) && !defined(__APPLE__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) if (printError) { fputs("Logo: Image Magick support is not compiled in\n", stderr); } diff --git a/src/logo/image/image.h b/src/logo/image/image.h index 25a6f4e9fa..e8bedf0a86 100644 --- a/src/logo/image/image.h +++ b/src/logo/image/image.h @@ -2,7 +2,7 @@ #include "../logo.h" -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) || defined(FF_HAVE_SIXEL) typedef enum FFLogoImageResult: uint8_t { FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed @@ -119,6 +119,11 @@ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, co bool ffImageAnimationOpenImageIO(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); #endif +#ifdef __ANDROID__ +bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageAnimationOpenAID(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error); +#endif + #ifdef FF_HAVE_SIXEL bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error); #endif From 560a9f419dea85a7d03cab71f8493fd677c41777 Mon Sep 17 00:00:00 2001 From: Ian Ribeiro Date: Fri, 18 Sep 2026 00:42:19 -0300 Subject: [PATCH 62/76] WM (Linux): supports Umbriel version detection (#2594) --- CHANGELOG.md | 1 + src/detection/wm/wm_linux.c | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64df0391bc..d18d3abb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Features: * Improved reliability of fastfetch's built-in HTTP client. (PublicIP, Weather) * It now supports custom ports and can properly handle chunked transfer encoding. * It is designed for minimal resource usage and fast performance. It does not support full HTTP features like HTTPS. Users can always use the `Command` module with `curl` to achieve similar functionality. +* Added Umbriel wayland compositor version detection (WM, Linux) Bugfixes: * Fixed Base64 encoding producing incorrect output for some inputs. (General) diff --git a/src/detection/wm/wm_linux.c b/src/detection/wm/wm_linux.c index a22a513fb4..b04bececad 100644 --- a/src/detection/wm/wm_linux.c +++ b/src/detection/wm/wm_linux.c @@ -168,6 +168,17 @@ static const char* getNiri(FFstrbuf* result) { return "Failed to run command `niri --version`"; } +static const char* getUmbriel(FFstrbuf* result) { + if (ffProcessAppendStdOut(result, (char* const[]) { "umbriel", "--version", nullptr }) == nullptr) { // umbriel 0.1.0 (7a448abe550e) + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufSubstrBeforeLastC(result, '('); + ffStrbufTrimRightSpace(result); + return nullptr; + } + + return "Failed to run command `umbriel --version`"; +} + static const char* getWeston(FFstrbuf* result) { FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); const char* error = ffFindExecutableInPath("weston", &path); @@ -327,6 +338,10 @@ const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_ return getNiri(result); } + if (ffStrbufEqualS(wmName, "umbriel")) { + return getUmbriel(result); + } + if (ffStrbufEqualS(wmName, "weston")) { return getWeston(result); } From 7baaae166f5f2db14b96481d182d6cd9f6933344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 08:16:45 +0800 Subject: [PATCH 63/76] DE (Android): improves Android ROM detection Fixes #2541 --- CHANGELOG.md | 5 + .../displayserver/displayserver_android.c | 311 ++++++++++++++++-- 2 files changed, 291 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d18d3abb32..796a8e2fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,11 @@ Features: * Added CPU name and frequency detection support on SPARC. (CPU, Linux) * Added package detection support for CRUX. (Packages, Linux) * Exposed in custom format as `{crux}`. +* Improved Android ROM detection (DE, Android) + * Added support for HarmonyOS, HarmonyOS NEXT, Flyme, JOYUI, SmartisanOS, realme UI, HydrogenOS, ZUI, ZUXOS, MyOS, NebulaAIOS, ObricUI, MiFavor, LineageOS, PixelExperience, EUI and 360 UI. + * Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string. + * Added Samsung OneUI support (#2541) + * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index 4e666eb1d3..6adb71c033 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,4 +1,5 @@ #include "displayserver.h" +#include "common/arrutil.h" #include "common/settings.h" #include "common/processing.h" #include "linux/displayserver_linux.h" @@ -135,56 +136,316 @@ static bool detectWithGetprop(FFDisplayServerResult* ds) { return false; } +// Several vendors embed the UI name and its version in `ro.build.display.id` without any +// separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`. +static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) { + FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate(); + if (!ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) { + return false; + } + + for (uint32_t i = 0; i < count; i++) { + uint32_t length = (uint32_t) strlen(names[i]); + if (!ffStrbufStartsWithS(&displayId, names[i])) { + continue; + } + + ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the model suffix + if (displayId.length > length) { + ffStrbufInsertNC(&displayId, length, 1, ' '); + } + ffStrbufSet(&ds->dePrettyName, &displayId); + return true; + } + + return false; +} + static bool detectDE(FFDisplayServerResult* ds) { - if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) // OriginOS 6 - { - ffStrbufAppendC(&ds->dePrettyName, ' '); - ffSettingsGetAndroidProperty("ro.vivo.product.version", &ds->dePrettyName); // PD2505D_xxx + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY brand = ffStrbufCreate(); + + // `ffSettingsGetAndroidProperty` appends to the given buffer, so the brand has to be read + // into its own buffer exactly once: reading it into a shared buffer repeatedly would + // concatenate the values and break every comparison against it. + ffSettingsGetAndroidProperty("ro.product.brand", &brand); + + // vivo reports the marketing name and version in `ro.vivo.os.build.display.id`, + // separated by an underscore (`Funtouch OS_10`, `OriginOS 5`), and the build number + // in `ro.vivo.product.version` (`PD2505D_A_9.16.42`). + if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) { + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + if (ffSettingsGetAndroidProperty("ro.vivo.product.version", &buffer)) { + ffStrbufAppendC(&ds->dePrettyName, ' '); + ffStrbufAppend(&ds->dePrettyName, &buffer); + } return true; } - if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName) || - ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) { + + // HarmonyOS 2.0 - 4.x is built on top of the Android framework and keeps reporting an + // EMUI version, but it is HarmonyOS. `ro.build.ohos.devicetype` is set only by those + // builds, and `hw_sc.build.platform.version` is the HarmonyOS version, while + // `ro.build.version.emui` only carries the EMUI compatibility version + // (HarmonyOS 2.0 == EMUI 12, 3.0 == 13, 4.0 == 14, 4.2 == 14.2, 4.3 == 15). + if (ffSettingsGetAndroidProperty("ro.build.ohos.devicetype", &buffer)) { + ffStrbufClear(&buffer); + if (!ffSettingsGetAndroidProperty("hw_sc.build.platform.version", &buffer) && + ffSettingsGetAndroidProperty("ro.huawei.build.display.id", &buffer)) { + // A few builds leave `hw_sc.build.platform.version` unset and only expose the + // version through `ro.huawei.build.display.id`, e.g. `JKM-AL00 2.0.0.263(C00E260R4P3)` + ffStrbufSubstrAfterFirstC(&buffer, ' '); + ffStrbufSubstrBeforeFirstC(&buffer, '('); + ffStrbufSubstrBeforeLastC(&buffer, '.'); + } + if (buffer.length > 0) { + ffStrbufSetF(&ds->dePrettyName, "HarmonyOS %s", buffer.chars); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS"); + } + return true; + } + + // HarmonyOS NEXT (5.0 and newer) drops the Android framework. It is the only Huawei + // family reporting `ro.build.display.id` as `System 104.5.0.001(60J9)`, and none of its + // properties carries the marketing version, so it can only be named without one. + if (ffStrbufIgnCaseEqualS(&brand, "HUAWEI") && + ffSettingsGetAndroidProperty("ro.build.display.id", &buffer) && + ffStrbufStartsWithS(&buffer, "System ")) { + ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS NEXT"); + return true; + } + + // HONOR reports MagicOS / MagicUI in `ro.build.version.magic`. MagicUI 3.x stores a + // bare version number (`3.0.1`) instead of a `MagicUI_x.y.z` string. + if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName)) { ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + if (!ffStrbufStartsWithS(&ds->dePrettyName, "Magic")) { + ffStrbufPrependS(&ds->dePrettyName, "MagicUI "); + } return true; } + + if (ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) { + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + return true; + } + + // Xiaomi HyperOS. `ro.mi.os.version.incremental` is `OS1.0.10.0.TLDCNXM`, while + // `ro.build.version.incremental` still starts with `V816` on HyperOS 1.0, so it can + // not be used to tell HyperOS and MIUI apart. if (ffSettingsGetAndroidProperty("ro.mi.os.version.name", &ds->dePrettyName)) { - // MiUI like ffStrbufClear(&ds->dePrettyName); - ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName); // Detail version number - if (ffStrbufStartsWithS(&ds->dePrettyName, "OS")) { - ds->dePrettyName.chars[0] = 'S'; - ds->dePrettyName.chars[1] = ' '; - ffStrbufPrependS(&ds->dePrettyName, "HyperO"); - } else if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { - ds->dePrettyName.chars[0] = ' '; - ffStrbufPrependS(&ds->dePrettyName, "MiUI"); + if (ffSettingsGetAndroidProperty("ro.mi.os.version.incremental", &ds->dePrettyName) && + ffStrbufStartsWithS(&ds->dePrettyName, "OS")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 1); // Drop the leading "OS" + ffStrbufPrependS(&ds->dePrettyName, "HyperOS "); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "HyperOS"); + } + return true; + } + + // Black Shark runs JOYUI, a MIUI fork, and therefore also sets + // `ro.miui.ui.version.name`. `ro.build.version.incremental` is `V11.0.4.0.JOYUI`. + if (ffStrbufIgnCaseEqualS(&brand, "blackshark")) { + ffStrbufClear(&ds->dePrettyName); + if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { + ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '.'); // Drop the trailing "JOYUI" + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "JOYUI "); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "JOYUI"); + } + return true; + } + + // MIUI. `ro.build.version.incremental` is `V14.0.1.0.TJJCNXM` on stable builds and a + // bare release date (`21.11.17`) on beta builds. + if (ffSettingsGetAndroidProperty("ro.miui.ui.version.name", &buffer)) { + ffStrbufClear(&ds->dePrettyName); + if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "MiUI "); } else { ffStrbufSetStatic(&ds->dePrettyName, "MiUI"); } return true; } - if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName)) { + + // realme UI is a ColorOS fork and reports both `ro.build.version.realmeui` and + // `ro.build.version.oplusrom`; the former is the realme UI version. + if (ffSettingsGetAndroidProperty("ro.build.version.realmeui", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "realme UI "); + return true; + } + + // ColorOS 12 and newer report `ro.build.version.oplusrom`, ColorOS 11 and older + // report `ro.build.version.opporom`. + if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName) || + ffSettingsGetAndroidProperty("ro.build.version.opporom", &ds->dePrettyName)) { if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { ffStrbufSubstrAfter(&ds->dePrettyName, 0); } - ffStrbufPrependS(&ds->dePrettyName, "ColorOS"); + ffStrbufPrependS(&ds->dePrettyName, "ColorOS "); return true; } + if (ffSettingsGetAndroidProperty("ro.oxygen.version", &ds->dePrettyName)) { - ffStrbufPrependS(&ds->dePrettyName, "OxygenOS"); + ffStrbufPrependS(&ds->dePrettyName, "OxygenOS "); return true; } - if (ffSettingsGetAndroidProperty("ro.product.brand", &ds->dePrettyName) && ffStrbufEqualS(&ds->dePrettyName, "asus")) { - ffStrbufClear(&ds->dePrettyName); - ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName); + + // HydrogenOS is the Chinese counterpart of OxygenOS and reports `ro.rom.version`. + if (ffStrbufEqualS(&brand, "OnePlus") && + ffSettingsGetAndroidProperty("ro.rom.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "H2OS "); return true; } - if (ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName)) { - if (ffStrbufStartsWithS(&ds->dePrettyName, "RedMagicOS")) { - ffStrbufInsertNC(&ds->dePrettyName, strlen("RedMagicOS"), 1, ' '); + + if (ffSettingsGetAndroidProperty("ro.build.version.oneui", &ds->dePrettyName)) { + // [ro.build.version.oneui]: [50101] => One UI 5.1.1 + // Samsung encodes the version with a base-100 carry: `major * 10000 + minor * 100 + patch` + uint32_t version = (uint32_t) ffStrbufToUInt(&ds->dePrettyName, 0); + uint32_t major = version / 10000; + uint32_t minor = version / 100 % 100; + uint32_t patch = version % 100; + if (major == 0) { + // Unexpected format, show the raw value + ffStrbufPrependS(&ds->dePrettyName, "OneUI "); + } else if (patch > 0) { + ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u.%u", major, minor, patch); + } else { + ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u", major, minor); + } + return true; + } + + // Flyme. `ro.build.display.id` is `Flyme 10.5.0.1A`, where the trailing `A` marks a + // stable release. `ro.flyme.version.id` holds the same string, except on Flyme 12, + // where it is an Android build id instead. + if (ffStrbufIgnCaseEqualS(&brand, "meizu")) { + if (!ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName) || + !ffStrbufStartsWithS(&ds->dePrettyName, "Flyme")) { + ffStrbufSetStatic(&ds->dePrettyName, "Flyme"); } + return true; + } - // Google Pixel uses native Android + // SmartisanOS. `ro.smartisan.version` is `4.2.6-201808311713-user-511` or + // `6.6.6.2_TNT-201904101033-user-oce`. + if (ffSettingsGetAndroidProperty("ro.smartisan.version", &ds->dePrettyName)) { + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '_'); + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufPrependS(&ds->dePrettyName, "SmartisanOS "); + return true; + } + + // ZUI / ZUXOS. `ro.com.zui.version` is the internal ZUI version (`17.0` for + // ZUXOS 1.1.10.138), while `ro.build.display.id` embeds the marketing name and + // version: `TB321FU_CN_OPEN_USER_Q00011.0_V_ZUXOS_1.1.10.138_ST_250626`. + if (ffSettingsGetAndroidProperty("ro.com.zui.version", &ds->dePrettyName)) { + FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate(); + const char* name = nullptr; + if (ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) { + if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUXOS_")) { + name = "ZUXOS"; + } else if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUI_")) { + name = "ZUI"; + } + if (name) { + ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the trailing "_ST_250626" + ffStrbufSetF(&ds->dePrettyName, "%s %s", name, displayId.chars); + return true; + } + } + ffStrbufPrependS(&ds->dePrettyName, "ZUI "); // Moto builds only expose `ro.com.zui.version` + return true; + } + + // 360 OS (QiKU) reports `ro.build.uiversion` as `360UI:V3.0`. + if (ffSettingsGetAndroidProperty("ro.build.uiversion", &ds->dePrettyName)) { + uint32_t index = ffStrbufFirstIndexC(&ds->dePrettyName, ':'); + if (index < ds->dePrettyName.length) { + // `360UI:V3.0` -> `360UI 3.0` + ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1); + if (ds->dePrettyName.chars[index] == 'V') { + ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1); + } + ffStrbufInsertNC(&ds->dePrettyName, index, 1, ' '); + } + return true; + } + + // LeEco EUI reports `ro.letv.release.version` as `6.0.030S`, where the trailing `S` + // marks a stable release. + if (ffSettingsGetAndroidProperty("ro.letv.release.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "EUI "); + return true; + } + + // nubia / ZTE + if (ffStrbufIgnCaseEqualS(&brand, "nubia") || ffStrbufIgnCaseEqualS(&brand, "zte")) { + // ObricUI reports `ro.os.ota.version`, e.g. + // `1.8.0.2-20260204-125753-RELEASE-user-pacific-b911` + if (ffSettingsGetAndroidProperty("ro.os.ota.version", &ds->dePrettyName)) { + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufPrependS(&ds->dePrettyName, "ObricUI "); + return true; + } + + // MyOS, NebulaAIOS and RedMagicOS embed the name and version in `ro.build.display.id` + static const char* const displayIdNames[] = { "RedMagicOS", "NebulaAIOS", "MyOS" }; + if (detectDEFromDisplayId(ds, displayIdNames, ARRAY_SIZE(displayIdNames))) { + return true; + } + + // The older nubiaUI stores them in `ro.build.nubia.rom.name` and `.code` + if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.name", &ds->dePrettyName)) { + if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.code", &buffer)) { + if (ffStrbufStartsWithS(&buffer, "V")) { + ffStrbufSubstrAfter(&buffer, 0); // `V1.0` -> `1.0` + } + ffStrbufAppendC(&ds->dePrettyName, ' '); + ffStrbufAppend(&ds->dePrettyName, &buffer); + } + return true; + } + + // MiFavor is the UI of the pre-MyOS ZTE phones; `ro.build.MiFavor_version` is a + // bare version number (`4.0`). Note that MyOS, NebulaAIOS and RedMagicOS reuse + // this property for their own version, so they are checked first. + if (ffSettingsGetAndroidProperty("ro.build.MiFavor_version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "MiFavor "); + return true; + } + } + + // LineageOS and other AOSP-based distributions + if (ffSettingsGetAndroidProperty("ro.lineage.build.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "LineageOS "); + return true; + } + if (ffSettingsGetAndroidProperty("org.pixelexperience.version.display", &ds->dePrettyName)) { + // PixelExperience_Plus_whyred-13.0-20230325-0421-OFFICIAL + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '_'); + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + return true; + } + + if (ffStrbufEqualS(&brand, "asus") && + ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { + return true; + } + if (ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName)) { + // Google Pixel and other devices running native Android return true; } From 098b501d0e1fbbe1c352625ba50e57db62fa07f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 09:49:32 +0800 Subject: [PATCH 64/76] Camera (Android): uses NDK instead of the unstable `termux-api` --- CHANGELOG.md | 2 + CMakeLists.txt | 1 + src/detection/camera/camera_android.c | 124 ++++++++++++++++++-------- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796a8e2fe9..b7dc5c55b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ Features: * Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string. * Added Samsung OneUI support (#2541) * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. +* Improved Camera detection on Android (Camera, Android) + * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/CMakeLists.txt b/CMakeLists.txt index c5affebc3c..fda52041c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2003,6 +2003,7 @@ elseif(ANDROID) PRIVATE "m" PRIVATE jnigraphics PRIVATE mediandk + PRIVATE camera2ndk ) if(ENABLE_WORDEXP) # https://github.com/termux/termux-packages/pull/7056 diff --git a/src/detection/camera/camera_android.c b/src/detection/camera/camera_android.c index 9a82668d6c..5b3b1ccfd8 100644 --- a/src/detection/camera/camera_android.c +++ b/src/detection/camera/camera_android.c @@ -1,58 +1,108 @@ #include "camera.h" -#include "common/processing.h" -#include "common/properties.h" +#include +#include +#include -#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" -#define FF_TERMUX_API_PARAM "CameraInfo" +// HAL_PIXEL_FORMAT_JPEG, the still capture format the stream configurations are keyed by. It is +// defined in , which the NDK does not ship, and shares its value with +// AHardwareBuffer's BLOB format. +#define FF_ANDROID_PIXEL_FORMAT_JPEG 0x21 -static inline void wrapYyjsonFree(yyjson_doc** doc) { - assert(doc); - if (*doc) { - yyjson_doc_free(*doc); +// Records the largest size the camera can stream in the given pixel format. The stream configurations +// are a flat array of (format, width, height, isInput) quads; a negative format matches any of them. +static void ffCameraMaxStreamSize(const ACameraMetadata* metadata, int32_t format, uint32_t* width, uint32_t* height) { + ACameraMetadata_const_entry entry; + if (ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry) != ACAMERA_OK) { + return; + } + if (entry.type != ACAMERA_TYPE_INT32) { + return; } -} -const char* ffDetectCamera([[maybe_unused]] FFlist* result) { - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + uint64_t maxArea = 0; + for (uint32_t i = 0; i + 3 < entry.count; i += 4) { + if (entry.data.i32[i + 3] != ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_OUTPUT) { + continue; + } + if (format >= 0 && entry.data.i32[i] != format) { + continue; + } - if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, nullptr })) { - return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; + uint32_t w = (uint32_t) entry.data.i32[i + 1]; + uint32_t h = (uint32_t) entry.data.i32[i + 2]; + if ((uint64_t) w * h > maxArea) { + maxArea = (uint64_t) w * h; + *width = w; + *height = h; + } } +} - [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(buffer.chars, buffer.length, 0, nullptr, nullptr); - if (!doc) { - return "Failed to parse camera info"; +const char* ffDetectCamera(FFlist* result) { + // The camera2 NDK and every entry point below were introduced in API 24, which is the API level + // this build targets, so nothing here is newer than the minimum supported version and + // common/androidApi.h has nothing to guard. That header covers the opposite case: entry points + // the NDK marks unavailable because they postdate the target, such as AImageDecoder (30) or + // AMediaCodec_getName (28). It also only ever makes *symbols* weak -- libcamera2ndk.so itself is + // API 24, so linking it unconditionally in CMakeLists.txt is fine on every supported device. + ACameraManager* manager = ACameraManager_create(); + if (!manager) { + return "ACameraManager_create() failed"; } - yyjson_val* root = yyjson_doc_get_root(doc); - if (!yyjson_is_arr(root)) { - return "Camera info result is not a JSON array"; + ACameraIdList* idList = nullptr; + if (ACameraManager_getCameraIdList(manager, &idList) != ACAMERA_OK || !idList) { + ACameraManager_delete(manager); + return "ACameraManager_getCameraIdList() failed"; } - yyjson_val* device; - size_t idx, max; - yyjson_arr_foreach (root, idx, max, device) { - FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); - { - const char* facing = yyjson_get_str(yyjson_obj_get(device, "facing")); - if (facing) { - ffStrbufInitF(&camera->name, "builtin-%s", facing); - } else { - ffStrbufInitStatic(&camera->name, "Unknown"); - } + for (int32_t i = 0; i < idList->numCameras; ++i) { + const char* id = idList->cameraIds[i]; + + ACameraMetadata* metadata = nullptr; + if (ACameraManager_getCameraCharacteristics(manager, id, &metadata) != ACAMERA_OK || !metadata) { + continue; } + + FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); ffStrbufInit(&camera->vendor); - ffStrbufInitJsonVal(&camera->id, yyjson_obj_get(device, "id")); - yyjson_val* sizes = yyjson_arr_get_first(yyjson_obj_get(device, "jpeg_output_sizes")); - if (yyjson_is_obj(sizes)) { - camera->width = (uint32_t) yyjson_get_uint(yyjson_obj_get(sizes, "width")); - camera->height = (uint32_t) yyjson_get_uint(yyjson_obj_get(sizes, "height")); + ffStrbufInit(&camera->colorspace); + ffStrbufInitS(&camera->id, id); + + ACameraMetadata_const_entry facing; + if (ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &facing) == ACAMERA_OK && facing.type == ACAMERA_TYPE_BYTE && facing.count >= 1) { + switch (facing.data.u8[0]) { + case ACAMERA_LENS_FACING_FRONT: + ffStrbufInitStatic(&camera->name, "builtin-front"); + break; + case ACAMERA_LENS_FACING_BACK: + ffStrbufInitStatic(&camera->name, "builtin-back"); + break; + case ACAMERA_LENS_FACING_EXTERNAL: + ffStrbufInitStatic(&camera->name, "builtin-external"); + break; + default: + ffStrbufInitStatic(&camera->name, "Unknown"); + break; + } } else { - camera->width = camera->height = 0; + ffStrbufInitStatic(&camera->name, "Unknown"); } - ffStrbufInit(&camera->colorspace); + + camera->width = camera->height = 0; + ffCameraMaxStreamSize(metadata, FF_ANDROID_PIXEL_FORMAT_JPEG, &camera->width, &camera->height); + if (camera->width == 0) { + // Not every camera HAL lists a JPEG format. The largest size of any output format is the + // same resolution on those devices. + ffCameraMaxStreamSize(metadata, -1, &camera->width, &camera->height); + } + + ACameraMetadata_free(metadata); } + ACameraManager_deleteCameraIdList(idList); + ACameraManager_delete(manager); + return nullptr; } From 811e634dc800c3f0977d6b3610c6abf5b1b4ac9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 11:10:55 +0800 Subject: [PATCH 65/76] Display (Android): adds real support via `/system/bin/cmd` --- CHANGELOG.md | 2 + src/common/impl/processing_linux.c | 8 +- src/common/impl/processing_windows.c | 5 +- src/common/processing.h | 13 +- src/detection/command/command.c | 1 + .../displayserver/displayserver_android.c | 162 ++++++++++-------- 6 files changed, 112 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7dc5c55b0..a85292783b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ Features: * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved Camera detection on Android (Camera, Android) * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. +* Improved Display detection on Android when fastfetch runs as an app rather than from `adb shell`, where `dumpsys display` is not permitted. (Display, Android) + * The displays are now read through the shell command interface of the display service, which needs no permission. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c index 5783f880fc..604094df38 100644 --- a/src/common/impl/processing_linux.c +++ b/src/common/impl/processing_linux.c @@ -56,7 +56,7 @@ static inline int ffPipe2(int* fds, int flags) { } // Not thread-safe -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { int pipes[2]; if (ffPipe2(pipes, O_CLOEXEC) == -1) { return "pipe() failed"; @@ -77,6 +77,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* posix_spawn_file_actions_init(&file_actions); posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + if (ffIsValidNativeFD(stdinFd)) { + posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO); + } static char* oldLang = nullptr; static int langIndex = -1; @@ -142,6 +145,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* // Child process dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + if (ffIsValidNativeFD(stdinFd)) { + dup2(stdinFd, STDIN_FILENO); + } putenv("LANG=C.UTF-8"); execvp(argv[0], argv); _exit(127); diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c index e90195dfe3..5ddcdc2367 100644 --- a/src/common/impl/processing_windows.c +++ b/src/common/impl/processing_windows.c @@ -83,7 +83,7 @@ static wchar_t* createChildEnvironment(void) { return result; } -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { const int32_t timeout = instance.config.general.processingTimeout; wchar_t pipeName[32]; @@ -131,6 +131,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* siStartInfo.hStdOutput = hChildPipeWrite; siStartInfo.hStdError = ffGetNullFD(); } + if (ffIsValidNativeFD(stdinFd)) { + siStartInfo.hStdInput = stdinFd; + } FF_AUTO_FREE wchar_t* cmdline = nullptr; { diff --git a/src/common/processing.h b/src/common/processing.h index 51105c8baf..b52070fdfe 100644 --- a/src/common/processing.h +++ b/src/common/processing.h @@ -1,6 +1,7 @@ #pragma once #include "common/FFstrbuf.h" +#include "common/io.h" // FFNativeFD, ffGetNullFD #ifndef _WIN32 #include // pid_t @@ -16,12 +17,18 @@ typedef struct FFProcessHandle { #endif } FFProcessHandle; -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle); +// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from +// our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do. +// Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the +// service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then +// only reports `Failure calling service : Failed transaction`. +#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1) +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle); const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, false, &handle); + const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } @@ -35,7 +42,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, true, &handle); + const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } diff --git a/src/detection/command/command.c b/src/detection/command/command.c index 5be3d8aea8..ef0095a576 100644 --- a/src/detection/command/command.c +++ b/src/detection/command/command.c @@ -22,6 +22,7 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand nullptr } : (char* const[]) { options->shell.chars, options->text.chars, nullptr }, options->useStdErr, + FF_PROCESS_INHERIT_STDIN, handle); } diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index 6adb71c033..f3c4b5f594 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,6 +1,7 @@ #include "displayserver.h" #include "common/arrutil.h" #include "common/settings.h" +#include "common/strutil.h" #include "common/processing.h" #include "linux/displayserver_linux.h" @@ -29,111 +30,126 @@ static bool checkHdrStatus(FFDisplayResult* display) { return false; } -static void detectWithDumpsys(FFDisplayServerResult* ds) { +static void detectWithCmd(FFDisplayServerResult* ds) { + // Unlike `dumpsys`, the shell command interface of the same service is not permission gated + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); - if (ffProcessAppendStdOut(&buf, (char*[]) { - "/system/bin/dumpsys", - "display", - nullptr, - }) != nullptr || - buf.length == 0) { - return; // Only works in `adb shell`, or when rooted + FFProcessHandle handle; + // `cmd` forwards its stdin to the service over binder, and the kernel rejects the whole + // transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal. + // Detaching the child from our stdin is only needed here, so the low level API is called + // instead of `ffProcessAppendStdOut`. + if (ffProcessSpawn((char*[]) { + "/system/bin/cmd", + "display", + "get-displays", + nullptr, + }, + false, + ffGetNullFD(), + &handle) != nullptr) { + return; // The shell command interface is not available on every Android version + } + + if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) { + return; } + ffStrbufTrimRightSpace(&buf); uint32_t index = 0; - while ((index = ffStrbufNextIndexS(&buf, index, "DisplayDeviceInfo")) < buf.length) { - index += strlen("DisplayDeviceInfo"); + while ((index = ffStrbufNextIndexS(&buf, index, "Display id ")) < buf.length) { + index += strlen("Display id "); + uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n'); buf.chars[nextIndex] = '\0'; const char* info = buf.chars + index; - // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2, defaultModeId 1, supportedModes [{id=1, width=1440, height=3200, fps=60.000004, alternativeRefreshRates=[24.000002, 30.000002, 40.0, 120.00001, 120.00001, 120.00001, 120.00001, 120.00001]}, + // 0: DisplayInfo{"Builtin display", displayId 0, ..., real 1440 x 3168, ..., mode 2, + // renderFrameRate 60.000004, ..., supportedModes [{id=2, width=1440, height=3168, + // fps=60.000004, ...}], ..., type INTERNAL, ..., density 560 (560.0 x 560.0) dpi, ...} + const char* field = strstr(info, "DisplayInfo{\""); FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64); - unsigned width = 0, height = 0, modeId = 0; + if (field) { + field += strlen("DisplayInfo{\""); + const char* nameEnd = strchr(field, '"'); + if (nameEnd) { + ffStrbufAppendNS(&name, (uint32_t) (nameEnd - field), field); + } + } + + unsigned width = 0, height = 0; + if ((field = strstr(info, ", real ")) && sscanf(field, ", real %u x %u", &width, &height) < 2) { + width = height = 0; + } + double refreshRate = 0; - // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2 - int res = sscanf(info, "{\"%63[^\"]\":%*s%u x %u, modeId%u", name.chars, &width, &height, &modeId); - if (res >= 3) { - if (res == 4) { - ++info; // skip first '{' - while ((info = strchr(info, '{'))) { - ++info; - - unsigned id; - double fps; - // id=1, width=1440, height=3200, fps=60.000004, - if (sscanf(info, "id=%u, %*s%*s fps=%lf", &id, &fps) >= 2) { - if (id == modeId) { - refreshRate = fps; - break; - } - } else { + if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &refreshRate) < 1) { + refreshRate = 0; + } + if (refreshRate <= 0) { + // `renderFrameRate` is only printed since Android 11. Older builds expose the active mode + // only, so its refresh rate has to be looked up in the list of supported modes. + unsigned activeMode = 0; + field = strstr(info, ", mode "); + if (field && sscanf(field, ", mode %u", &activeMode) >= 1) { + field = strstr(info, "supportedModes ["); + while (field && (field = strstr(field, "{id="))) { + // {id=2, width=1440, height=3168, fps=60.000004, ... + unsigned id = 0; + double fps = 0; + if (sscanf(field, "{id=%u, width=%*u, height=%*u, fps=%lf", &id, &fps) < 2) { + break; + } + if (id == activeMode) { + refreshRate = fps; break; } + ++field; } } + } - ffStrbufRecalculateLength(&name); - FFDisplayResult* display = ffdsAppendDisplay(ds, - (uint32_t) width, - (uint32_t) height, - refreshRate, - 0, - 0, - 0, - 0, - 0, - &name, - FF_DISPLAY_TYPE_UNKNOWN, - false, - 0, - 0, - 0, - "dumpsys"); - if (display) { - display->hdrStatus = checkHdrStatus(display); + FFDisplayType type = FF_DISPLAY_TYPE_UNKNOWN; + if ((field = strstr(info, ", type "))) { + field += strlen(", type "); + if (ffStrStartsWith(field, "INTERNAL")) { + type = FF_DISPLAY_TYPE_BUILTIN; + } else if (ffStrStartsWith(field, "EXTERNAL")) { + type = FF_DISPLAY_TYPE_EXTERNAL; } } - index = nextIndex + 1; - } -} + unsigned density = 0; + if ((field = strstr(info, ", density ")) && sscanf(field, ", density %u", &density) < 1) { + density = 0; + } -static bool detectWithGetprop(FFDisplayServerResult* ds) { - // Only for MiUI - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + unsigned displayId = 0; + bool primary = sscanf(info, "%u", &displayId) >= 1 && displayId == 0; // Display 0 is the default one - if (ffSettingsGetAndroidProperty("persist.sys.miui_resolution", &buffer) && - ffStrbufContainC(&buffer, ',')) { - // 1440,3200,560 => width,height,densityDpi - uint32_t width = (uint32_t) ffStrbufToUInt(&buffer, 0); - ffStrbufSubstrAfterFirstC(&buffer, ','); - uint32_t height = (uint32_t) ffStrbufToUInt(&buffer, 0); - ffStrbufSubstrAfterFirstC(&buffer, ','); - uint32_t dpi = (uint32_t) ffStrbufToUInt(&buffer, 0) * 96 / 160; + // Android counts density in dpi with 160 as the 1x baseline, fastfetch uses 96 FFDisplayResult* display = ffdsAppendDisplay(ds, width, height, + refreshRate, + density * 96 / 160, 0, - dpi, 0, 0, 0, + &name, + type, + primary, 0, - nullptr, - FF_DISPLAY_TYPE_BUILTIN, - false, 0, 0, - 0, - "getprop"); + "cmd"); if (display) { display->hdrStatus = checkHdrStatus(display); } - return !!display; - } - return false; + index = nextIndex + 1; + } } // Several vendors embed the UI name and its version in `ro.build.display.id` without any @@ -477,9 +493,7 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); - if (!detectWithGetprop(ds)) { - detectWithDumpsys(ds); - } + detectWithCmd(ds); detectDE(ds); } From 2fb7fabc4d8c00b724a7185a622bc45adedb35f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 11:16:17 +0800 Subject: [PATCH 66/76] CI (Haiku): don't reboot after installing deps --- .github/workflows/build-haiku-amd64.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-haiku-amd64.yml b/.github/workflows/build-haiku-amd64.yml index c6642b9b16..ba287ca1e5 100644 --- a/.github/workflows/build-haiku-amd64.yml +++ b/.github/workflows/build-haiku-amd64.yml @@ -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 From ecbf218d6e81752bbcdff75d0a1a154d6f67b296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 11:21:36 +0800 Subject: [PATCH 67/76] Chore: fixes compiling errors --- src/common/smbios.h | 2 +- tests/base64.c | 2 +- tests/endian-bigendian.c | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/smbios.h b/src/common/smbios.h index cd5418f198..7aa0d50482 100644 --- a/src/common/smbios.h +++ b/src/common/smbios.h @@ -100,5 +100,5 @@ typedef const FFSmbiosHeader* FFSmbiosHeaderTable[FF_SMBIOS_TYPE__MAX]; [[nodiscard]] const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable(void); #ifdef __linux__ -[[gnu::nonnull(1, 2, 3), nodiscard]] bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer); +[[gnu::nonnull(1, 2, 3)]] bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer); #endif diff --git a/tests/base64.c b/tests/base64.c index 5f05f55de0..1376fe214f 100644 --- a/tests/base64.c +++ b/tests/base64.c @@ -135,7 +135,7 @@ static void verifyRoundTrip(void) { uint32_t decodedLength = 0; memset(decoded, 0, sizeof(decoded)); - VERIFY(ffBase64DecodeRaw(encodedLength, encoded, &decodedLength, decoded)); + ffBase64DecodeRaw(encodedLength, encoded, &decodedLength, decoded); VERIFY(decodedLength == length); VERIFY(memcmp(decoded, source, length) == 0); } diff --git a/tests/endian-bigendian.c b/tests/endian-bigendian.c index 39056bedf6..8b76ccf5cf 100644 --- a/tests/endian-bigendian.c +++ b/tests/endian-bigendian.c @@ -30,12 +30,12 @@ // reading a big endian value is a no-op ... _Static_assert(FF_READ_BE((uint16_t) 0x1122) == (uint16_t) 0x1122, "FF_READ_BE must be the identity on a big endian host"); _Static_assert(FF_READ_BE(0x11223344u) == 0x11223344u, "FF_READ_BE must be the identity on a big endian host"); -_Static_assert(FF_READ_BE(0x1122334455667788ull) == 0x1122334455667788ull, "FF_READ_BE must be the identity on a big endian host"); +_Static_assert(FF_READ_BE((uint64_t) 0x1122334455667788ull) == (uint64_t) 0x1122334455667788ull, "FF_READ_BE must be the identity on a big endian host"); // ... and reading a little endian one swaps. _Static_assert(FF_READ_LE((uint16_t) 0x1122) == (uint16_t) 0x2211, "FF_READ_LE must swap on a big endian host"); _Static_assert(FF_READ_LE(0x11223344u) == 0x44332211u, "FF_READ_LE must swap on a big endian host"); -_Static_assert(FF_READ_LE(0x1122334455667788ull) == 0x8877665544332211ull, "FF_READ_LE must swap on a big endian host"); +_Static_assert(FF_READ_LE((uint64_t) 0x1122334455667788ull) == (uint64_t) 0x8877665544332211ull, "FF_READ_LE must swap on a big endian host"); int main(void) { // Everything this test asserts is checked at compile time, so reaching this line is the result. From 873a881dab5f1acabab2f8cbdab8cd4d8bde27f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 17:22:51 +0800 Subject: [PATCH 68/76] Battery (Android): rewrites impl via `/dev/binder` --- CHANGELOG.md | 3 + CMakeLists.txt | 1 + src/common/android/binder.c | 237 ++++++++++++++++++++++++ src/common/android/binder.h | 224 ++++++++++++++++++++++ src/detection/battery/battery_android.c | 177 ++++++++++++------ 5 files changed, 582 insertions(+), 60 deletions(-) create mode 100644 src/common/android/binder.c create mode 100644 src/common/android/binder.h diff --git a/CHANGELOG.md b/CHANGELOG.md index a85292783b..713ee19ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ Features: * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved Camera detection on Android (Camera, Android) * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. +* Improved Battery detection on Android (Battery, Android) + * The charge level and charging state are now read from the battery properties service over `/dev/binder` instead of `termux-api BatteryStatus`, so the Termux:API app is no longer required and no subprocess is spawned. + * Battery temperature, cycle count, manufacturer, model name, serial number and manufacture date need the `BATTERY_STATS` permission, which an app cannot obtain, so they are no longer reported. * Improved Display detection on Android when fastfetch runs as an app rather than from `adb shell`, where `dumpsys display` is not permitted. (Display, Android) * The displays are now read through the shell command interface of the display service, which needs no permission. * Improved COSMIC detection (DE / WM, Linux) diff --git a/CMakeLists.txt b/CMakeLists.txt index fda52041c3..018c7d2ba7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -615,6 +615,7 @@ if(LINUX) ) elseif(ANDROID) list(APPEND LIBFASTFETCH_SRC + src/common/android/binder.c src/common/impl/dbus.c src/common/impl/io_unix.c src/common/impl/netif_linux.c diff --git a/src/common/android/binder.c b/src/common/android/binder.c new file mode 100644 index 0000000000..6d07a9147c --- /dev/null +++ b/src/common/android/binder.c @@ -0,0 +1,237 @@ +#include "common/android/binder.h" + +#include +#include +#include + +const char* ffBinderOpen(FFBinder* binder) { + *binder = (FFBinder) { .fd = -1, .shared = nullptr, .sharedSize = 0, .protocolVersion = 0 }; + + const int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC); + if (fd < 0) { + return "Failed to open " FF_BINDER_DEVICE; + } + + struct binder_version version = { .protocol_version = 0 }; + if (ioctl(fd, BINDER_VERSION, &version) != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) { + close(fd); + 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) { + close(fd); + return "Failed to mmap " FF_BINDER_DEVICE; + } + + *binder = (FFBinder) { + .fd = fd, + .shared = (uint8_t*) shared, + .sharedSize = FF_BINDER_SHARED_SIZE, + .protocolVersion = version.protocol_version, + }; + 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, const FFBinderParcel* parcel, FFBinderReply* reply) { + reply->size = 0; + reply->code = 0; + reply->flags = 0; + reply->handleCount = 0; + + if (parcel->truncated) { + 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 = 0, // a synchronous call; TF_ONE_WAY is the only flag the kernel acts on here + .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) { + 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: + return "Binder transaction failed"; + case BR_DEAD_REPLY: + 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)) { + 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) { + 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 and keep the + // strong handles: those are the ones a later transaction can address. + 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) { + continue; + } + if (reply->handleCount < FF_BINDER_MAX_HANDLES) { + reply->handles[reply->handleCount++] = 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, + }; + ioctl(binder->fd, BINDER_WRITE_READ, &release); + + return error; + } + + default: + return "Unexpected binder command"; + } + } + + 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, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + return "Service manager rejected the request"; + } + if (ffBinderReadI32(reply.data, reply.size, 0) != 0) { + return "Service manager raised an exception"; + } + if (reply.handleCount == 0) { + return "Service is not registered"; + } + + *handle = reply.handles[0]; + return nullptr; +} diff --git a/src/common/android/binder.h b/src/common/android/binder.h new file mode 100644 index 0000000000..5ad0e1642b --- /dev/null +++ b/src/common/android/binder.h @@ -0,0 +1,224 @@ +#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. +// +// See .workbuddy-ai/android-binder-raw-client.md for the full write-up and the tooling. + +#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 + +#include +#include + +#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 + +// 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]; +} 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 = {} }; +} + +// 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. Handles the +// reply carries are acquired and added to `reply->handles` before the reply buffer is freed. +// +// `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, 4, 5), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, 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); diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c index 325be96cb7..37e938ef28 100644 --- a/src/detection/battery/battery_android.c +++ b/src/detection/battery/battery_android.c @@ -1,71 +1,133 @@ #include "fastfetch.h" #include "battery.h" -#include "common/strutil.h" +#include "common/androidApi.h" +#include "common/android/binder.h" #include "common/processing.h" #include "common/properties.h" -#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" -#define FF_TERMUX_API_PARAM "BatteryStatus" - -static inline void wrapYyjsonFree(yyjson_doc** doc) { - assert(doc); - if (*doc) { - yyjson_doc_free(*doc); - } -} - -static const char* parseTermuxApi(FFBatteryOptions* options, FFlist* results) { - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); - - if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, nullptr })) { - return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; - } - - [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(buffer.chars, buffer.length, 0, nullptr, nullptr); - if (!doc) { - return "Failed to parse battery info"; - } - - yyjson_val* root = yyjson_doc_get_root(doc); - if (!yyjson_is_obj(root)) { - return "Battery info result is not a JSON object"; - } - - FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); +// Android exposes the battery through BatteryManager, which the NDK does not wrap and whose sysfs +// backing is unreadable from an app UID. What does work without any permission is +// android.os.IBatteryPropertiesRegistrar in system_server, reachable either over /dev/binder (see +// common/android/binder.h) or through a `dumpsys battery` subprocess. The binder route is the cheap +// one: ~0.3 ms for both properties, and no child process. +// +// The registrar only answers properties that do not need BATTERY_STATS, and that rules out +// everything the module would otherwise like to show. Manufacturer, model name, technology, serial +// number, manufacture date, temperature and cycle count all sit behind that permission. + +// The transaction code and the reply layout are both positional, and neither is negotiated with the +// service, so each only stays correct as long as AOSP keeps the order it already has. The code has +// not: `IBatteryPropertiesRegistrar` still declared registerListener / unregisterListener / +// getProperty up to Android 9, and Android 10 dropped the two listener methods, which moved +// `getProperty` from the third position to the first. The layout has, because `BatteryProperty` has +// only ever grown at the end -- API 35 appended [string8 mValueString] to it -- so the field read +// below is still the first one written. +// +// Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the +// end of the reply, so a drift shows up as a wrong capacity, not as a failed call. +#define FF_BATTERY_ANDROID_SERVICE "batteryproperties" +#define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar" + +// BatteryManager.BATTERY_PROPERTY_* +#define FF_BATTERY_ANDROID_PROPERTY_CAPACITY 4u +#define FF_BATTERY_ANDROID_PROPERTY_STATUS 6u + +// BatteryManager.BATTERY_STATUS_* +#define FF_BATTERY_ANDROID_STATUS_CHARGING 2u +#define FF_BATTERY_ANDROID_STATUS_DISCHARGING 3u + +// BatteryProperty starts with [int64 mValueLong], behind the usual [exception code][return value] +// [out-param non-null marker] prefix of an AIDL reply. API 35 appended [string8 mValueString] after +// the long for the one string valued property (`BATTERY_PROPERTY_SERIAL_NUMBER`), which does not +// move the long, so one offset covers every release. +#define FF_BATTERY_ANDROID_VALUE_OFFSET 12 +#define FF_BATTERY_ANDROID_VALUE_UNSET 0x8000000000000000ULL // Long.MIN_VALUE, i.e. never filled in + +static void initResult(FFBatteryResult* battery) { battery->temperature = FF_BATTERY_TEMP_UNSET; battery->cycleCount = 0; battery->timeRemaining = -1; + battery->capacity = 0; battery->status = FF_BATTERY_STATUS_NONE; ffStrbufInit(&battery->manufacturer); ffStrbufInit(&battery->modelName); ffStrbufInit(&battery->technology); ffStrbufInit(&battery->serial); ffStrbufInit(&battery->manufactureDate); +} - battery->capacity = yyjson_get_num(yyjson_obj_get(root, "percentage")); - const char* acStatus = yyjson_get_str(yyjson_obj_get(root, "plugged")); - if (acStatus) { - if (ffStrEquals(acStatus, "PLUGGED_AC")) { - battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; - } else if (ffStrEquals(acStatus, "PLUGGED_USB")) { - battery->status |= FF_BATTERY_STATUS_USB_CONNECTED; - } else if (ffStrEquals(acStatus, "PLUGGED_WIRELESS")) { - battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED; - } +static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t property, uint64_t* value) { + // `getProperty` is the third method declared in `IBatteryPropertiesRegistrar` up to Android 9 and + // the first one from Android 10 on. Written as an `if` because clang rejects `__builtin_available` + // -- which is what FF_API_AT_LEAST expands to -- in any other position. + uint32_t transaction = 3u; + if (FF_API_AT_LEAST(29)) { + transaction = 1u; } - const char* status = yyjson_get_str(yyjson_obj_get(root, "status")); - if (status) { - if (ffStrEquals(status, "CHARGING")) { - battery->status |= FF_BATTERY_STATUS_CHARGING; - } else if (ffStrEquals(status, "DISCHARGING")) { - battery->status |= FF_BATTERY_STATUS_DISCHARGING; - } + + uint8_t parcelBuffer[128]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_DESCRIPTOR); + ffBinderParcelPutU32(&parcel, property); + + uint8_t replyBuffer[128]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + const char* error = ffBinderTransact(binder, handle, transaction, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + return "Battery service rejected the request"; + } + if (ffBinderReadI32(reply.data, reply.size, 0) != 0) { + return "Battery service raised an exception"; + } + if (ffBinderReadI32(reply.data, reply.size, 4) != 0) { + return "Battery service does not report this property"; } - if (options->temp) { - battery->temperature = yyjson_get_num(yyjson_obj_get(root, "temperature")); + const uint64_t result = ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_VALUE_OFFSET); + if (result == FF_BATTERY_ANDROID_VALUE_UNSET) { + return "Battery service left this property empty"; } + *value = result; + return nullptr; +} + +static const char* parseBinder(FFlist* results) { + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + const char* error = ffBinderOpen(&binder); + if (error != nullptr) { + return error; + } + + uint32_t handle = 0; + error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle); + if (error != nullptr) { + return error; + } + + uint64_t capacity = 0; + error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity); + if (error != nullptr) { + return error; + } + + uint64_t status = 0; + error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status); + if (error != nullptr) { + return error; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + initResult(battery); + battery->capacity = (double) capacity; + if (status == FF_BATTERY_ANDROID_STATUS_CHARGING) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } return nullptr; } @@ -93,16 +155,7 @@ static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) { ffStrbufClear(&temp); FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); - battery->temperature = FF_BATTERY_TEMP_UNSET; - battery->cycleCount = 0; - battery->timeRemaining = -1; - battery->capacity = 0; - battery->status = FF_BATTERY_STATUS_NONE; - ffStrbufInit(&battery->manufacturer); - ffStrbufInit(&battery->modelName); - ffStrbufInit(&battery->technology); - ffStrbufInit(&battery->serial); - ffStrbufInit(&battery->manufactureDate); + initResult(battery); if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; @@ -152,8 +205,12 @@ static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) { } const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { - const char* error = parseTermuxApi(options, results); - if (error && parseDumpsys(options, results) == nullptr) { + // The binder route needs no permission and costs ~0.3 ms. `dumpsys battery` is kept as a + // fallback for `adb shell` and rooted environments, where the binder route also works but the + // extra fields it reports are worth having. termux-api was dropped: it returns nothing on this + // device and can hang for minutes, the same reason its camera path was removed. + const char* error = parseBinder(results); + if (error != nullptr && parseDumpsys(options, results) == nullptr) { return nullptr; } return error; From e1d7360453a02f05479822e21b8c46d111ffd39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 17:31:25 +0800 Subject: [PATCH 69/76] Display (Android): adds richer information and adds detectWithGetprop back --- CHANGELOG.md | 12 +- .../displayserver/displayserver_android.c | 321 ++++++++++++++---- 2 files changed, 256 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 713ee19ef4..08e07dbdf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Changes: * ImageMagick is no longer used for image logos on Windows, macOS and Android, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS, AImageDecoder on Android). (Logo) + * An image logo needs Android 11 (API 30), the release that introduced the platform image decoder, and is reported as an error on Android 10 and older. * ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available. * It is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible. @@ -29,7 +30,7 @@ Features: * `--logo-animation-frame ` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option. * The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. * The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, AImageDecoder on Android, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image. - * On Android an animation needs Android 12 (API 31), which is where AImageDecoder gained the ability to decode past the first frame. AImageDecoder composes the frames itself; its API does not expose a repeat count, so an animation is reported as looping forever. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image. + * On Android an animation needs Android 12 (API 31). The repeat count is not available there, so an animation is reported as looping forever. A build with no image decoder, or one built with ImageMagick 6, reports an error instead of silently showing a still image. * A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame. * Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo) * Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in. @@ -43,14 +44,19 @@ Features: * Added support for HarmonyOS, HarmonyOS NEXT, Flyme, JOYUI, SmartisanOS, realme UI, HydrogenOS, ZUI, ZUXOS, MyOS, NebulaAIOS, ObricUI, MiFavor, LineageOS, PixelExperience, EUI and 360 UI. * Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string. * Added Samsung OneUI support (#2541) + * It is read from system properties, so no particular Android version is required. * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved Camera detection on Android (Camera, Android) * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. + * This needs Android 7.0 (API 24), which is where the camera2 NDK was introduced. * Improved Battery detection on Android (Battery, Android) * The charge level and charging state are now read from the battery properties service over `/dev/binder` instead of `termux-api BatteryStatus`, so the Termux:API app is no longer required and no subprocess is spawned. + * The service interface changed in Android 10, and the right request is picked at run time, so every Android release is covered. * Battery temperature, cycle count, manufacturer, model name, serial number and manufacture date need the `BATTERY_STATS` permission, which an app cannot obtain, so they are no longer reported. -* Improved Display detection on Android when fastfetch runs as an app rather than from `adb shell`, where `dumpsys display` is not permitted. (Display, Android) - * The displays are now read through the shell command interface of the display service, which needs no permission. +* Improved Display detection on Android (Display, Android) + * The displays are now read from the display service instead of a vendor property that only some Xiaomi devices set. The preferred mode, the physical size, the rotation, the manufacture date and the display id are now reported as well. + * This needs Android 13 (API 33). On Android 12 and older only that vendor property is available to an app, and a device that does not set it reports no display. + * The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index f3c4b5f594..e194d58aa5 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -7,66 +7,95 @@ #include -static bool checkHdrStatus(FFDisplayResult* display) { +static bool detectWithGetprop(FFDisplayServerResult* ds) { + // Only for MiUI FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); - if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) { - if (ffStrbufIgnCaseEqualS(&buffer, "true")) { - display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; - - if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &buffer) && - ffStrbufToUInt(&buffer, 0) > 0) { - display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; - } - - return true; - } else { - display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; - return true; - } + if (ffSettingsGetAndroidProperty("persist.sys.miui_resolution", &buffer) && + ffStrbufContainC(&buffer, ',')) { + // 1440,3200,560 => width,height,densityDpi + uint32_t width = (uint32_t) ffStrbufToUInt(&buffer, 0); + ffStrbufSubstrAfterFirstC(&buffer, ','); + uint32_t height = (uint32_t) ffStrbufToUInt(&buffer, 0); + ffStrbufSubstrAfterFirstC(&buffer, ','); + uint32_t dpi = (uint32_t) ffStrbufToUInt(&buffer, 0) * 96 / 160; + FFDisplayResult* display = ffdsAppendDisplay(ds, + width, + height, + 0, + dpi, + 0, + 0, + 0, + 0, + nullptr, + FF_DISPLAY_TYPE_BUILTIN, + false, + 0, + 0, + 0, + "getprop"); + return !!display; } - display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; return false; } -static void detectWithCmd(FFDisplayServerResult* ds) { - // Unlike `dumpsys`, the shell command interface of the same service is not permission gated - +// `cmd display get-displays` and `dumpsys display` print the same thing -- the `DisplayInfo` of every +// display, one per line -- so one parser covers both and only the command and the marker in front of +// each record differ: +// +// * `cmd display get-displays` needs no permission, which is what makes it usable for an app UID, +// but the subcommand was only added to `DisplayManagerShellCommand` in Android 13. Android 11 and +// 12 answer `Unknown command: get-displays` on stdout with exit code 255. +// * `dumpsys display` covers every release, including the ones that predate `get-displays`, but it +// is gated behind `android.permission.DUMP`, so it only answers for `adb shell` and root. +// +// The record layout has changed across releases, and every difference is accepted rather than version +// checked: +// +// * The mode list is printed as `modes [...]` up to Android 14 and as `supportedModes [...]` from +// Android 15 on, which also prints `appsSupportedModes [...]` right behind it. Both spellings are +// searched for. +// * `renderFrameRate` is printed from Android 15 on. Before that the active mode's fps is the only +// refresh rate the dump carries. The active mode's fps is preferred even where it exists, see +// the comment on `refreshRate` below. +// * `displayGroupId` is printed from Android 12 on, the physical dpi behind `density` from +// Android 11 on, and `isForceSdr` from Android 15 on. +// +// A record is one line, and the `DisplayInfo{` inside it is what gets parsed, so the `Display id 0: ` +// of the one command and the `mBaseDisplayInfo=` of the other are both skipped by the same code. +static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], const char* marker, const char* platformApi) { FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); FFProcessHandle handle; // `cmd` forwards its stdin to the service over binder, and the kernel rejects the whole // transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal. - // Detaching the child from our stdin is only needed here, so the low level API is called - // instead of `ffProcessAppendStdOut`. - if (ffProcessSpawn((char*[]) { - "/system/bin/cmd", - "display", - "get-displays", - nullptr, - }, - false, - ffGetNullFD(), - &handle) != nullptr) { - return; // The shell command interface is not available on every Android version + // Detaching the child from our stdin is only needed here, so the low level API is called instead + // of `ffProcessAppendStdOut`. + if (ffProcessSpawn(argv, false, ffGetNullFD(), &handle) != nullptr) { + return false; // Neither command is available on every Android version } if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) { - return; + return false; } ffStrbufTrimRightSpace(&buf); uint32_t index = 0; - while ((index = ffStrbufNextIndexS(&buf, index, "Display id ")) < buf.length) { - index += strlen("Display id "); + while ((index = ffStrbufNextIndexS(&buf, index, marker)) < buf.length) { + index += strlen(marker); uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n'); buf.chars[nextIndex] = '\0'; const char* info = buf.chars + index; // 0: DisplayInfo{"Builtin display", displayId 0, ..., real 1440 x 3168, ..., mode 2, - // renderFrameRate 60.000004, ..., supportedModes [{id=2, width=1440, height=3168, - // fps=60.000004, ...}], ..., type INTERNAL, ..., density 560 (560.0 x 560.0) dpi, ...} + // renderFrameRate 60.000004, ..., defaultMode 4, ..., supportedModes [{id=2, + // width=1440, height=3168, fps=60.000004, ...}], ..., hdrCapabilities + // HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}, isForceSdr false, ..., + // rotation 0, ..., type INTERNAL, uniqueId "local:4630946557703207059", ..., + // density 560 (560.0 x 560.0) dpi, ..., deviceProductInfo DeviceProductInfo{..., + // manufactureDate=ManufactureDate{week=27, year=2006}, ...}, ...} const char* field = strstr(info, "DisplayInfo{\""); FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64); if (field) { @@ -77,36 +106,74 @@ static void detectWithCmd(FFDisplayServerResult* ds) { } } + // `real` is the size the display currently uses, which is smaller than the panel's when the + // framework emulates a smaller display size unsigned width = 0, height = 0; if ((field = strstr(info, ", real ")) && sscanf(field, ", real %u x %u", &width, &height) < 2) { width = height = 0; } - double refreshRate = 0; - if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &refreshRate) < 1) { - refreshRate = 0; + // `renderFrameRate` is printed from Android 15 on. It is documented as "a divisor of the + // active mode refresh rate", so it is the rate the display is currently *rendering* at and + // can be lower than the mode it is set to. It is therefore only used for a record whose + // mode list can not be read, where a possibly divided rate still beats none. + double renderFrameRate = 0; + if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &renderFrameRate) < 1) { + renderFrameRate = 0; } - if (refreshRate <= 0) { - // `renderFrameRate` is only printed since Android 11. Older builds expose the active mode - // only, so its refresh rate has to be looked up in the list of supported modes. - unsigned activeMode = 0; - field = strstr(info, ", mode "); - if (field && sscanf(field, ", mode %u", &activeMode) >= 1) { - field = strstr(info, "supportedModes ["); - while (field && (field = strstr(field, "{id="))) { - // {id=2, width=1440, height=3168, fps=60.000004, ... - unsigned id = 0; - double fps = 0; - if (sscanf(field, "{id=%u, width=%*u, height=%*u, fps=%lf", &id, &fps) < 2) { - break; - } - if (id == activeMode) { - refreshRate = fps; - break; - } - ++field; - } + + unsigned activeMode = 0, defaultMode = 0; + if ((field = strstr(info, ", mode ")) && sscanf(field, ", mode %u", &activeMode) < 1) { + activeMode = 0; + } + if ((field = strstr(info, ", defaultMode ")) && sscanf(field, ", defaultMode %u", &defaultMode) < 1) { + defaultMode = 0; + } + + // The modes are listed with their resolution in the natural orientation, which is also how + // the preferred values are reported on the other platforms. `defaultMode` is the mode the + // display itself prefers, so it carries the panel's native resolution. + uint32_t preferredWidth = 0, preferredHeight = 0; + double preferredRefreshRate = 0, activeModeRefreshRate = 0; + field = strstr(info, ", supportedModes ["); + if (field == nullptr) { + field = strstr(info, ", modes ["); // Android 14 and older + } + while (field && (field = strstr(field, "{id="))) { + // {id=2, width=1440, height=3168, fps=60.000004, ... + unsigned id = 0, modeWidth = 0, modeHeight = 0; + double fps = 0; + if (sscanf(field, "{id=%u, width=%u, height=%u, fps=%lf", &id, &modeWidth, &modeHeight, &fps) < 4) { + break; + } + if (id == activeMode) { + activeModeRefreshRate = fps; + } + if (id == defaultMode) { + preferredWidth = modeWidth; + preferredHeight = modeHeight; + preferredRefreshRate = fps; + } + if (activeModeRefreshRate > 0 && preferredWidth > 0) { + break; // Both are in, and the same list is printed a second time from Android 15 on } + ++field; + } + + // The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports + // (`refreshRateOverride` if it is set, the mode's own rate otherwise) and what every other + // platform reports. `renderFrameRate` is deliberately not preferred: it is a render + // cadence that follows the content rather than a property of the display, and it does not + // exist before Android 15, so using it would make the reported rate change with the + // Android version as well as with what is on screen. + double refreshRate = activeModeRefreshRate; + if (refreshRate <= 0) { + refreshRate = renderFrameRate; + } + + unsigned rotation = 0; + if ((field = strstr(info, ", rotation ")) && sscanf(field, ", rotation %u", &rotation) < 1) { + rotation = 0; } FFDisplayType type = FF_DISPLAY_TYPE_UNKNOWN; @@ -114,18 +181,63 @@ static void detectWithCmd(FFDisplayServerResult* ds) { field += strlen(", type "); if (ffStrStartsWith(field, "INTERNAL")) { type = FF_DISPLAY_TYPE_BUILTIN; - } else if (ffStrStartsWith(field, "EXTERNAL")) { + } else if (ffStrStartsWith(field, "EXTERNAL") || ffStrStartsWith(field, "WIFI")) { + // A WIFI display is a wireless sink, which is as external as a wired one type = FF_DISPLAY_TYPE_EXTERNAL; } } unsigned density = 0; - if ((field = strstr(info, ", density ")) && sscanf(field, ", density %u", &density) < 1) { - density = 0; + double physicalXDpi = 0, physicalYDpi = 0; + if ((field = strstr(info, ", density "))) { + // `density 640 (501.0411 x 509.28604) dpi`, the physical dpi is only printed since + // Android 11 + if (sscanf(field, ", density %u (%lf x %lf) dpi", &density, &physicalXDpi, &physicalYDpi) < 1) { + density = 0; + } + } + + // The physical dpi describes the panel itself and does not change with the logical display + // size, so the physical size has to be derived from the native resolution + uint32_t physicalWidth = 0, physicalHeight = 0; + if (physicalXDpi > 0) { + physicalWidth = (uint32_t) ((preferredWidth ? preferredWidth : width) * 25.4 / physicalXDpi + 0.5); + } + if (physicalYDpi > 0) { + physicalHeight = (uint32_t) ((preferredHeight ? preferredHeight : height) * 25.4 / physicalYDpi + 0.5); + } + + // `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` on a + // physical display and `virtual:...` on a virtual one + uint64_t id = 0; + if ((field = strstr(info, ", uniqueId \""))) { + field += strlen(", uniqueId \""); + const char* uniqueIdEnd = strchr(field, '"'); + const char* digits = uniqueIdEnd ? memchr(field, ':', (size_t) (uniqueIdEnd - field)) : nullptr; + id = (uint64_t) strtoull(digits ? digits + 1 : field, nullptr, 10); + } + + uint16_t manufactureYear = 0, manufactureWeek = 0; + if ((field = strstr(info, ", deviceProductInfo "))) { + // `manufactureDate=ManufactureDate{week=27, year=2006}`, either field may be `null` + const char* date = strstr(field, "manufactureDate=ManufactureDate{"); + unsigned year = 0, week = 0; + if (date && sscanf(date + strlen("manufactureDate=ManufactureDate{"), "week=%u, year=%u", &week, &year) == 2) { + manufactureYear = (uint16_t) year; + manufactureWeek = (uint16_t) week; + } else if ((field = strstr(field, ", modelYear=")) && sscanf(field, ", modelYear=%u", &year) == 1) { + // A display reports either the date of manufacture or the model year + manufactureYear = (uint16_t) year; + } } + // `displayId` sits inside the record, not in front of it: `cmd` prints the id again in its + // own prefix, but `dumpsys` prints only `mBaseDisplayInfo=` unsigned displayId = 0; - bool primary = sscanf(info, "%u", &displayId) >= 1 && displayId == 0; // Display 0 is the default one + if ((field = strstr(info, ", displayId ")) && sscanf(field, ", displayId %u", &displayId) < 1) { + displayId = 0; + } + bool primary = displayId == 0; // Display 0 is the default one // Android counts density in dpi with 160 as the 1x baseline, fastfetch uses 96 FFDisplayResult* display = ffdsAppendDisplay(ds, @@ -133,23 +245,79 @@ static void detectWithCmd(FFDisplayServerResult* ds) { height, refreshRate, density * 96 / 160, - 0, - 0, - 0, - 0, + preferredWidth, + preferredHeight, + preferredRefreshRate, + rotation, &name, type, primary, - 0, - 0, - 0, - "cmd"); + id, + physicalWidth, + physicalHeight, + platformApi); if (display) { - display->hdrStatus = checkHdrStatus(display); + display->manufactureYear = manufactureYear; + display->manufactureWeek = manufactureWeek; + + // Reported for every display, not only for the built-in one: `hdrCapabilities` is a + // field of the `DisplayInfo` record itself, so it describes that display and nothing + // else, and the other platforms report HDR per display too (EDID on Linux, the + // advanced color info per target on Windows). An external display or a wireless sink + // carries the field as well. + // + // `hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}` is printed + // since Android 11, where an empty list means that the display can not do HDR at all. + // The two fallbacks below it are device wide vendor properties, which is the price of + // answering for a record that does not print the field. + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + field = strstr(info, "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=["); + if (field) { + field += strlen("hdrCapabilities HdrCapabilities{mSupportedHdrTypes=["); + display->hdrStatus = *field == ']' ? FF_DISPLAY_HDR_STATUS_UNSUPPORTED : FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) { + display->hdrStatus = ffStrbufIgnCaseEqualS(&buffer, "true") ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + } + + if (display->hdrStatus == FF_DISPLAY_HDR_STATUS_SUPPORTED) { + // `persist.sys.hdr_mode` is non-zero while HDR is turned on, and `isForceSdr + // true` means that the framework disabled every HDR capability of this display. + // Note that `ffSettingsGetAndroidProperty` appends, so the value needs its own + // buffer. + FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate(); + if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) && + ffStrbufToUInt(&hdrMode, 0) > 0 && + !strstr(info, ", isForceSdr true")) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } + } } - index = nextIndex + 1; + // The last display of the dump is not followed by a newline, so the loop must not step + // past the end of the buffer (`ffStrbufNextIndexC` returns the length when it finds none) + index = nextIndex < buf.length ? nextIndex + 1 : buf.length; } + + // A command that produced no `DisplayInfo` record has to be reported as a failure: the caller + // keys off this value to decide whether to try the other command, and on Android 12 and older + // `dumpsys` is the only one that can still yield a display. + return ds->displays.length > 0; +} + +static bool detectWithCmd(FFDisplayServerResult* ds) { + return detectWithCommand(ds, + (char*[]) { "/system/bin/cmd", "display", "get-displays", nullptr }, + "Display id ", + "cmd"); +} + +static bool detectWithDumpsys(FFDisplayServerResult* ds) { + return detectWithCommand(ds, + (char*[]) { "/system/bin/dumpsys", "display", nullptr }, + "mBaseDisplayInfo=", + "dumpsys"); } // Several vendors embed the UI name and its version in `ro.build.display.id` without any @@ -493,7 +661,12 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); - detectWithCmd(ds); + // `cmd` comes first because it needs no permission and therefore also works for an app UID. + // `dumpsys` is the only route that answers on Android 12 and older, and only for `adb shell` and + // root, and `getprop` is MiUI specific and the last resort. + if (!detectWithCmd(ds) && !detectWithDumpsys(ds)) { + detectWithGetprop(ds); + } detectDE(ds); } From 2594c01fc184a7b9d21402aca92bec62e38ba62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 20:24:43 +0800 Subject: [PATCH 70/76] Common: renames `androidApi.h` for `android/api.h` --- src/common/{androidApi.h => android/api.h} | 0 src/detection/battery/battery_android.c | 2 +- src/detection/camera/camera_android.c | 2 +- src/detection/codec/codec_android.c | 2 +- src/logo/image/aid.c | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/common/{androidApi.h => android/api.h} (100%) diff --git a/src/common/androidApi.h b/src/common/android/api.h similarity index 100% rename from src/common/androidApi.h rename to src/common/android/api.h diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c index 37e938ef28..a99c773a0c 100644 --- a/src/detection/battery/battery_android.c +++ b/src/detection/battery/battery_android.c @@ -1,6 +1,6 @@ #include "fastfetch.h" #include "battery.h" -#include "common/androidApi.h" +#include "common/android/api.h" #include "common/android/binder.h" #include "common/processing.h" #include "common/properties.h" diff --git a/src/detection/camera/camera_android.c b/src/detection/camera/camera_android.c index 5b3b1ccfd8..abdbe3d9d0 100644 --- a/src/detection/camera/camera_android.c +++ b/src/detection/camera/camera_android.c @@ -42,7 +42,7 @@ static void ffCameraMaxStreamSize(const ACameraMetadata* metadata, int32_t forma const char* ffDetectCamera(FFlist* result) { // The camera2 NDK and every entry point below were introduced in API 24, which is the API level // this build targets, so nothing here is newer than the minimum supported version and - // common/androidApi.h has nothing to guard. That header covers the opposite case: entry points + // common/android/api.h has nothing to guard. That header covers the opposite case: entry points // the NDK marks unavailable because they postdate the target, such as AImageDecoder (30) or // AMediaCodec_getName (28). It also only ever makes *symbols* weak -- libcamera2ndk.so itself is // API 24, so linking it unconditionally in CMakeLists.txt is fine on every supported device. diff --git a/src/detection/codec/codec_android.c b/src/detection/codec/codec_android.c index 54b39b9a6b..b699dbb228 100644 --- a/src/detection/codec/codec_android.c +++ b/src/detection/codec/codec_android.c @@ -1,6 +1,6 @@ #include "codec.h" -#include "common/androidApi.h" +#include "common/android/api.h" #include "common/strutil.h" #include diff --git a/src/logo/image/aid.c b/src/logo/image/aid.c index 4e7ae343a2..602aeac309 100644 --- a/src/logo/image/aid.c +++ b/src/logo/image/aid.c @@ -1,6 +1,6 @@ #include "image.h" -#include "common/androidApi.h" +#include "common/android/api.h" #include "common/io.h" #include "common/mallocHelper.h" @@ -122,7 +122,7 @@ FF_REQUIRES_API(30) static bool androidResolveDecoder(AImageDecoder* decoder, FF bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { // AImageDecoder is API 30, and fastfetch still runs on devices below that, so this is a real - // run-time check and not a compile-time constant. See common/androidApi.h for why. + // run-time check and not a compile-time constant. See common/android/api.h for why. if (FF_API_AT_LEAST(30)) { FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC); if (fd < 0) { From 665c4724823e1396c3d0503fb8b002ff149d9432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 19 Sep 2026 00:39:25 +0800 Subject: [PATCH 71/76] Wifi (Android): rewrites impl via `/dev/binder` --- CHANGELOG.md | 4 + CMakeLists.txt | 1 + src/common/android/dex.c | 358 +++++++++++++++ src/common/android/dex.h | 30 ++ src/detection/wifi/wifi_android.c | 714 ++++++++++++++++++++++++++++-- 5 files changed, 1072 insertions(+), 35 deletions(-) create mode 100644 src/common/android/dex.c create mode 100644 src/common/android/dex.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e07dbdf6..45e6ebf66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ Features: * The displays are now read from the display service instead of a vendor property that only some Xiaomi devices set. The preferred mode, the physical size, the rotation, the manufacture date and the display id are now reported as well. * This needs Android 13 (API 33). On Android 12 and older only that vendor property is available to an app, and a device that does not set it reports no display. * The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one. +* Improved WiFi detection on Android (Wifi, Android) + * The connection details are now read from the WiFi service over `/dev/binder` instead of `termux-api WifiConnectionInfo`, so the Termux:API app is no longer required and no subprocess is spawned. + * The interface name and its state, the connection state and the Wi-Fi standard are now reported as well. + * This needs Android 11 (API 30), the release that moved the Wi-Fi framework into an APEX. There is no fallback, so Android 10 and older report an error instead. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/CMakeLists.txt b/CMakeLists.txt index 018c7d2ba7..bf2175145b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -616,6 +616,7 @@ if(LINUX) elseif(ANDROID) list(APPEND LIBFASTFETCH_SRC src/common/android/binder.c + src/common/android/dex.c src/common/impl/dbus.c src/common/impl/io_unix.c src/common/impl/netif_linux.c diff --git a/src/common/android/dex.c b/src/common/android/dex.c new file mode 100644 index 0000000000..7fdb90458e --- /dev/null +++ b/src/common/android/dex.c @@ -0,0 +1,358 @@ +#include "common/android/dex.h" + +#include +#include +#include +#include + +#ifdef FF_HAVE_ZLIB + #include "common/library.h" + #include +#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. The central directory would carry the same +// fields, but the local header sits right in front of the data, so one walk over the file finds +// both the bounds and the payload. +#define FF_ZIP_LOCAL_HEADER_SIZE 30 +#define FF_ZIP_OFF_METHOD 8 +#define FF_ZIP_OFF_COMPRESSED_SIZE 18 +#define FF_ZIP_OFF_UNCOMPRESSED_SIZE 22 +#define FF_ZIP_OFF_NAME_LENGTH 26 +#define FF_ZIP_OFF_EXTRA_LENGTH 28 +#define FF_ZIP_METHOD_STORED 0 +#define FF_ZIP_METHOD_DEFLATED 8 + +#define FF_DEX_ENTRY "classes.dex" +#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); +} + +// LEB128, at most five bytes for the 32-bit values a dex stores this way. +static uint32_t dexUleb128(const uint8_t** p) { + uint32_t value = 0; + for (int shift = 0; shift <= 28; shift += 7) { + const uint8_t byte = *(*p)++; + value |= (uint32_t) (byte & 0x7f) << shift; + if ((byte & 0x80) == 0) { + break; + } + } + return value; +} + +// encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes. A +// `static final int` constant is written as VALUE_INT, whose payload is a sign-extended +// little-endian integer. Reading through 64 bits keeps every shift in range even if the size field +// is nonsense; the caller's length check is what rejects such a dex. +static int32_t dexEncodedInt(const uint8_t** p) { + const uint8_t header = *(*p)++; + const uint32_t size = (uint32_t) (header >> 5) + 1; + uint64_t value = 0; + for (uint32_t i = 0; i < size && i < 8; ++i) { + value |= (uint64_t) (*(*p)++) << (i * 8); + } + const uint32_t bits = size * 8; + if (bits < 64 && (value & (1ull << (bits - 1))) != 0) { + value |= ~((1ull << bits) - 1); + } + return (int32_t) (uint32_t) value; +} + +// 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); + if (ids < dex || (size_t) (end - ids) < (size_t) index * 4 + 4) { + return nullptr; + } + const uint8_t* p = dex + dexU32(ids + (size_t) index * 4); + if (p < dex || p >= end) { + return nullptr; + } + (void) dexUleb128(&p); + if (p >= end) { + return nullptr; + } + return memchr(p, '\0', (size_t) (end - p)) != nullptr ? (const char*) p : nullptr; +} + +// Walks the class's static field list and the class's static value array in step, and returns the +// value paired with `fieldName`. Both lists are ordered by field index and cover exactly the same +// fields -- when they do not, the pairing is not trustworthy and nothing is returned. +static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, int32_t* result) { + const uint8_t* end = dex + size; + + 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"; + } + end = dex + fileSize; + + // The class's type index, so that the class_def_item and the field ids can be filtered by class. + const uint8_t* types = dex + dexU32(dex + FF_DEX_OFF_TYPE_IDS); + const uint32_t typeCount = dexU32(dex + FF_DEX_OFF_TYPE_IDS_SIZE); + if (types < dex || (size_t) (end - types) < (size_t) typeCount * 4) { + return "The dex type table is out of range"; + } + uint32_t typeIndex = UINT32_MAX; + for (uint32_t i = 0; i < typeCount; ++i) { + const char* descriptor = dexString(dex, end, dexU32(types + (size_t) i * 4)); + if (descriptor != nullptr && strcmp(descriptor, classDescriptor) == 0) { + typeIndex = i; + break; + } + } + if (typeIndex == UINT32_MAX) { + return "The class is not in the dex"; + } + + 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 (classes < dex || (size_t) (end - classes) < (size_t) classCount * FF_DEX_CLASS_DEF_SIZE) { + return "The dex class table is out of range"; + } + + for (uint32_t i = 0; i < classCount; ++i) { + const uint8_t* classDef = classes + (size_t) i * FF_DEX_CLASS_DEF_SIZE; + if (dexU32(classDef) != typeIndex) { + continue; + } + + 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); + if (fields < dex || fields >= end || values < dex || values >= end) { + return "The dex class data is out of range"; + } + + const uint32_t staticFields = dexUleb128(&fields); + (void) dexUleb128(&fields); // instance_fields_size + (void) dexUleb128(&fields); // direct_methods_size + (void) dexUleb128(&fields); // virtual_methods_size + const uint32_t staticValues = dexUleb128(&values); + 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) { + if (fields >= end) { + return "The dex static field list is truncated"; + } + fieldIndex += dexUleb128(&fields); // field_idx_diff + (void) dexUleb128(&fields); // access_flags + const int32_t value = dexEncodedInt(&values); + + if ((size_t) (end - fieldIds) < ((size_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 && strcmp(name, fieldName) == 0) { + *result = value; + return nullptr; + } + } + return "The field is not a static field of the class"; + } + return "The class has no class_def_item"; +} + +// --------------------------------------------------------------------------------------------- +// 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`, or `inflated` + size_t size; + uint8_t* inflated; // owned; only set when the entry had to be decompressed +} FFDexMapping; + +static void wrapDexMapping(FFDexMapping* mapping) { + assert(mapping); + free(mapping->inflated); + if (mapping->mapped != nullptr) { + munmap(mapping->mapped, mapping->mappedSize); + } +} + +// The local file header of `classes.dex` carries both the payload bounds and its compression, so a +// single walk over the jar is enough to find it either way. AOSP builds framework jars with the +// entry STORED so that ART can map it, which is the common case and needs no decompression. +static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) { + for (size_t i = 0; i + FF_ZIP_LOCAL_HEADER_SIZE <= jarSize; ++i) { + if (memcmp(jar + i, "PK\x03\x04", 4) != 0) { + continue; + } + const uint16_t nameLength = dexU16(jar + i + FF_ZIP_OFF_NAME_LENGTH); + const uint16_t extraLength = dexU16(jar + i + FF_ZIP_OFF_EXTRA_LENGTH); + if (nameLength != sizeof(FF_DEX_ENTRY) - 1) { + continue; + } + const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) nameLength + (size_t) extraLength; + if (headerSize > jarSize - i) { + continue; + } + if (memcmp(jar + i + FF_ZIP_LOCAL_HEADER_SIZE, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY) - 1) != 0) { + continue; + } + + const uint32_t compressedSize = dexU32(jar + i + FF_ZIP_OFF_COMPRESSED_SIZE); + if (compressedSize == 0 || compressedSize > jarSize - i - headerSize) { + // A zero size means a data descriptor follows the payload instead of the header; the + // magic scan in the caller still covers that case as long as the entry is STORED. + continue; + } + *method = dexU16(jar + i + FF_ZIP_OFF_METHOD); + *uncompressedSize = dexU32(jar + i + FF_ZIP_OFF_UNCOMPRESSED_SIZE); + *data = jar + i + headerSize; + *dataSize = compressedSize; + return nullptr; + } + return "The jar has no usable classes.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. +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") + + uint8_t* buffer = malloc(uncompressedSize); + if (buffer == nullptr) { + 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; + + if (ffinflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) { + free(buffer); + return "inflateInit2 failed"; + } + const int status = ffinflate(&stream, Z_FINISH); + ffinflateEnd(&stream); + if (status != Z_STREAM_END || stream.total_out != (uLong) uncompressedSize) { + free(buffer); + return "Inflating classes.dex failed"; + } + + *out = buffer; + return nullptr; +} +#endif + +const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result) { + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; + + const int fd = open(jarPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "open(jar) failed"; + } + + struct stat st = {}; + if (fstat(fd, &st) != 0 || st.st_size <= 0) { + close(fd); + return "fstat(jar) failed"; + } + mapping.mappedSize = (size_t) st.st_size; + mapping.mapped = mmap(nullptr, mapping.mappedSize, PROT_READ, MAP_PRIVATE, fd, 0); + // The mapping outlives the descriptor, so the descriptor can go either way from here. + close(fd); + if (mapping.mapped == MAP_FAILED) { + mapping.mapped = nullptr; + return "mmap(jar) failed"; + } + + const uint8_t* data = nullptr; + size_t dataSize = 0; + uint32_t uncompressedSize = 0; + uint16_t method = FF_ZIP_METHOD_STORED; + + if (findDexEntry(mapping.mapped, mapping.mappedSize, &data, &dataSize, &uncompressedSize, &method) != nullptr) { + data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize); + if (data == nullptr) { + return "No dex in the jar"; + } + } else if (method == FF_ZIP_METHOD_STORED) { + dataSize = uncompressedSize; + } else if (method == FF_ZIP_METHOD_DEFLATED) { + #ifdef FF_HAVE_ZLIB + const char* error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated); + if (error != nullptr) { + return error; + } + data = mapping.inflated; + dataSize = uncompressedSize; + #else + return "The jar deflates classes.dex and fastfetch was built without zlib"; + #endif + } else { + return "classes.dex uses an unsupported compression method"; + } + + mapping.data = data; + mapping.size = dataSize; + return dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, result); +} diff --git a/src/common/android/dex.h b/src/common/android/dex.h new file mode 100644 index 0000000000..34197506a5 --- /dev/null +++ b/src/common/android/dex.h @@ -0,0 +1,30 @@ +#pragma once + +// AIDL gives every method of an interface a transaction code at build time: +// +// static final int TRANSACTION_ = IBinder.FIRST_CALL_TRANSACTION + ; +// +// 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" + +// Resolves the int value of `.` out of the `classes.dex` of `jarPath`. +// `classDescriptor` is the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;". +// Returns nullptr on success, a static message otherwise. +[[gnu::nonnull(1, 2, 3, 4), nodiscard]] const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result); diff --git a/src/detection/wifi/wifi_android.c b/src/detection/wifi/wifi_android.c index 0fd5b3a9d5..706469bb71 100644 --- a/src/detection/wifi/wifi_android.c +++ b/src/detection/wifi/wifi_android.c @@ -1,33 +1,663 @@ #include "wifi.h" -#include "common/processing.h" -#include "common/properties.h" +#include "common/android/api.h" +#include "common/android/binder.h" +#include "common/android/dex.h" +#include "common/debug.h" -#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" -#define FF_TERMUX_API_PARAM "WifiConnectionInfo" +#include +#include +#include -static inline void wrapYyjsonFree(yyjson_doc** doc) { - assert(doc); - if (*doc) { - yyjson_doc_free(*doc); +// Android gives an app two ways to see the current connection, and both are weaker than the desktop +// ones. `/system/bin/cmd wifi status` prints everything fastfetch wants, but it does not run in the +// calling process: `cmd` hands the arguments to the service over binder and the command body runs +// inside system_server, where it reads a locally built WifiInfo. An app can therefore not ask for +// it -- the call comes back as a silent exit status 255 with nothing on either stream, which is +// what makes it look like it worked. `dumpsys wifi` needs android.permission.DUMP, and +// /proc/net/wireless, /proc/net/dev and /sys/class/net/wlan0 are all EACCES. That leaves +// `IWifiManager.getConnectionInfo`, which needs only ACCESS_WIFI_STATE -- a normal permission, +// granted without asking. There is no Termux:API fallback: it returned less than this does, and +// needed a separate app installed. +// +// The transaction code is read out of the device's own jar rather than carried as a table, so the +// module is not tied to a list of releases. It does need the jar to exist, and Android 11 is the +// release that moved the Wi-Fi framework into an APEX -- before that the class sat in framework.jar, +// which is not read here. So Android 11 (API 30) is the floor, and an older release reports an error +// rather than a guess. +// +// The reply layout itself has been checked against the AOSP source as far back as Android 9, which +// is why nothing about it is release-gated: the head grew a link speed in Android 10 and a Wi-Fi +// standard in Android 11, and both are found by shape. +// +// Whether the radio is on is a second call, `IWifiManager.getWifiEnabledState`, which takes no +// arguments and answers with a `WifiManager.WIFI_STATE_*`. It is needed because the interface +// cannot answer for itself: bionic's `getifaddrs` lists only the interfaces that hold an address, +// so the flags that would say whether `wlan0` is up leave the list exactly when there is no +// association -- and nothing else in the reply separates a Wi-Fi that is off from one that is on +// with nothing behind it. Both answer with an empty `WifiInfo`, the same at every offset, down to +// the `02:00:00:00:00:00` placeholder it writes in place of a BSSID. The call is made only when the +// interface was not in the list, so a connected device -- where `IFF_UP` is both closer to hand and +// more accurate -- pays nothing for it. +// +// Three things about the call are positional rather than negotiated, and all three were measured +// on a vivo V2505A (Android 16) and a Redmi 9A (Android 11): +// +// * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on +// Android 11 and 41 on Android 16) -- see common/android/dex.h. +// * The reply layout drifts, and not only by release. Android 12 dropped the duplicate length word +// that Android 11 and 10 wrote in front of the SSID octets, and the vivo's Android 16 build +// fills an int between the transmit and the receive speed that the Redmi's Android 11 build +// leaves out -- while AOSP's own Android 16 writes the head exactly the Android 11 way, so that +// one int is a vendor addition rather than a release difference. Frequency is the only channel +// centre in the head, so it is located by value, with the whole result validated before it is +// used. A wrong guess shows up as a rejected layout, not as a plausible wrong number. +// * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every +// vendor: the Android 11 build on the Redmi carries three words there that AOSP's own Android 11 +// does not. It is located by shape for that reason. +// +// BSSID and MAC are the one place the binder route beats `cmd wifi status`: MAC addresses are +// redacted for a caller targeting a recent SDK, and the shell targets the current one, so `cmd wifi +// status` prints 24:**:**:**:70:2c where this reads 24:a4:87:3c:70:2c. +// +// What is deliberately left empty: +// +// * `security`. `WifiInfo` carries no security type on Android 11 at all, and on Android 16 the +// value the shell command prints is not in the parcel either -- `cmd wifi status` shows it only +// because it reads the object in system_server. No other call an app can make returns it, so +// the field stays empty rather than being guessed. +// * The IPv4 address. It is in the parcel, but FFWifiConnection has no field for it and +// `inf.description` is the interface name, as on every other platform. The LocalIP module +// reports addresses. +// * `inf.description` while the interface is not in the address list. Its name is not knowable +// from anywhere else -- sysfs and /proc/net are EACCES -- so the field stays empty rather than +// naming an interface that was guessed at. `inf.status` is still reported, from the radio. + +#define FF_WIFI_ANDROID_SERVICE "wifi" +#define FF_WIFI_ANDROID_DESCRIPTOR "android.net.wifi.IWifiManager" + +// Android 11 is the release that moved the Wi-Fi framework out of framework.jar and into an APEX, +// which is where this jar is. Nothing older carries it, so there is no second path to try: the +// class is either in the APEX or the release is too old. +#define FF_WIFI_ANDROID_JAR "/apex/com.android.wifi/javalib/framework-wifi.jar" +#define FF_WIFI_ANDROID_STUB "Landroid/net/wifi/IWifiManager$Stub;" +#define FF_WIFI_ANDROID_GET_CONNECTION_INFO "TRANSACTION_getConnectionInfo" +// Whether the radio is on is the one thing the connection does not say. An empty `WifiInfo` is what +// the service answers with both while Wi-Fi is off and while it is on with nothing associated, so +// the two states are told apart by asking the service directly. The method takes no arguments and +// answers with a `WifiManager.WIFI_STATE_*`. +#define FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE "TRANSACTION_getWifiEnabledState" +#define FF_WIFI_ANDROID_WIFI_STATE_DISABLED 1 +#define FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN 4 + +// The other argument is the caller's package name. The shell UID owns exactly one package and the +// service accepts that name, which is what `cmd` and `dumpsys` pass. See getOwnPackage(). +#define FF_WIFI_ANDROID_SHELL_UID 2000 +#define FF_WIFI_ANDROID_SHELL_PACKAGE "com.android.shell" + +// An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker; +// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed have held those +// offsets from Android 9 to 16, and the transmit speed joined them in Android 10 without moving +// since, so all four are read at fixed offsets. Everything after them is located by shape. +#define FF_WIFI_ANDROID_OFF_NET_ID 8 +#define FF_WIFI_ANDROID_OFF_RSSI 12 +#define FF_WIFI_ANDROID_OFF_LINK_SPEED 16 +#define FF_WIFI_ANDROID_OFF_TX_LINK_SPEED 20 + +// A `WifiInfo` with no connection to describe carries the sentinels for the two fields that are +// always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot +// measure. Both were measured on the vivo's Android 16 reply, which is what the service answers +// while Wi-Fi is on with nothing associated -- the head holds no frequency and no SSID at all, only +// the "02:00:00:00:00:00" placeholder that `WifiInfo` writes in place of a BSSID. +#define FF_WIFI_ANDROID_NET_ID_NONE (-1) +#define FF_WIFI_ANDROID_RSSI_NONE (-127) + +// Frequency is looked for in the head, after the link speeds and before the address. See the note +// above for why it is not at a fixed offset. +#define FF_WIFI_ANDROID_FREQ_SCAN_FROM 0x14 +#define FF_WIFI_ANDROID_FREQ_SCAN_TO 0x34 + +// Distances from Frequency. AOSP writes an address as a flag byte followed by a byte array, and +// omits the array entirely when there is no IPv4 address -- an IPv6-only network does that -- which +// shifts everything after it by two words. The base of the SSID section is therefore derived from +// that flag rather than fixed, and only the SSID's own offset is left as a pair: Android 11 wrote +// the length twice in front of the octets and Android 12 dropped the duplicate. +// +// The SSID is the one field WifiInfo writes as raw bytes rather than as a UTF-16 string, so its +// length is a byte count and a NUL among the bytes means the offset was wrong. +#define FF_WIFI_ANDROID_FREQ_HAS_IP 0x04 +#define FF_WIFI_ANDROID_FREQ_IP_LENGTH 0x08 +#define FF_WIFI_ANDROID_FREQ_SSID_BASE 0x10 +#define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08 +#define FF_WIFI_ANDROID_SSID_LENGTH 0x04 + +// The receive speed is the word in front of Frequency; the transmit speed is the word after the link +// speed. The vivo writes one extra word between those two that AOSP does not -- it repeats the +// transmit speed -- so the receive speed can only be reached from Frequency, and Frequency cannot be +// reached by stepping over the two link speeds. Both were checked against `cmd wifi status` at the +// same moment: 131/219 on the vivo, 86/-1 on the Redmi. +#define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04 +#define FF_WIFI_ANDROID_SSID 0x08 // Android 12 and later +#define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older +#define FF_WIFI_ANDROID_SSID_MAX_LENGTH 32 + +// `WifiInfo` writes the Wi-Fi standard as one of the `ScanResult.WIFI_STANDARD_*` values. It is +// followed by the two maximum-supported link speeds, which is what makes it recognisable: an int +// naming a standard, then two plausible rates. The band is checked against it as well, so a value +// that only fits another band is rejected. A layout that is not recognised leaves `protocol` empty +// instead of reporting the wrong standard. +#define FF_WIFI_ANDROID_STANDARD_LEGACY 1 +#define FF_WIFI_ANDROID_STANDARD_11A 2 +#define FF_WIFI_ANDROID_STANDARD_11B 3 +#define FF_WIFI_ANDROID_STANDARD_11N 4 +#define FF_WIFI_ANDROID_STANDARD_11AC 5 +#define FF_WIFI_ANDROID_STANDARD_11AX 6 +#define FF_WIFI_ANDROID_STANDARD_11AD 7 +#define FF_WIFI_ANDROID_STANDARD_11BE 8 +#define FF_WIFI_ANDROID_RATE_MAX 20000 + +// The whole WifiInfo parcel measured 1340 bytes on Android 16, most of it the MLO and ANQP tail. +#define FF_WIFI_ANDROID_REPLY_SIZE 4096 + +// The layout is found by shape rather than by offset, so a rejected reply leaves nothing to go on. +// The debug dump covers the head -- where the frequency, the address flag and the SSID sit -- and +// stops before the tail, which is the long MLO and ANQP list and takes no part in locating +// anything. Read with `fastfetch -s Wifi` under a debug build. +#define FF_WIFI_ANDROID_DEBUG_DUMP_SIZE 0x60 + +// --------------------------------------------------------------------------------------------- +// Binder +// --------------------------------------------------------------------------------------------- + +// `getConnectionInfo` takes the caller's package name, and whether it is checked depends on the +// release: the vivo answered "Package com.termux does not belong to 2000" for a name the shell does +// not own, while the Redmi accepted the same name. Passing the real one is what works on both. 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 -- but the executable path carries it: an app's binaries live under +// /data/data// or /data/user///, and /proc/self/exe resolves there. +// +// A binary outside those directories has no package of its own, and that is not an edge case: a +// static build pushed to /data/local/tmp is how this runs on a device without Termux, and the Redmi +// reported "Cannot determine the package name of this process" for exactly that. The shell UID can +// be answered instead, because it has one name the service accepts. No other UID reaches here: an +// app's own binaries are always under its data directory, so a failure there is a real failure. +static bool getOwnPackage(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 (strncmp(path, dataPrefix, strlen(dataPrefix)) == 0) { + rest = path + strlen(dataPrefix); + } else if (strncmp(path, userPrefix, strlen(userPrefix)) == 0) { + rest = strchr(path + strlen(userPrefix), '/'); + if (rest != nullptr) { + rest += 1; + } + } + if (rest == nullptr) { + FF_DEBUG("The executable is not in an app data directory (\"%s\"), uid %u", path, (unsigned) getuid()); + if (getuid() != FF_WIFI_ANDROID_SHELL_UID) { + return false; + } + static const char shellPackage[] = FF_WIFI_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; } -const char* ffDetectWifi(FFlist* result) { - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); +static bool isMacAddress(const char* value, uint32_t length) { + if (length != 17) { + return false; + } + for (uint32_t i = 0; i < length; ++i) { + const char c = value[i]; + if (i % 3 == 2) { + if (c != ':') { + return false; + } + } else if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; +} + +// SupplicantState, in the order AOSP declares it. +static const char* const FF_WIFI_ANDROID_STATES[] = { + "DISCONNECTED", "INTERFACE_DISABLED", "INACTIVE", "SCANNING", "AUTHENTICATING", "ASSOCIATING", + "ASSOCIATED", "FOUR_WAY_HANDSHAKE", "GROUP_HANDSHAKE", "COMPLETED", "DORMANT", "UNINITIALIZED", "INVALID", +}; + +static bool isSupplicantState(const char* value, uint32_t length) { + for (uint32_t i = 0; i < ARRAY_SIZE(FF_WIFI_ANDROID_STATES); ++i) { + if (strlen(FF_WIFI_ANDROID_STATES[i]) == length && memcmp(FF_WIFI_ANDROID_STATES[i], value, length) == 0) { + return true; + } + } + return false; +} + +typedef struct FFWifiAndroidConnection { + char interface[IF_NAMESIZE + 1]; + char state[24]; + char ssid[FF_WIFI_ANDROID_SSID_MAX_LENGTH + 1]; + char bssid[18]; + int32_t rssi; + int32_t linkSpeed; + int32_t txLinkSpeed; + int32_t rxLinkSpeed; + int32_t standard; + size_t stateEnd; + uint16_t frequency; + bool up; + bool upKnown; // whether the state of the interface was established at all + bool connected; +} FFWifiAndroidConnection; + +// The Wi-Fi interface is the one the HAL names `wlan*`, and it is looked up rather than assumed so +// that a second one (`wlan1` on a dual-STA device) is still reported by name. The flags describe the +// interface, so the first entry for a name is as good as any other. +// +// What bionic's `getifaddrs` will not do is list an interface that has no address: the list is +// built out of the addresses, so `wlan0` leaves it as soon as nothing is associated -- measured on +// the vivo, and the Redmi behaves the same. That is exactly the state its flags are wanted in, so +// when the name is missing the radio state stands in for them. See detectWithBinder(). +static void detectInterface(FFWifiAndroidConnection* connection) { + struct ifaddrs* addrs = nullptr; + if (getifaddrs(&addrs) != 0) { + FF_DEBUG("getifaddrs failed, so neither the interface name nor its state is known"); + return; + } + + for (const struct ifaddrs* ifa = addrs; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_name == nullptr || strncmp(ifa->ifa_name, "wlan", 4) != 0) { + continue; + } + snprintf(connection->interface, sizeof(connection->interface), "%s", ifa->ifa_name); + connection->up = (ifa->ifa_flags & IFF_UP) != 0; + connection->upKnown = true; + FF_DEBUG("Interface \"%s\" is %s (IFF_UP %s)", connection->interface, connection->up ? "up" : "down", connection->up ? "set" : "clear"); + break; + } + freeifaddrs(addrs); + if (connection->interface[0] == '\0') { + FF_DEBUG("No interface named wlan* is present"); + } +} + +static const char* wifiStandardName(int32_t standard) { + switch (standard) { + case FF_WIFI_ANDROID_STANDARD_LEGACY: return "802.11"; + case FF_WIFI_ANDROID_STANDARD_11A: return "802.11a"; + case FF_WIFI_ANDROID_STANDARD_11B: return "802.11b"; + case FF_WIFI_ANDROID_STANDARD_11N: return "802.11n (Wi-Fi 4)"; + case FF_WIFI_ANDROID_STANDARD_11AC: return "802.11ac (Wi-Fi 5)"; + case FF_WIFI_ANDROID_STANDARD_11AX: return "802.11ax (Wi-Fi 6)"; + case FF_WIFI_ANDROID_STANDARD_11AD: return "802.11ad (WiGig)"; + case FF_WIFI_ANDROID_STANDARD_11BE: return "802.11be (Wi-Fi 7)"; + default: return nullptr; + } +} + +// 11ac and 11ad live in bands the connection is not in when it reports a 2.4 GHz frequency, so a +// candidate that disagrees with the band is a misread rather than a standard. +static bool isStandardPlausible(int32_t standard, uint16_t frequency) { + if (frequency < 3000 && (standard == FF_WIFI_ANDROID_STANDARD_11AC || standard == FF_WIFI_ANDROID_STANDARD_11AD)) { + return false; + } + if (frequency > 50000 && standard != FF_WIFI_ANDROID_STANDARD_11AD) { + return false; + } + return true; +} + +// WifiInfo writes the BSSID, the MAC and the supplicant state as length-prefixed UTF-16 strings, and +// their distance from the head depends on how long the SSID is and on which release wrote the +// parcel. They are recognised by shape instead of by offset: a 17 character `hh:hh:hh:hh:hh:hh`, and +// one of the thirteen state names. The BSSID is written before the MAC, so the first match is the +// one wanted. `from` is the end of the SSID octets, which is where the BSSID follows. +static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAndroidConnection* connection) { + for (size_t offset = from; offset + 4 <= size; ++offset) { + const uint32_t length = ffBinderReadU32(data, size, offset); + if (length == 0 || length > 24 || offset + 4 + (size_t) length * 2 > size) { + continue; + } + + const uint8_t* chars = data + offset + 4; + char buffer[25]; + bool ascii = true; + for (uint32_t i = 0; i < length; ++i) { + if (chars[i * 2 + 1] != 0 || chars[i * 2] < 0x20 || chars[i * 2] >= 0x7f) { + ascii = false; + break; + } + buffer[i] = (char) chars[i * 2]; + } + if (!ascii) { + continue; + } + buffer[length] = '\0'; + + if (connection->bssid[0] == '\0' && isMacAddress(buffer, length)) { + memcpy(connection->bssid, buffer, length + 1); + } else if (connection->state[0] == '\0' && isSupplicantState(buffer, length)) { + memcpy(connection->state, buffer, length + 1); + // The state is the last string before the tail, so where it ends is where the tail + // starts. A Parcel pads a string to the next word, terminator included. + connection->stateEnd = offset + 4 + ((length * 2 + 2 + 3) & ~3u); + } + if (connection->bssid[0] != '\0' && connection->state[0] != '\0') { + return; + } + } +} + +// AOSP writes the Wi-Fi standard between three nullable strings and the two maximum-supported link +// speeds, all of them ints. Vendors insert fields around that run -- the Android 11 build on the +// Redmi carries three words before the standard that AOSP's own Android 11 does not -- so a fixed +// offset would be a guess. The run is recognised by shape instead, searching forward from the end of +// the supplicant state: an int naming a standard, followed by two rates, in a band the standard can +// live in. +static void findWifiStandard(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) { + if (connection->stateEnd == 0) { + return; // without the state string there is no trustworthy place to start from + } + + for (size_t offset = connection->stateEnd; offset + 12 <= size; offset += 4) { + const int32_t standard = ffBinderReadI32(data, size, offset); + if (wifiStandardName(standard) == nullptr || !isStandardPlausible(standard, connection->frequency)) { + continue; + } + const int32_t maxTx = ffBinderReadI32(data, size, offset + 4); + const int32_t maxRx = ffBinderReadI32(data, size, offset + 8); + if (maxTx < 0 || maxTx > FF_WIFI_ANDROID_RATE_MAX || maxRx < 0 || maxRx > FF_WIFI_ANDROID_RATE_MAX) { + continue; + } + connection->standard = standard; + FF_DEBUG("Wi-Fi standard %d at +0x%zx, maximum speeds %d/%d", standard, offset, maxTx, maxRx); + return; + } + FF_DEBUG("No Wi-Fi standard matched after the supplicant state at +0x%zx", connection->stateEnd); +} + +#ifndef NDEBUG +// The reply is read by shape, so a reply that fails every shape leaves nothing behind to diagnose +// it with: the bytes are the only evidence. Each line carries the words next to their bytes, +// because what the reader is looking for -- a value that would pass for a frequency, an address +// flag, a string length -- is an int. +static void debugDumpReply(const uint8_t* data, size_t size) { + const size_t limit = size < FF_WIFI_ANDROID_DEBUG_DUMP_SIZE ? size : FF_WIFI_ANDROID_DEBUG_DUMP_SIZE; + for (size_t offset = 0; offset < limit; offset += 16) { + char hex[16 * 3 + 1]; + size_t used = 0; + for (size_t i = 0; i < 16 && offset + i < limit; ++i) { + used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]); + } + FF_DEBUG(" +0x%02zx %-47s | %d %d %d %d", offset, hex, + ffBinderReadI32(data, size, offset), ffBinderReadI32(data, size, offset + 4), + ffBinderReadI32(data, size, offset + 8), ffBinderReadI32(data, size, offset + 12)); + } +} + #define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size) +#else + #define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) ((void) 0) +#endif + +static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) { + if (size < FF_WIFI_ANDROID_OFF_TX_LINK_SPEED + 4) { + FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size); + return "The reply is too short to hold a WifiInfo"; + } + const int32_t exception = ffBinderReadI32(data, size, 0); + if (exception != 0) { + FF_DEBUG("The Wifi service raised exception %d", exception); + return "The Wifi service raised an exception"; + } + if (ffBinderReadU32(data, size, 4) == 0) { + // A null WifiInfo is what the service returns when it has nothing to report, which is the + // normal reply while Wi-Fi is off. The interface, when it is still there, is reported as + // disconnected. + FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size); + return nullptr; + } + FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d, tx link speed %d", + size, + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID), + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI), + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED), + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED)); + + // The two orders WifiSsid has written its length in: once before the octets on Android 11 and + // older, not at all on Android 12 and later, which leaves the octets one word earlier. The one + // that matches this release is tried first. + uint32_t ssidOffsets[2] = { FF_WIFI_ANDROID_SSID_LEGACY, FF_WIFI_ANDROID_SSID }; + if (FF_API_AT_LEAST(31)) { + ssidOffsets[0] = FF_WIFI_ANDROID_SSID; + ssidOffsets[1] = FF_WIFI_ANDROID_SSID_LEGACY; + } + + for (uint32_t base = FF_WIFI_ANDROID_FREQ_SCAN_FROM; base <= FF_WIFI_ANDROID_FREQ_SCAN_TO; base += 4) { + const uint16_t frequency = (uint16_t) ffBinderReadU32(data, size, base); + if (frequency == 0 || ffWifiFreqToChannel(frequency) == 0) { + continue; + } + FF_DEBUG("+0x%02x holds %u, which is channel %u", base, frequency, ffWifiFreqToChannel(frequency)); + + const int32_t hasIp = ffBinderReadI32(data, size, base + FF_WIFI_ANDROID_FREQ_HAS_IP); + if (hasIp != 0 && hasIp != 1) { + FF_DEBUG(" rejected: +0x%02x is not an address flag but %d", base + FF_WIFI_ANDROID_FREQ_HAS_IP, hasIp); + continue; + } + const uint32_t ipLength = ffBinderReadU32(data, size, base + FF_WIFI_ANDROID_FREQ_IP_LENGTH); + if (hasIp == 1 && ipLength != 4) { + FF_DEBUG(" rejected: the address at +0x%02x is %u bytes long", base + FF_WIFI_ANDROID_FREQ_IP_LENGTH, ipLength); + continue; + } + const uint32_t ssidBase = base + (hasIp == 1 ? FF_WIFI_ANDROID_FREQ_SSID_BASE : FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP); + const int32_t ssidPresent = ffBinderReadI32(data, size, ssidBase); + if (ssidPresent != 1) { + FF_DEBUG(" rejected: the SSID at +0x%02x is marked %d, not present", ssidBase, ssidPresent); + continue; // the SSID of a reported connection is never null + } + + const uint32_t ssidLength = ffBinderReadU32(data, size, ssidBase + FF_WIFI_ANDROID_SSID_LENGTH); + if (ssidLength == 0 || ssidLength > FF_WIFI_ANDROID_SSID_MAX_LENGTH) { + FF_DEBUG(" rejected: the SSID length at +0x%02x is %u", ssidBase + FF_WIFI_ANDROID_SSID_LENGTH, ssidLength); + continue; + } + + for (uint32_t i = 0; i < ARRAY_SIZE(ssidOffsets); ++i) { + const size_t ssidOffset = ssidBase + ssidOffsets[i]; + if (ssidOffset + ssidLength > size) { + continue; + } + + // The SSID is a raw byte string, so a NUL in it means this was not the SSID after all. + if (memchr(data + ssidOffset, '\0', ssidLength) != nullptr) { + FF_DEBUG(" rejected: the %u SSID bytes at +0x%zx hold a NUL", ssidLength, ssidOffset); + continue; + } + memcpy(connection->ssid, data + ssidOffset, ssidLength); + connection->ssid[ssidLength] = '\0'; + + connection->frequency = frequency; + connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI); + connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED); + connection->txLinkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED); + connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED); + + findStrings(data, size, ssidOffset + ssidLength, connection); + FF_DEBUG("SSID \"%s\" at +0x%zx, BSSID \"%s\", supplicant state \"%s\"", + connection->ssid, ssidOffset, connection->bssid, connection->state); + // The supplicant state is the authoritative signal; the network id is the fallback for a + // parcel whose state string was not recognised. + connection->connected = connection->state[0] != '\0' + ? strcmp(connection->state, "COMPLETED") == 0 + : ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) >= 0; + findWifiStandard(data, size, connection); + FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s", + connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed, + connection->connected ? "yes" : "no"); + return nullptr; + } + } + + // Nothing in the head matched, but an unassociated connection has nothing in the head to match: + // no frequency, no SSID and -- unlike a connection -- the two sentinels `WifiInfo` fills in when + // it has nothing to describe. That reply is the service saying there is nothing to report, which + // is a state and not a failure. A reply carrying a network id of its own is a connection, so a + // shape that still does not match one keeps being reported as a failure. + if (ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) == FF_WIFI_ANDROID_NET_ID_NONE && + ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI) == FF_WIFI_ANDROID_RSSI_NONE) { + FF_DEBUG("No network id and no signal in the %zu byte reply: there is no connection to report", size); + return nullptr; + } + + FF_DEBUG("No frequency, no SSID and no supplicant state matched in this reply:"); + FF_WIFI_ANDROID_DEBUG_DUMP(data, size); + return "The Wifi service returned a layout fastfetch does not understand"; +} + +// Which of the speeds the service wrote becomes a rate. It writes a negative one when it does not +// know, which is what the Redmi does for the receive side, and a link speed of 0 when the link is +// down. `generic` is the fallback for a direction the service left unset: the transmit side has one +// -- the generic link speed is a real negotiated rate, just not direction-specific -- the way the +// Linux backend falls back to SIOCGIWRATE when the station info carries no bitrate. There is no +// generic receive speed, so 0 is passed there. +static double wifiAndroidRate(int32_t specific, int32_t generic) { + const int32_t rate = specific > 0 ? specific : generic; + return rate > 0 && rate <= FF_WIFI_ANDROID_RATE_MAX ? (double) rate : -DBL_MAX; +} + +// Calls a method of the service that takes no arguments and answers with an int. The transaction +// code comes out of the jar the same way `getConnectionInfo`'s does, so a release that does not +// declare the method answers UNKNOWN_TRANSACTION instead of a number that means something else. +static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* transactionField, int32_t* result) { + int32_t transaction = 0; + const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, transactionField, &transaction); + if (error != nullptr) { + return error; + } + + uint8_t parcelBuffer[256]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR); - if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, nullptr })) { - return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; + uint8_t replyBuffer[64]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(binder, handle, (uint32_t) transaction, &parcel, &reply); + if (error != nullptr) { + return error; } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", transactionField, transaction); + return "The Wifi service does not answer that request"; + } + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("\"%s\" is transaction %d and raised exception %d", transactionField, transaction, exception); + return "The Wifi service raised an exception"; + } + *result = ffBinderReadI32(reply.data, reply.size, 4); + FF_DEBUG("\"%s\" is transaction %d and answered %d", transactionField, transaction, *result); + return nullptr; +} + +static const char* detectWithBinder(FFlist* result) { + char package[128]; + if (!getOwnPackage(package, sizeof(package))) { + return "Cannot determine the package name of this process"; + } + + int32_t transaction = 0; + const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &transaction); + if (error != nullptr) { + FF_DEBUG("Reading the transaction code from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error); + return error; + } + + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + error = ffBinderOpen(&binder); + if (error != nullptr) { + return error; + } + + uint32_t handle = 0; + error = ffBinderLookupService(&binder, FF_WIFI_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle); + if (error != nullptr) { + return error; + } + FF_DEBUG("The \"%s\" service is handle %u, %s is transaction %d", + FF_WIFI_ANDROID_SERVICE, handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, transaction); + + uint8_t parcelBuffer[256]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR); + ffBinderParcelPutString16(&parcel, package); + // callingFeatureId is nullable, and a null String is a length of -1 rather than an empty string. + ffBinderParcelPutI32(&parcel, -1); - [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(buffer.chars, buffer.length, 0, nullptr, nullptr); - if (!doc) { - return "Failed to parse wifi connection info"; + uint8_t replyBuffer[FF_WIFI_ANDROID_REPLY_SIZE]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, handle, (uint32_t) transaction, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("The reply carries status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0)); + return "Wifi service rejected the request"; + } + + FFWifiAndroidConnection connection = {}; + detectInterface(&connection); + error = parseConnectionInfo(reply.data, reply.size, &connection); + if (error != nullptr) { + return error; } - yyjson_val* root = yyjson_doc_get_root(doc); - if (!yyjson_is_obj(root)) { - return "Wifi info result is not a JSON object"; + if (!connection.upKnown) { + // `getifaddrs` leaves the interface out while it has no address, so its flags are missing in + // exactly the state they are wanted in. The radio answers for it: a Wi-Fi that is off is one + // whose interface is down, and a Wi-Fi that is on with nothing associated keeps an interface + // that is up. WIFI_STATE_DISABLING is a radio that is still up, so only the one value means + // down, and WIFI_STATE_UNKNOWN is the service declining to say. + int32_t state = 0; + const char* stateError = callIntMethod(&binder, handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &state); + if (stateError == nullptr && state != FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN) { + connection.up = state != FF_WIFI_ANDROID_WIFI_STATE_DISABLED; + connection.upKnown = true; + FF_DEBUG("The radio is %s, which stands in for the interface flags", connection.up ? "on" : "off"); + } else { + FF_DEBUG("The state of the radio is not known: %s", + stateError != nullptr ? stateError : "the service answered WIFI_STATE_UNKNOWN"); + } + } + + // Nothing was found to report on at all. That is a device without a Wi-Fi interface rather than + // one with a Wi-Fi that is off, which has a state to print by now. + if (!connection.upKnown && !connection.connected) { + return "No Wi-Fi interface is present"; } FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); @@ -45,26 +675,40 @@ const char* ffDetectWifi(FFlist* result) { item->conn.channelWidth = 0; item->conn.frequency = 0; - ffStrbufAppendJsonVal(&item->inf.status, yyjson_obj_get(root, "supplicant_state")); - if (!item->inf.status.length) { - ffStrbufAppendS(&item->inf.status, "Unknown"); - return nullptr; + ffStrbufSetS(&item->inf.description, connection.interface); + // The name is empty when the interface was not in the address list, and the state is empty when + // nothing established it: both are left empty rather than guessed at. The module prints this + // state whenever there is no SSID to print instead, which is exactly the case where neither is + // in the reply. + if (connection.upKnown) { + ffStrbufSetStatic(&item->inf.status, connection.up ? "Up" : "Down"); } - - if (!ffStrbufEqualS(&item->inf.status, "COMPLETED")) { + ffStrbufSetStatic(&item->conn.status, connection.connected ? "connected" : "disconnected"); + if (!connection.connected) { + FF_DEBUG("Nothing is associated: interface \"%s\" is \"%s\", the connection is \"%s\"", + connection.interface, item->inf.status.chars, item->conn.status.chars); return nullptr; } - double rssi = yyjson_get_num(yyjson_obj_get(root, "rssi")); - item->conn.signalQuality = rssi >= -50 ? 100 : rssi <= -100 ? 0 - : (rssi + 100) * 2; - - ffStrbufAppendJsonVal(&item->inf.description, yyjson_obj_get(root, "ip")); - ffStrbufAppendJsonVal(&item->conn.bssid, yyjson_obj_get(root, "bssid")); - ffStrbufAppendJsonVal(&item->conn.ssid, yyjson_obj_get(root, "ssid")); - item->conn.frequency = (uint16_t) yyjson_get_int(yyjson_obj_get(root, "frequency_mhz")); - item->conn.txRate = yyjson_get_num(yyjson_obj_get(root, "link_speed_mbps")); - item->conn.channel = ffWifiFreqToChannel(item->conn.frequency); - + const char* protocol = wifiStandardName(connection.standard); + if (protocol != nullptr) { + ffStrbufSetStatic(&item->conn.protocol, protocol); + } + item->conn.signalQuality = connection.rssi >= -50 ? 100 : connection.rssi <= -100 ? 0 + : (connection.rssi + 100) * 2; + ffStrbufSetS(&item->conn.bssid, connection.bssid); + ffStrbufSetS(&item->conn.ssid, connection.ssid); + item->conn.frequency = connection.frequency; + item->conn.txRate = wifiAndroidRate(connection.txLinkSpeed, connection.linkSpeed); + item->conn.rxRate = wifiAndroidRate(connection.rxLinkSpeed, 0); + item->conn.channel = ffWifiFreqToChannel(connection.frequency); + FF_DEBUG("\"%s\" %s: \"%s\", \"%s\", %s, %u MHz (channel %u), signal %.0f, tx %d, rx %d", + connection.interface, item->inf.status.chars, item->conn.ssid.chars, item->conn.bssid.chars, + item->conn.protocol.length ? item->conn.protocol.chars : "(no standard)", connection.frequency, + item->conn.channel, item->conn.signalQuality, connection.txLinkSpeed, connection.rxLinkSpeed); return nullptr; } + +const char* ffDetectWifi(FFlist* result) { + return detectWithBinder(result); +} From 58692adabce88ea4a84ac503e021dc458e53892d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 19 Sep 2026 07:58:59 +0800 Subject: [PATCH 72/76] Common (Networking): fixes a non-pure function marked as pure Fixes CI --- src/common/networking.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/networking.h b/src/common/networking.h index 55949027d6..7191472148 100644 --- a/src/common/networking.h +++ b/src/common/networking.h @@ -38,7 +38,7 @@ typedef struct FFNetworkingState { // Restricting the range matters because the body may already share the same buffer. // Returns a pointer to the first character of the value; `valueLen` receives its // length excluding the terminating CRLF. Returns nullptr when the header is absent. -[[gnu::nonnull(1, 3, 4), gnu::pure, nodiscard]] const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen); +[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen); // Checks whether a `Transfer-Encoding: chunked` body has been received in full, so that // framing does not have to rely on the server closing the connection. From 6142077656aae3270e99874d495ba9eeabb5b528 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sat, 19 Sep 2026 17:45:12 +0800 Subject: [PATCH 73/76] Chore: eliminates a compiler warning --- src/detection/top/top_bsd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detection/top/top_bsd.c b/src/detection/top/top_bsd.c index 108e5e97e4..587ac2fb4d 100644 --- a/src/detection/top/top_bsd.c +++ b/src/detection/top/top_bsd.c @@ -6,7 +6,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, [[maybe_unused]] FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC }; size_t length; From efbfda9cf8190a209adb9f49975f01301d42dfbf Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sat, 19 Sep 2026 17:46:13 +0800 Subject: [PATCH 74/76] Doc: updates changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e6ebf66c..1752e4265e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ Bugfixes: * This fixes PKG package count detection on FreeBSD. * Fixed `{#keys}` and `{#title}` in module format strings not honoring the `brightColor` display option. (Format) * Fixed `paddingTop` and `paddingLeft` being ignored by the `kitty-icat` image logo type. (Logo) +* Fixed issues when running on big-endian platforms. * Some internal cleanups and optimizations. Logos: From 34639b61b3732b67a71cfe086a999e9d325abb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sat, 19 Sep 2026 18:55:30 +0800 Subject: [PATCH 75/76] Presets: updates examples/25 [ci skip] Fixes #2595 --- presets/examples/25.jsonc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/presets/examples/25.jsonc b/presets/examples/25.jsonc index ddb3fdf2da..fbbf8a29be 100644 --- a/presets/examples/25.jsonc +++ b/presets/examples/25.jsonc @@ -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": "", From 4a33a2c41060d17fed963795d416bba79fd5f57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Sun, 20 Sep 2026 00:50:01 +0800 Subject: [PATCH 76/76] Player (Windows): improves player name detection --- CHANGELOG.md | 2 + CMakeLists.txt | 1 + src/detection/media/media_windows.cpp | 114 +++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1752e4265e..87c2336ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ Features: * It now supports custom ports and can properly handle chunked transfer encoding. * It is designed for minimal resource usage and fast performance. It does not support full HTTP features like HTTPS. Users can always use the `Command` module with `curl` to achieve similar functionality. * Added Umbriel wayland compositor version detection (WM, Linux) +* Improved the player name detection on Windows to show the name Windows shows for it. (Player, Windows) + * An unpackaged player such as Chrome is now reported as `Google Chrome` instead of `Chrome`. Bugfixes: * Fixed Base64 encoding producing incorrect output for some inputs. (General) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf2175145b..75437aec9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1942,6 +1942,7 @@ elseif(WIN32) PRIVATE "cfgmgr32" PRIVATE "winbrand" PRIVATE "secur32" + PRIVATE "shlwapi" PRIVATE "windowscodecs" ) if(NOT ENABLE_WIN81_COMPAT) diff --git a/src/detection/media/media_windows.cpp b/src/detection/media/media_windows.cpp index 8f608c1250..940fe9befc 100644 --- a/src/detection/media/media_windows.cpp +++ b/src/detection/media/media_windows.cpp @@ -11,6 +11,11 @@ extern "C" { #include #include + #include + #include + #include + #include + #include #include #include @@ -210,6 +215,99 @@ static HRESULT ffSaveThumbnailToTempPath( return S_OK; } +// Path 1: `Windows.ApplicationModel.AppInfo`, which answers straight from the package manifest. +// It only knows packaged (MSIX) apps, but for those it is the cheap source: resolving a +// `PackageFamilyName!AppId` AppUserModelId costs about half of what the shell needs, because the +// shell has to look the application up in the package graph. A *failed* lookup still costs a +// couple of milliseconds of WinRT class activation, so this is only worth trying on names that +// are actually packaged -- the caller gates it, see `resolveAppUserModelId`. +static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { + FF_AUTO_RELEASE_COM_OBJECT abi_t* statics = nullptr; + if (FAILED(ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", winrt::guid_of(), &statics)) || !statics) { + return false; + } + + HSTRING_HEADER header; + HSTRING aumidString; + if (FAILED(WindowsCreateStringReference(aumid, (UINT32) wcslen(aumid), &header, &aumidString))) { + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT abi_t* appInfo = nullptr; + if (FAILED(statics->GetFromAppUserModelId(reinterpret_cast(aumidString), reinterpret_cast(&appInfo))) || !appInfo) { + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT abi_t* displayInfo = nullptr; + if (FAILED(appInfo->get_DisplayInfo(reinterpret_cast(&displayInfo))) || !displayInfo) { + return false; + } + + [[gnu::cleanup(deleteHstring)]] HSTRING displayName = nullptr; + if (FAILED(displayInfo->get_DisplayName(reinterpret_cast(&displayName))) || !displayName) { + return false; + } + + ffStrbufSetHstring(result, displayName); + return result->length > 0; +} + +// Path 2: the Start menu's `AppsFolder` namespace, i.e. the (display name, AppUserModelId) table +// Windows itself displays. It covers packaged and unpackaged apps alike, and inside that namespace +// an item's parsing name *is* its AppUserModelId, so `ParseDisplayName` finds a child directly -- +// going through `SHCreateItemFromParsingName(L"shell:AppsFolder\\" + aumid)` costs several times +// more, because the `shell:` protocol has to be activated first. +static bool resolveAppsFolderAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { + const size_t aumidLength = wcslen(aumid); + wchar_t name[512]; + if (aumidLength == 0 || aumidLength >= ARRAY_SIZE(name)) { + return false; + } + wmemcpy(name, aumid, aumidLength + 1); // `ParseDisplayName` wants a mutable string + + FF_AUTO_RELEASE_COM_OBJECT IShellItem* folder = nullptr; + if (FAILED(SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder))) || !folder) { + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT IShellFolder* shellFolder = nullptr; + if (FAILED(folder->BindToHandler(nullptr, BHID_SFObject, IID_PPV_ARGS(&shellFolder))) || !shellFolder) { + return false; + } + + LPITEMIDLIST child = nullptr; + ULONG attributes = 0; + if (FAILED(shellFolder->ParseDisplayName(nullptr, nullptr, name, &attributes, &child, nullptr)) || !child) { + return false; + } + + bool success = false; + STRRET strret = {}; + if (SUCCEEDED(shellFolder->GetDisplayNameOf(child, SHGDN_INFOLDER, &strret))) { + wchar_t displayName[ARRAY_SIZE(name)]; + if (SUCCEEDED(StrRetToBufW(&strret, child, displayName, ARRAY_SIZE(displayName)))) { + ffStrbufSetWS(result, displayName); + success = result->length > 0; + } + } + + CoTaskMemFree(child); + return success; +} + +// The two sources above agree on every packaged app, and the shell alone covers everything else, +// so picking between them is purely a matter of cost. `PackageFamilyName!AppId` is the shape +// `AppInfo` can serve, and the `!` is what distinguishes it from an unpackaged AppUserModelId +// (`Chrome`, `PotPlayerMini64.exe`, a derived path). The shell stays the fallback either way, so a +// packaged-looking name that `AppInfo` rejects is still resolved. +static bool resolveAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { + if (wcschr(aumid, L'!') && resolvePackagedAppUserModelId(aumid, result)) { + return true; + } + + return resolveAppsFolderAppUserModelId(aumid, result); +} + static const char* getMedia(FFMediaResult* result, bool saveCover) { const char* error = ffInitCom(); if (error) { @@ -373,19 +471,9 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) { } } - FF_AUTO_RELEASE_COM_OBJECT abi_t* appInfoStatics = nullptr; - hr = ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", winrt::guid_of(), &appInfoStatics); - if (SUCCEEDED(hr) && appInfoStatics) { - FF_AUTO_RELEASE_COM_OBJECT abi_t* appInfo = nullptr; - if (SUCCEEDED(appInfoStatics->GetFromAppUserModelId(reinterpret_cast(playerId), reinterpret_cast(&appInfo))) && appInfo) { - FF_AUTO_RELEASE_COM_OBJECT abi_t* displayInfo = nullptr; - if (SUCCEEDED(appInfo->get_DisplayInfo(reinterpret_cast(&displayInfo))) && displayInfo) { - [[gnu::cleanup(deleteHstring)]] HSTRING displayName = nullptr; - if (SUCCEEDED(displayInfo->get_DisplayName(reinterpret_cast(&displayName)))) { - ffStrbufSetHstring(&result->player, displayName); - } - } - } + if (playerId) { + uint32_t aumidLength = 0; + resolveAppUserModelId(WindowsGetStringRawBuffer(playerId, &aumidLength), &result->player); } if (result->player.length == 0) {