Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 9fac492ac9 CI: Bump the github-actions group across 1 directory with 5 updates
Bumps the github-actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [uraimo/run-on-arch-action](https://github.com/uraimo/run-on-arch-action) | `3.2.0` | `3.2.1` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.1` | `4.38.2` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.1` | `4.38.2` |
| [vmactions/solaris-vm](https://github.com/vmactions/solaris-vm) | `1.4.0` | `1.4.1` |
| [msys2/setup-msys2](https://github.com/msys2/setup-msys2) | `2.32.0` | `2.33.0` |



Updates `uraimo/run-on-arch-action` from 3.2.0 to 3.2.1
- [Release notes](https://github.com/uraimo/run-on-arch-action/releases)
- [Commits](https://github.com/uraimo/run-on-arch-action/compare/460cb8e6d9f726a588fc9b5e681c8a6cab09ae41...fa1f3e7de95534497266c7265950b5188355fbfb)

Updates `github/codeql-action/init` from 4.38.1 to 4.38.2
- [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/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `github/codeql-action/analyze` from 4.38.1 to 4.38.2
- [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/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `vmactions/solaris-vm` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/vmactions/solaris-vm/releases)
- [Commits](https://github.com/vmactions/solaris-vm/compare/a11f407d3720a3717673d4194f19ee63b284977e...87d3c436511cef802cd1637f86f73b1a97715c8c)

Updates `msys2/setup-msys2` from 2.32.0 to 2.33.0
- [Release notes](https://github.com/msys2/setup-msys2/releases)
- [Changelog](https://github.com/msys2/setup-msys2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/msys2/setup-msys2/compare/66cd2cce69caa17b53920067426061ca1de3a884...ec48f7c5447b3140e2b088413ae3a55687bccb6e)

---
updated-dependencies:
- dependency-name: uraimo/run-on-arch-action
  dependency-version: 3.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: vmactions/solaris-vm
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: msys2/setup-msys2
  dependency-version: 2.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 04:45:05 +00:00
81 changed files with 828 additions and 5620 deletions
+10 -29
View File
@@ -59,14 +59,13 @@
.legend-item.is-hidden .legend-swatch { opacity: .35; } .legend-item.is-hidden .legend-swatch { opacity: .35; }
.chart-tooltip { position: fixed; z-index: 1000; pointer-events: none; width: min(90vw, 900px); max-height: calc(100vh - 24px); overflow: hidden; padding: .7rem .85rem; border: 1px solid var(--border); border-radius: .6rem; background: var(--card); color: var(--text); box-shadow: 0 10px 30px #0f172a33; font-size: .8rem; opacity: 0; transition: opacity .1s ease; } .chart-tooltip { position: fixed; z-index: 1000; pointer-events: none; width: min(90vw, 900px); max-height: calc(100vh - 24px); overflow: hidden; padding: .7rem .85rem; border: 1px solid var(--border); border-radius: .6rem; background: var(--card); color: var(--text); box-shadow: 0 10px 30px #0f172a33; font-size: .8rem; opacity: 0; transition: opacity .1s ease; }
.chart-tooltip-title { margin-bottom: .35rem; font-weight: 700; white-space: normal; overflow-wrap: anywhere; } .chart-tooltip-title { margin-bottom: .35rem; font-weight: 700; white-space: normal; overflow-wrap: anywhere; }
.chart-tooltip-message { margin-bottom: .35rem; color: var(--muted); font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
.chart-tooltip-body { max-height: calc(100vh - 76px); overflow: auto; columns: 220px auto; column-gap: 1.5rem; } .chart-tooltip-body { max-height: calc(100vh - 76px); overflow: auto; columns: 220px auto; column-gap: 1.5rem; }
.chart-tooltip-row { display: flex; align-items: center; gap: .45rem; break-inside: avoid; white-space: nowrap; } .chart-tooltip-row { display: flex; align-items: center; gap: .45rem; break-inside: avoid; white-space: nowrap; }
.chart-tooltip-swatch { width: .55rem; height: .55rem; border-radius: 50%; flex: 0 0 auto; } .chart-tooltip-swatch { width: .55rem; height: .55rem; border-radius: 50%; flex: 0 0 auto; }
.empty { padding: 2rem 0; color: var(--muted); } .empty { padding: 2rem 0; color: var(--muted); }
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
<script src="data.js"></script> <script src="data.js"></script>
<script src="https://unpkg.com/chart.js@latest/dist/chart.umd.min.js"></script>
</head> </head>
<body> <body>
<header> <header>
@@ -74,11 +73,11 @@
<h1>Fastfetch Benchmark</h1> <h1>Fastfetch Benchmark</h1>
<button id="theme-toggle" class="theme-toggle" type="button"></button> <button id="theme-toggle" class="theme-toggle" type="button"></button>
</div> </div>
<p class="intro">Performance history grouped by CPU family. Each chart contains all modules and the total process time for one operating system and architecture. All metric series are shown by default except total process time, module Weather, and module PublicIp — toggle any series from the legend below the chart.</p> <p class="intro">Performance history grouped by CPU family. Each chart contains all modules and total process time for one operating system and architecture. Module curves are available from the legend and hidden by default.</p>
<p class="links"><a href="data.js">Download raw benchmark data</a></p> <p class="links"><a href="data.js">Download raw benchmark data</a></p>
</header> </header>
<nav id="tabs" class="tabs" aria-label="CPU architecture"></nav> <nav id="tabs" class="tabs" aria-label="CPU architecture"></nav>
<main id="panels"><p class="empty">Loading charts…</p></main> <main id="panels"></main>
<script> <script>
(() => { (() => {
const benchmarkData = window.BENCHMARK_DATA || {}; const benchmarkData = window.BENCHMARK_DATA || {};
@@ -104,12 +103,11 @@
} }
function entryMetadata(entry) { function entryMetadata(entry) {
const raw = entry.commit?.timestamp ?? entry.date; const raw = entry.commit && entry.commit.timestamp || entry.date;
const date = new Date(raw); const date = new Date(raw);
return { return {
date: Number.isNaN(date.valueOf()) ? String(entry.date ?? '') : date.toISOString(), date: Number.isNaN(date.valueOf()) ? String(entry.date || '') : date.toISOString(),
sha: entry.commit?.id ?? 'unknown', sha: entry.commit && entry.commit.id ? entry.commit.id : 'unknown'
message: entry.commit?.message?.trim() ?? ''
}; };
} }
@@ -133,8 +131,6 @@
'#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', '#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2',
'#db2777', '#65a30d', '#4f46e5', '#ca8a04', '#0f766e', '#be123c' '#db2777', '#65a30d', '#4f46e5', '#ca8a04', '#0f766e', '#be123c'
]; ];
// Series hidden on first render; every other metric is shown by default.
const hiddenByDefault = new Set(['total process time', 'module Weather', 'module PublicIp']);
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
const panels = document.getElementById('panels'); const panels = document.getElementById('panels');
const root = document.documentElement; const root = document.documentElement;
@@ -143,13 +139,6 @@
let theme = savedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); let theme = savedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
const chartsByInstance = []; const chartsByInstance = [];
if (!window.Chart) {
tabs.hidden = true;
panels.innerHTML = '<p class="empty">Failed to load Chart.js.</p>';
return;
}
panels.replaceChildren();
function chartColors() { function chartColors() {
const styles = getComputedStyle(root); const styles = getComputedStyle(root);
return { return {
@@ -178,12 +167,6 @@
const row = chart._benchmarkRows[tooltip.dataPoints[0].dataIndex]; const row = chart._benchmarkRows[tooltip.dataPoints[0].dataIndex];
title.textContent = `${new Date(row.date).toLocaleString()} · ${row.sha}`; title.textContent = `${new Date(row.date).toLocaleString()} · ${row.sha}`;
tooltipElement.appendChild(title); tooltipElement.appendChild(title);
if (row.message) {
const message = document.createElement('div');
message.className = 'chart-tooltip-message';
message.textContent = row.message;
tooltipElement.appendChild(message);
}
const bodyElement = document.createElement('div'); const bodyElement = document.createElement('div');
bodyElement.className = 'chart-tooltip-body'; bodyElement.className = 'chart-tooltip-body';
for (const body of [...tooltip.dataPoints].sort((a, b) => b.parsed.y - a.parsed.y)) { for (const body of [...tooltip.dataPoints].sort((a, b) => b.parsed.y - a.parsed.y)) {
@@ -246,9 +229,7 @@
families.get(family).push({ key, rows }); families.get(family).push({ key, rows });
} }
// 'Other' is intentionally not rendered: it only catches stray series (e.g. the retired const familyOrder = ['Intel', 'ARM', 'Other'];
// 'macos universal' records) that should not show up as their own tab
const familyOrder = ['Intel', 'ARM'];
const availableFamilies = familyOrder.filter(family => families.has(family)); const availableFamilies = familyOrder.filter(family => families.has(family));
for (const [index, family] of availableFamilies.entries()) { for (const [index, family] of availableFamilies.entries()) {
@@ -330,7 +311,7 @@
pointRadius: 3, pointRadius: 3,
tension: 0.2, tension: 0.2,
spanGaps: true, spanGaps: true,
hidden: hiddenByDefault.has(metric) hidden: metric !== 'total process time'
})) }))
}, },
options: { options: {
@@ -392,7 +373,7 @@
chart.update(); chart.update();
}); });
addLegendAction('Reset', () => { addLegendAction('Reset', () => {
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, !hiddenByDefault.has(metric))); metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, metric === 'total process time'));
refreshLegend(); refreshLegend();
chart.update(); chart.update();
}); });
@@ -414,7 +395,7 @@
} }
} }
if (!availableFamilies.length) { if (!groups.size) {
tabs.hidden = true; tabs.hidden = true;
panels.innerHTML = '<p class="empty">No benchmark data available.</p>'; panels.innerHTML = '<p class="empty">No benchmark data available.</p>';
} }
-163
View File
@@ -1,163 +0,0 @@
name: Reusable Android
on:
workflow_call:
inputs:
abi:
required: true
type: string
permissions:
contents: read
env:
CMAKE_BUILD_TYPE: ${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }}
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: uname -a
run: uname -a
# The NDK comes with the runner image, so nothing is downloaded here. The image is pinned to
# ubuntu-24.04 rather than ubuntu-latest because which NDK a build used is part of what it is.
#
# The image's *latest* NDK, not its default one: the default is r27d (clang 18), while fastfetch
# is built and shipped on Termux with clang 21. Testing three majors behind the compiler the
# supported path uses buys nothing and rejects code clang 21 accepts.
- name: print NDK version
run: |
echo "default: ${ANDROID_NDK_HOME}"
echo "latest: ${ANDROID_NDK_LATEST_HOME}"
test -f "${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake"
cat "${ANDROID_NDK_LATEST_HOME}/source.properties"
"${ANDROID_NDK_LATEST_HOME}"/toolchains/llvm/prebuilt/*/bin/clang --version
# freetype is the one library this build wants that the NDK does not ship: its headers are not
# in the sysroot and the runner image has no libfreetype-dev, so without this step
# ff_lib_enable(FREETYPE ...) prints "Library: missing: FREETYPE" and
# src/detection/terminalfont/terminalfont_android.c compiles only its #else branch. That is the
# branch the shipped Termux build never takes, so the freetype code would rot untested here.
#
# Handing it the host's copy is sound, for two reasons. fastfetch never links freetype --
# BINARY_LINK_TYPE is dlopen, so ff_lib_enable skips its target_link_libraries and only the
# include path and FF_HAVE_FREETYPE are needed. And freetype's public headers carry no host
# assumptions: FT_SIZEOF_LONG and FT_SIZEOF_INT in freetype/config/integer-types.h are derived
# from FT_ULONG_MAX/FT_UINT_MAX, which come from the *target's* <limits.h>, so the same copy
# describes FT_FaceRec -- and the offset of the family_name that terminalfont_android.c reads --
# correctly for aarch64, armv7 and x86_64 alike.
#
# pkg-config is the only route that can find it: the NDK toolchain leaves PKG_CONFIG_PATH,
# PKG_CONFIG_LIBDIR and PKG_CONFIG_SYSROOT_DIR untouched, so the ordinary host search path is
# what gets used.
#
# The .pc is written here rather than taken from libfreetype-dev, whose own copy carries
# `Requires.private: zlib, bzip2, libpng, harfbuzz, libbrotlidec`. pkgconf -- which is what
# `pkg-config` has been on Ubuntu since 22.10 -- fails outright on a private requirement it
# cannot resolve, even for --exists, and libfreetype-dev only pulls in libpng-dev and
# zlib1g-dev. Nothing is linked, so the include directory is the whole contract. A Version
# field is mandatory (pkgconf treats a .pc without one as not found), hence the sed.
- name: install freetype headers
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libfreetype-dev
test -f /usr/include/freetype2/ft2build.h
VERSION=$(awk '/^#define FREETYPE_(MAJOR|MINOR|PATCH)/ {printf "%s%s", sep, $3; sep="."}' /usr/include/freetype2/freetype/freetype.h)
test -n "${VERSION}"
mkdir -p freetype-pc
cat > freetype-pc/freetype2.pc <<'EOF'
prefix=/usr
includedir=${prefix}/include
Name: FreeType 2
Description: A free, high-quality, and portable font engine.
Version: 0
Cflags: -I${includedir}/freetype2
EOF
sed -i "s/^Version: .*/Version: ${VERSION}/" freetype-pc/freetype2.pc
PKG_CONFIG_PATH="$PWD/freetype-pc" pkg-config --cflags freetype2
# android-24 (Android 7.0) is Termux's own floor, and it is the floor for a second, independent
# reason: libcamera2ndk.so only exists from API 24, fastfetch links it unconditionally, and the
# NDK has no such thing as a weak *library* reference -- a build aimed lower would fail to load
# on exactly the devices it was aimed at. The level is also what makes this job check anything:
# at 24 the NDK marks AImageDecoder_* / AndroidBitmap_compress (30) and AMediaCodec_getName /
# AMediaCodec_releaseName (28) unavailable, which is what CMakeLists.txt's
# -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability pair is built on.
# Building against a newer platform would quietly stop enforcing it.
#
# BUILD_TESTS stays off. The test binaries are host programs, there is no device here to run
# them on, and `ctest` therefore cannot follow -- unlike every other job in this workflow set.
# The platform branch of the build is what this job covers.
- name: configure project
env:
# Picks up the freetype2.pc written by the step above. The rest of the search path is left
# alone, so zlib and every other host .pc file is still found exactly as before.
PKG_CONFIG_PATH: ${{ github.workspace }}/freetype-pc
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=Off -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake" -DANDROID_ABI=${{ inputs.abi }} -DANDROID_PLATFORM=android-24 -DCMAKE_INSTALL_PREFIX=/data/data/com.termux/files/usr .
# A silent regression back to "Library: missing: FREETYPE" would look exactly like a passing
# build -- the module would simply stop being compiled, and the only trace would be a line in
# the configure log nobody diffs. Assert that the define actually reached the compile line.
- name: check freetype was detected
run: |
grep -q -- '-DFF_HAVE_FREETYPE=1' CMakeFiles/libfastfetch.dir/flags.make || {
echo "::error::freetype was not detected, so terminalfont_android.c compiled its #else branch only"
grep -o -- '-DFF_HAVE_[A-Z0-9_]*' CMakeFiles/libfastfetch.dir/flags.make | sort -u
exit 1
}
- name: build project
run: cmake --build . --target package --verbose -j4
- name: print ELF header
run: readelf --wide -h fastfetch
- name: print dynamic dependencies
run: readelf --wide -d fastfetch
# libjnigraphics.so, libmediandk.so and libcamera2ndk.so are what provide the entry points
# CMakeLists.txt weakens. A weak reference resolves to null instead of failing the load, and
# FF_ANDROID_API_AT_LEAST() reads the platform level rather than the symbol -- so a library
# dropped from the link line still passes the guard on a device new enough, and the call goes
# through a null pointer. DT_NEEDED is the only place that mistake is visible.
- name: check linked libraries
run: |
for lib in libjnigraphics.so libmediandk.so libcamera2ndk.so; do
readelf --wide -d fastfetch | grep -q "NEEDED.*\[${lib}\]" || { echo "::error::${lib} is not in DT_NEEDED"; exit 1; }
done
# Every reference to an entry point the NDK marks unavailable at API 24 has to be weak: a strong
# one turns a device below API 28/30 into a load failure instead of a graceful downgrade, which
# is the entire reason those three files are compiled with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__.
# `nm -D` prints a weak undefined symbol as `w` and a strong undefined one as `U`.
#
# The families are whole prefixes on purpose: every AImageDecoder* entry point is API 30 or 31,
# but the frame-info and header-info ones spell the prefix AImageDecoderFrameInfo / HeaderInfo
# without an underscore, and matching only `AImageDecoder_` would leave them unchecked. The
# AMediaCodec ones are named one by one because the rest of that API -- createDecoderByType,
# createEncoderByType, delete -- is API 21 and is correctly a strong reference.
- name: check weak references
run: |
nm -D fastfetch | grep -E ' (AImageDecoder|AndroidBitmap_compress|AMediaCodec_getName$|AMediaCodec_releaseName$)' > weak.txt
cat weak.txt
test -s weak.txt || { echo "::error::none of these entry points is referenced any more -- if the last use is gone on purpose, this check and the -Werror=unguarded-availability pair it guards should go with it"; exit 1; }
if grep -qv '^ *w ' weak.txt; then
echo "::error::expected every one of these to be a weak reference:"
grep -v '^ *w ' weak.txt
exit 1
fi
- name: print size
run: |
file fastfetch
ls -l fastfetch
- name: upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fastfetch-android-${{ inputs.abi }}
path: ./fastfetch-*.*
+31 -10
View File
@@ -1,7 +1,14 @@
name: Reusable macOS Universal name: Reusable macOS Hosts
on: on:
workflow_call: workflow_call:
inputs:
arch:
required: true
type: string
runs-on:
required: true
type: string
permissions: permissions:
contents: read contents: read
@@ -11,7 +18,7 @@ env:
jobs: jobs:
build: build:
runs-on: macos-latest runs-on: ${{ inputs.runs-on }}
steps: steps:
- name: checkout repository - name: checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -19,20 +26,34 @@ jobs:
- name: uname -a - name: uname -a
run: uname -a run: uname -a
- 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 - name: install required packages with Homebrew
if: inputs.arch == 'aarch64'
run: | run: |
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew update HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew update
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk chafa lua jq HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk imagemagick chafa lua quickjs-ng jq
- name: configure project - name: configure project
run: cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -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 - name: build project
run: cmake --build . --target package --verbose -j4 run: cmake --build . --target package --verbose -j4
- name: verify universal binary
run: lipo -archs fastfetch
- name: list features - name: list features
run: ./fastfetch --list-features run: ./fastfetch --list-features
@@ -51,7 +72,7 @@ jobs:
run: | run: |
real_seconds=$(awk '$1 == "real" { print $2 }' fastfetch.time) real_seconds=$(awk '$1 == "real" { print $2 }' fastfetch.time)
jq -e 'type == "array"' fastfetch.json > /dev/null jq -e 'type == "array"' fastfetch.json > /dev/null
jq --arg platform "macos" --arg arch "aarch64" --argjson real_seconds "$real_seconds" ' jq --arg platform "macos" --arg arch "${{ inputs.arch }}" --argjson real_seconds "$real_seconds" '
map(select(.stat | type == "number") | { map(select(.stat | type == "number") | {
name: ("fastfetch " + $platform + " " + $arch + " module " + .type), name: ("fastfetch " + $platform + " " + $arch + " module " + .type),
unit: "ms", unit: "ms",
@@ -76,11 +97,11 @@ jobs:
- name: upload artifacts - name: upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: fastfetch-macos-universal name: fastfetch-macos-${{ inputs.arch }}
path: ./fastfetch-*.* path: ./fastfetch-*.*
- name: upload benchmark data - name: upload benchmark data
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: benchmark-macos-universal name: benchmark-macos-${{ inputs.arch }}
path: benchmark.json path: benchmark.json
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
with: with:
msystem: ${{ inputs.msystem }} msystem: ${{ inputs.msystem }}
update: true update: true
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-imagemagick mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers
- name: print msys version - name: print msys version
run: uname -a run: uname -a
+10 -16
View File
@@ -72,27 +72,22 @@ jobs:
contents: read contents: read
uses: ./.github/workflows/build-musl-amd64.yml uses: ./.github/workflows/build-musl-amd64.yml
android: macos-hosts:
needs: no-features-test needs: no-features-test
name: Android-${{ matrix.abi }} name: macOS-${{ matrix.arch }}
permissions: permissions:
contents: read contents: read
strategy: strategy:
matrix: matrix:
abi: include:
- arm64-v8a - arch: amd64
- armeabi-v7a runs-on: macos-15-intel
- x86_64 - arch: aarch64
uses: ./.github/workflows/build-android.yml runs-on: macos-latest
with:
abi: ${{ matrix.abi }}
macos-hosts:
needs: no-features-test
name: macOS-universal
permissions:
contents: read
uses: ./.github/workflows/build-macos-hosts.yml uses: ./.github/workflows/build-macos-hosts.yml
with:
arch: ${{ matrix.arch }}
runs-on: ${{ matrix.runs-on }}
omnios-amd64: omnios-amd64:
needs: no-features-test needs: no-features-test
@@ -195,7 +190,6 @@ jobs:
- linux-loong64 - linux-loong64
- linux-vms - linux-vms
- musl-amd64 - musl-amd64
- android
- macos-hosts - macos-hosts
- freebsd-amd64 - freebsd-amd64
- openbsd-amd64 - openbsd-amd64
+1 -29
View File
@@ -1,36 +1,15 @@
# Unreleased
Bugfixes:
* Improved uptime detection on SunOS via kstat. (#2674, Uptime, SunOS)
* Fixed substring formatting returning the remaining string when a negative end index is before the start of the string. (General)
# 2.70.0 # 2.70.0
Changes: Changes:
* LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits. * LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits.
* Interfaces that carry no unicast address are no longer reported, since both the addresses and the interface properties are now enumerated from the unicast address table. (LocalIP, Windows)
* libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package. * libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package.
* QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General) * QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General)
* The Command module option `useStdErr` has been replaced with `useOutput`, which selects which of the command's output streams to capture: `"stdout"`, `"stderr"` or `"both"`.
* It defaults to `"both"`, so a command that logs to stderr now has that output captured instead of discarded.
* Set it to `"stdout"` or `"stderr"` to restore the old single-stream behavior. (Command)
* The Shell module now reports the shell from the password database (`/etc/passwd`) instead of the `$SHELL` environment variable, which may differ when the login shell was changed after the session started. (Title)
* The Disk custom format no longer provides `{size-available}`. `{size-free}` now reports whichever of the two the `disk.useAvailable` option selects, and the raw byte counts are available as `{bytes-used}` and `{bytes-total}`. (Disk)
* This also means `{size-free}` now changes with `disk.useAvailable`; it used to always report the file system's free size.
* The Disk JSON output no longer includes `bytes.available` and `bytes.used`. (Disk)
* `bytes.free` and `bytes.total` are unchanged; the used size can be derived as `total - free`.
Features: Features:
* Added Sound detection on Android. (Sound, Android) * Added Sound detection on Android. (Sound, Android)
* Added display detection on Android over binder, which needs neither a child process nor a permission. (Display, Android)
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android) * Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
* Added media detection on Android when fastfetch runs as an app UID. (Media, Android)
* Requires `Notification access` permission
* Added app display-name detection on Android, so the media module names the player the way the app names itself instead of by its package name, and the terminal module reports the label Termux is installed under. (Media, Terminal, Android)
* Performance improvements & internal cleanups * Performance improvements & internal cleanups
* Added Finnish language support. (General) * Added Finnish Language Support
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
* Added an ALSA fallback to Sound on Linux, so a machine that runs no sound server reports its output device and volume instead of an error. (Sound, Linux)
Bugfixes: Bugfixes:
* Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash) * Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash)
@@ -39,13 +18,6 @@ Bugfixes:
* Fixed the foot terminal font size not being parsed when written as `Font Name-size`, e.g. `Berkeley Mono-12`. (#2624, TerminalFont, Linux) * Fixed the foot terminal font size not being parsed when written as `Font Name-size`, e.g. `Berkeley Mono-12`. (#2624, TerminalFont, Linux)
* Fixed `/boot` being hidden by default on Haiku, where it is the system volume. (Disk, Haiku) * Fixed `/boot` being hidden by default on Haiku, where it is the system volume. (Disk, Haiku)
* Improved Snapdragon X1/X2 model name detection on Linux. (#2625, CPU, Linux) * Improved Snapdragon X1/X2 model name detection on Linux. (#2625, CPU, Linux)
* Fixed the Android build failing with `GL/gl.h` not found, because the NDK ships the EGL headers that turned OpenGL detection on but not the desktop GL headers it includes. (OpenGL, Android)
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
* Fixed assertion failure when percentage exceeds 100. (Percent)
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
* Fixed Android display detection on devices running Android 17. (Display, Android)
* Fixed Windows volumes with a per-user quota (such as network shares) reporting a wrong total size and a wrapped-around used size, because the total size was quota-limited while the free size was not. (#2651, Disk, Windows)
* On Windows 10 1809 (build 17763) and later, the whole volume's capacity and free space are now reported by default. Set `disk.useAvailable` to `true` to keep reporting the space that is available to the current user, quota included.
Logos: Logos:
* Added filled Artix logo (#2631) * Added filled Artix logo (#2631)
+9 -113
View File
@@ -60,7 +60,6 @@ endif()
include(CheckIncludeFile) include(CheckIncludeFile)
include(CheckFunctionExists) include(CheckFunctionExists)
include(CheckSymbolExists)
if(LINUX) if(LINUX)
CHECK_INCLUDE_FILE("linux/version.h" HAVE_LINUX_VERSION_H) CHECK_INCLUDE_FILE("linux/version.h" HAVE_LINUX_VERSION_H)
@@ -454,29 +453,7 @@ fastfetch_encode_c_string("${DATATEXT_JSON_HELP}" DATATEXT_JSON_HELP)
configure_file(src/fastfetch_config.h.in fastfetch_config.h @ONLY) configure_file(src/fastfetch_config.h.in fastfetch_config.h @ONLY)
configure_file(src/fastfetch_datatext.h.in fastfetch_datatext.h @ONLY) configure_file(src/fastfetch_datatext.h.in fastfetch_datatext.h @ONLY)
if(APPLE) if(APPLE)
# `mediaremoted` serves now-playing data only to clients whose bundle id starts
# with "com.apple.": -[MRDMediaRemoteClient isAllowedAccessToDataFromPlayerPath:]
# accepts the 0x200 entitlement bit either for the private
# com.apple.mediaremote.now-playing-read-access entitlement or for that prefix,
# and it derives the client's identity from the code signature / XPC audit token
# rather than from the process's own NSBundle. Both the embedded
# CFBundleIdentifier and the ad-hoc signing identifier below therefore have to
# carry the prefix; keep them in sync through these variables.
set(FASTFETCH_BUNDLE_ID "com.apple.fastfetch")
set(FLASHFETCH_BUNDLE_ID "com.apple.flashfetch")
# configure_file() inputs are not configure dependencies on their own, so
# without this an edit to Info.plist.in never even regenerates Info.plist.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/common/apple/Info.plist.in")
# Each executable gets its own plist: the file is embedded with -sectcreate and read
# back by the system as the bundle id, so two targets sharing one would leave one of
# them advertising an identifier that its code signature does not match.
set(BUNDLE_ID "${FASTFETCH_BUNDLE_ID}")
configure_file(src/common/apple/Info.plist.in Info.plist @ONLY) configure_file(src/common/apple/Info.plist.in Info.plist @ONLY)
if(BUILD_FLASHFETCH)
set(BUNDLE_ID "${FLASHFETCH_BUNDLE_ID}")
configure_file(src/common/apple/Info.plist.in Info.plist.flashfetch @ONLY)
endif()
unset(BUNDLE_ID)
endif() endif()
#################### ####################
@@ -529,7 +506,6 @@ set(LIBFASTFETCH_SRC
src/common/impl/FFPlatform.c src/common/impl/FFPlatform.c
src/common/impl/smbios.c src/common/impl/smbios.c
src/common/impl/strutil.c src/common/impl/strutil.c
src/common/impl/zlib.c
src/detection/bluetoothradio/bluetoothradio.c src/detection/bluetoothradio/bluetoothradio.c
src/detection/bootmgr/bootmgr.c src/detection/bootmgr/bootmgr.c
src/detection/chassis/chassis.c src/detection/chassis/chassis.c
@@ -623,7 +599,6 @@ if(LINUX)
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
src/detection/displayserver/linux/wayland/wp-color-management-v1-protocol.c src/detection/displayserver/linux/wayland/wp-color-management-v1-protocol.c
@@ -679,11 +654,9 @@ if(LINUX)
) )
elseif(ANDROID) elseif(ANDROID)
list(APPEND LIBFASTFETCH_SRC list(APPEND LIBFASTFETCH_SRC
src/common/android/applabel.c
src/common/android/binder.c src/common/android/binder.c
src/common/android/dex.c src/common/android/dex.c
src/common/android/package.c src/common/android/package.c
src/common/android/zip.c
src/common/impl/dbus.c src/common/impl/dbus.c
src/common/impl/io_unix.c src/common/impl/io_unix.c
src/common/impl/netif_linux.c src/common/impl/netif_linux.c
@@ -713,7 +686,6 @@ elseif(ANDROID)
src/detection/diskio/diskio_linux.c src/detection/diskio/diskio_linux.c
src/detection/displayserver/displayserver_android.c src/detection/displayserver/displayserver_android.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/kde-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
@@ -802,7 +774,6 @@ elseif(FreeBSD)
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/kde-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
@@ -901,7 +872,6 @@ elseif(NetBSD)
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/kde-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
@@ -987,7 +957,6 @@ elseif(OpenBSD)
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/kde-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
@@ -1081,7 +1050,6 @@ elseif(APPLE)
src/detection/libc/libc_apple.c src/detection/libc/libc_apple.c
src/detection/locale/locale_linux.c src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c src/detection/localip/localip_linux.c
src/detection/localip/localip_apple.c
src/detection/gamepad/gamepad_apple.m src/detection/gamepad/gamepad_apple.m
src/detection/media/media_apple.m src/detection/media/media_apple.m
src/detection/memory/memory_apple.c src/detection/memory/memory_apple.c
@@ -1248,7 +1216,6 @@ elseif(SunOS)
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/global-output.c
src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/kde-output.c
src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wl-output-protocol.c
@@ -1408,7 +1375,6 @@ elseif(GNU)
src/detection/diskio/diskio_nosupport.c src/detection/diskio/diskio_nosupport.c
src/detection/displayserver/linux/displayserver_linux.c src/detection/displayserver/linux/displayserver_linux.c
src/detection/displayserver/linux/wayland/wayland.c src/detection/displayserver/linux/wayland/wayland.c
src/detection/displayserver/linux/wayland/mutter.c
src/detection/displayserver/linux/common.c src/detection/displayserver/linux/common.c
src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/drm.c
src/detection/displayserver/linux/wmde.c src/detection/displayserver/linux/wmde.c
@@ -1582,15 +1548,11 @@ if(APPLE AND BINARY_LINK_TYPE STREQUAL "dlopen")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/opt/homebrew/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,/opt/local/lib") 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() endif()
# Termux installs the libraries fastfetch dlopens -- freetype among them -- under TARGET_DIR_ROOT, which
# is not on the Android linker's default search path (DT_RUNPATH, LD_LIBRARY_PATH and the default
# namespace). Termux's own clang injects -rpath=$PREFIX/lib into every binary it links, so a build made
# inside Termux finds them; a plain NDK build, which is what the CI does, does not.
if(ANDROID) if(ANDROID)
if(CMAKE_SIZEOF_VOID_P EQUAL 8) if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64")
else() else()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib")
endif() endif()
endif() endif()
@@ -1698,13 +1660,6 @@ if(FreeBSD OR OpenBSD OR NetBSD)
unset(CMAKE_REQUIRED_DEFINITIONS) unset(CMAKE_REQUIRED_DEFINITIONS)
endif() endif()
if(OpenBSD)
check_symbol_exists("getexecpath" "unistd.h" HAVE_GETEXECPATH)
if(HAVE_GETEXECPATH)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GETEXECPATH=1)
endif()
endif()
if(HAVE_PIPE2) if(HAVE_PIPE2)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_PIPE2) target_compile_definitions(libfastfetch PUBLIC FF_HAVE_PIPE2)
endif() endif()
@@ -1780,18 +1735,9 @@ function(ff_lib_enable VARNAME PKGCONFIG_NAMES CMAKE_NAME) # [CMAKE_TARGET_NAME]
endif() endif()
endif() endif()
else() else()
# find_package modules disagree on how they name their result variables. Newer ones set(${VARNAME}_INCLUDE_DIRS ${${CMAKE_NAME}_INCLUDE_DIRS})
# export <PackageName>_* (FindVulkan sets Vulkan_INCLUDE_DIRS), but older ones only set(${VARNAME}_LIBRARIES ${${CMAKE_NAME}_LIBRARIES})
# set the upper-case form: FindFreetype sets FREETYPE_INCLUDE_DIRS and set(${VARNAME}_CFLAGS_OTHER ${${CMAKE_NAME}_CFLAGS_OTHER})
# FREETYPE_LIBRARIES and has no Freetype_INCLUDE_DIRS at all. Assigning
# unconditionally would therefore wipe the include path the module had just
# provided, and the package would be "found" with nothing to compile against.
# Fill in only the gaps.
foreach(_FF_SUFFIX INCLUDE_DIRS LIBRARIES CFLAGS_OTHER)
if(NOT DEFINED ${VARNAME}_${_FF_SUFFIX} OR "${${VARNAME}_${_FF_SUFFIX}}" STREQUAL "")
set(${VARNAME}_${_FF_SUFFIX} ${${CMAKE_NAME}_${_FF_SUFFIX}})
endif()
endforeach()
endif() endif()
endif() endif()
endif() endif()
@@ -1910,11 +1856,9 @@ ff_lib_enable(OPENCL
"OpenCL" "OpenCL"
"OpenCL" "OpenCL"
) )
# `Freetype` is the module CMake ships (Modules/FindFreetype.cmake); there is no FindFreeType2, so
# this name never resolved and only pkg-config could ever turn freetype on.
ff_lib_enable(FREETYPE ff_lib_enable(FREETYPE
"freetype2" "freetype2"
"Freetype" "FreeType2"
) )
ff_lib_enable(ELF ff_lib_enable(ELF
"libelf" "libelf"
@@ -2213,17 +2157,7 @@ if(NOT WIN32)
endif() endif()
endif() endif()
if(NOT ENABLE_WORDEXP) if(NOT ENABLE_WORDEXP)
# wordexp.h is not the only thing that can be missing. bionic declares glob() and globfree() message(STATUS "wordexp.h not found or disabled, glob used instead")
# only from API 28, so an NDK build aimed at 24 has neither and a fallback that assumed glob
# would have nothing to call. The symbol is what has to be checked, not the header: <glob.h>
# is present at every level, it just declares nothing below 28.
check_symbol_exists("glob" "glob.h" HAVE_GLOB)
if(HAVE_GLOB)
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GLOB=1)
message(STATUS "wordexp.h not found or disabled, glob used instead")
else()
message(STATUS "neither wordexp.h nor glob.h is usable, config paths will not be expanded")
endif()
endif() endif()
if(ENABLE_THREADS AND CMAKE_USE_PTHREADS_INIT) if(ENABLE_THREADS AND CMAKE_USE_PTHREADS_INIT)
CHECK_INCLUDE_FILE("pthread_np.h" HAVE_PTHREAD_NP) CHECK_INCLUDE_FILE("pthread_np.h" HAVE_PTHREAD_NP)
@@ -2282,15 +2216,6 @@ set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "")
set_target_properties(fastfetch PROPERTIES LINKER_LANGUAGE C) set_target_properties(fastfetch PROPERTIES LINKER_LANGUAGE C)
if(WIN32) if(WIN32)
# version.rc embeds manifest.xml (RT_MANIFEST) and logo.ico (ICON) as *resource
# references*, not as #includes, so the resource compiler never reports them and
# CMake would keep a stale .res when only the manifest or the icon changes. Both
# are plain files rather than generated ones, so declare them by hand; the source
# file property is per-path, which is why flashfetch below needs no repeat.
set_property(SOURCE src/common/windows/version.rc APPEND PROPERTY OBJECT_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/manifest.xml"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/logo.ico"
)
target_sources(fastfetch target_sources(fastfetch
PRIVATE src/common/windows/version.rc PRIVATE src/common/windows/version.rc
) )
@@ -2298,15 +2223,6 @@ elseif(APPLE)
target_link_options(fastfetch target_link_options(fastfetch
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
) )
# `LINKER:` options are handed to ld verbatim and are not registered as build
# dependencies, so without this ninja never relinks when Info.plist changes.
set_property(TARGET fastfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist")
# ld derives the ad-hoc code-signing identifier from the output file name and
# ignores the embedded CFBundleIdentifier, so set it explicitly.
add_custom_command(TARGET fastfetch POST_BUILD
COMMAND codesign --force --sign - --identifier "${FASTFETCH_BUNDLE_ID}" "$<TARGET_FILE:fastfetch>"
VERBATIM
)
endif() endif()
if(BINARY_LINK_TYPE STREQUAL "static") if(BINARY_LINK_TYPE STREQUAL "static")
@@ -2332,15 +2248,8 @@ if (BUILD_FLASHFETCH)
PRIVATE src/common/windows/version.rc PRIVATE src/common/windows/version.rc
) )
elseif(APPLE) elseif(APPLE)
# flashfetch embeds its own plist so that its CFBundleIdentifier matches the
# identifier it is signed with below (see the APPLE block near the top).
target_link_options(flashfetch target_link_options(flashfetch
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist.flashfetch PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
)
set_property(TARGET flashfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist.flashfetch")
add_custom_command(TARGET flashfetch POST_BUILD
COMMAND codesign --force --sign - --identifier "${FLASHFETCH_BUNDLE_ID}" "$<TARGET_FILE:flashfetch>"
VERBATIM
) )
endif() endif()
@@ -2480,13 +2389,6 @@ if (BUILD_TESTS)
PRIVATE libfastfetch PRIVATE libfastfetch
) )
add_executable(fastfetch-test-gpu-pci
tests/gpu_pci.c
)
target_link_libraries(fastfetch-test-gpu-pci
PRIVATE libfastfetch
)
enable_testing() enable_testing()
add_test(NAME test-cache COMMAND fastfetch-test-cache) add_test(NAME test-cache COMMAND fastfetch-test-cache)
add_test(NAME test-logo COMMAND fastfetch-test-logo) add_test(NAME test-logo COMMAND fastfetch-test-logo)
@@ -2506,7 +2408,6 @@ if (BUILD_TESTS)
add_test(NAME test-temps COMMAND fastfetch-test-temps) add_test(NAME test-temps COMMAND fastfetch-test-temps)
add_test(NAME test-endian COMMAND fastfetch-test-endian) add_test(NAME test-endian COMMAND fastfetch-test-endian)
add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian) add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian)
add_test(NAME test-gpu-pci COMMAND fastfetch-test-gpu-pci)
endif() endif()
################## ##################
@@ -2568,12 +2469,7 @@ install(
set(CPACK_GENERATOR "TGZ;ZIP") set(CPACK_GENERATOR "TGZ;ZIP")
if(APPLE) if(APPLE)
if(CMAKE_OSX_ARCHITECTURES MATCHES ";") # multiple architectures, e.g. arm64;x86_64 string(TOLOWER "${CMAKE_PROJECT_NAME}-macos-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME) # use macos instead of darwin
set(_FF_CPACK_ARCH universal)
else()
set(_FF_CPACK_ARCH ${CMAKE_SYSTEM_PROCESSOR})
endif()
string(TOLOWER "${CMAKE_PROJECT_NAME}-macos-${_FF_CPACK_ARCH}" CPACK_PACKAGE_FILE_NAME) # use macos instead of darwin
elseif(IS_MUSL) elseif(IS_MUSL)
string(TOLOWER "${CMAKE_PROJECT_NAME}-musl-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME) string(TOLOWER "${CMAKE_PROJECT_NAME}-musl-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME)
else() else()
+1 -1
View File
@@ -680,7 +680,7 @@ cmake --build build
cd build && ctest --output-on-failure cd build && ctest --output-on-failure
``` ```
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), Android, macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus a benchmark job. The Android job is the one that does not run what it builds: it cross-compiles for arm64-v8a, armeabi-v7a and x86_64 with the NDK at API 24 and inspects the result instead, because CI has no device or emulator. Its dynamic-symbol checks are what keep the weak references the platform branch depends on from turning into load failures on older devices. 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 modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.** **If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
+4 -4
View File
@@ -9,8 +9,8 @@
[![homebrew downloads](https://img.shields.io/homebrew/installs/dm/fastfetch?logo=homebrew)](https://formulae.brew.sh/formula/fastfetch#default) [![homebrew downloads](https://img.shields.io/homebrew/installs/dm/fastfetch?logo=homebrew)](https://formulae.brew.sh/formula/fastfetch#default)
[![GitHub all releases](https://img.shields.io/github/downloads/fastfetch-cli/fastfetch/total?logo=github)](https://github.com/fastfetch-cli/fastfetch/releases) [![GitHub all releases](https://img.shields.io/github/downloads/fastfetch-cli/fastfetch/total?logo=github)](https://github.com/fastfetch-cli/fastfetch/releases)
[![GitHub release (with filter)](https://img.shields.io/github/v/release/fastfetch-cli/fastfetch?logo=github)](https://github.com/fastfetch-cli/fastfetch/releases) [![GitHub release (with filter)](https://img.shields.io/github/v/release/fastfetch-cli/fastfetch?logo=github)](https://github.com/fastfetch-cli/fastfetch/releases)
[![latest packaged version(s)](https://repology.org/badge/latest-versions/fastfetch.svg)](https://repology.org/project/fastfetch/versions) [![latest packaged version(s)](https://repology.amdmi3.ru/badge/latest-versions/fastfetch.svg)](https://repology.amdmi3.ru/project/fastfetch/versions)
[![Packaging status](https://repology.org/badge/tiny-repos/fastfetch.svg)](https://repology.org/project/fastfetch/versions) [![Packaging status](https://repology.amdmi3.ru/badge/tiny-repos/fastfetch.svg)](https://repology.amdmi3.ru/project/fastfetch/versions)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fastfetch-cli/fastfetch) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fastfetch-cli/fastfetch)
[![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-red)](README-cn.md) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-red)](README-cn.md)
@@ -36,8 +36,8 @@ There are [screenshots on different platforms](https://github.com/fastfetch-cli/
Some distributions package outdated versions of fastfetch. Older versions receive no support, so please always try to use the latest version. Some distributions package outdated versions of fastfetch. Older versions receive no support, so please always try to use the latest version.
<a href="https://repology.org/project/fastfetch/versions"> <a href="https://repology.amdmi3.ru/project/fastfetch/versions">
<img src="https://repology.org/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right"> <img src="https://repology.amdmi3.ru/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right">
</a> </a>
* Ubuntu: [`ppa:zhangsongcui3371/fastfetch`](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) (Ubuntu 22.04 or newer; latest version) * Ubuntu: [`ppa:zhangsongcui3371/fastfetch`](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) (Ubuntu 22.04 or newer; latest version)
+9 -23
View File
@@ -277,7 +277,7 @@
"batteryFormat": { "batteryFormat": {
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)", "description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
"type": "string" "type": "string"
}, },
"biosFormat": { "biosFormat": {
@@ -325,7 +325,7 @@
"type": "string" "type": "string"
}, },
"cpuFormat": { "cpuFormat": {
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count", "description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name (x86 only)\n 14. {technology}: CPU technology (x86 only)",
"type": "string" "type": "string"
}, },
"cpucacheFormat": { "cpucacheFormat": {
@@ -353,7 +353,7 @@
"type": "string" "type": "string"
}, },
"diskFormat": { "diskFormat": {
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used (formatted)\n 2. {size-total}: Size total (formatted)\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free (formatted)\n 26. {bytes-used}: Size used (in bytes)\n 27. {bytes-total}: Size total (in bytes)", "description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used\n 2. {size-total}: Size total\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free\n 26. {size-available}: Size available",
"type": "string" "type": "string"
}, },
"diskioFormat": { "diskioFormat": {
@@ -465,7 +465,7 @@
"type": "string" "type": "string"
}, },
"poweradapterFormat": { "poweradapterFormat": {
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model name\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number", "description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"type": "string" "type": "string"
}, },
"processesFormat": { "processesFormat": {
@@ -2381,24 +2381,10 @@
"description": "Command text to execute", "description": "Command text to execute",
"type": "string" "type": "string"
}, },
"useOutput": { "useStdErr": {
"description": "Which of the command's output streams to capture\nThe stream that is not captured is discarded", "description": "Whether to use stderr instead of stdout for command output",
"type": "string", "type": "boolean",
"oneOf": [ "default": false
{
"const": "stdout",
"description": "Capture stdout only and discard stderr"
},
{
"const": "stderr",
"description": "Capture stderr only and discard stdout"
},
{
"const": "both",
"description": "Capture both stdout and stderr"
}
],
"default": "both"
}, },
"parallel": { "parallel": {
"description": "Whether to execute the command in parallel with other commands\nThis can improve performance when using multiple commands, but may cause issues with some commands", "description": "Whether to execute the command in parallel with other commands\nThis can improve performance when using multiple commands, but may cause issues with some commands",
@@ -2716,7 +2702,7 @@
}, },
"useAvailable": { "useAvailable": {
"type": "boolean", "type": "boolean",
"description": "Use f_bavail instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results\nOn Windows, set to true to make fastfetch honors the quota settings", "description": "Use f_bavail (lpFreeBytesAvailableToCaller for Windows) instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results",
"default": false "default": false
}, },
"percent": { "percent": {
-6
View File
@@ -466,12 +466,6 @@ static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length; return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
} }
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainAnyC(const FFstrbuf* strbuf, const char* cs) {
assert(cs != nullptr);
return strpbrk(strbuf->chars, cs) != nullptr;
}
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) { [[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); assert(start <= strbuf->length);
-560
View File
@@ -1,560 +0,0 @@
#include "common/android/applabel.h"
#include "common/android/binder.h"
#include "common/android/dex.h"
#include "common/android/zip.h"
#include "common/debug.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
// The package manager, and the one method that describes an app to a caller that is not the shell.
// IPackageManager has always been part of framework.jar -- it is not one of the frameworks that
// moved into an APEX -- so there is one path and no fallback.
#define FF_ANDROID_LABEL_SERVICE "package"
#define FF_ANDROID_LABEL_DESCRIPTOR "android.content.pm.IPackageManager"
#define FF_ANDROID_LABEL_JAR "/system/framework/framework.jar"
#define FF_ANDROID_LABEL_STUB "Landroid/content/pm/IPackageManager$Stub;"
#define FF_ANDROID_LABEL_TRANSACTION "TRANSACTION_getApplicationInfo"
#define FF_ANDROID_LABEL_TABLE "resources.arsc"
// The longest package name AIDL accepts is 255 characters, and the request is one string16 of it, the
// interface token, a 64-bit flags argument and a 32-bit user id.
// clang-format off
#define FF_ANDROID_LABEL_REQUEST_SIZE \
(12 + ((4 + 2 * (sizeof(FF_ANDROID_LABEL_DESCRIPTOR) - 1) + 3) & ~3u) \
+ ((4 + 2 * 255 + 3) & ~3u) + 8u + 4u)
// clang-format on
// An ApplicationInfo parcel runs 2.0 to 3.2 KB on the test device, and the surplus is what a reply
// with more split names and directories than any of those would spend.
#define FF_ANDROID_LABEL_REPLY_SIZE (16 * 1024)
// Long enough for the `/data/app/~~<22 base64 chars>==/<package>-<22 chars>==/base.apk` the platform
// installs to, with room to spare.
#define FF_ANDROID_LABEL_PATH_SIZE 512
// ---------------------------------------------------------------------------------------------
// The resource table
// ---------------------------------------------------------------------------------------------
//
// `resources.arsc` is a ResTable: a header, a global string pool, and then one ResTable_package per
// package it defines, each carrying its own pools for type names and entry keys followed by the
// types themselves. Resolving `labelRes` is two indices deep -- the package's number names the
// package and the type, the low half names the entry -- and every step below reads the file rather
// than a table of what the file is expected to hold, because the layout is the platform's and the
// numbers are per APK.
// ResChunk_header: type(2) headerSize(2) size(4). Every chunk in the file starts with one.
#define FF_ARSC_TABLE_TYPE 0x0002
#define FF_ARSC_STRING_POOL_TYPE 0x0001
#define FF_ARSC_TABLE_PACKAGE_TYPE 0x0200
#define FF_ARSC_TABLE_TYPE_TYPE 0x0201
// ResStringPool: header(8) stringCount(4) styleCount(4) flags(4) stringsStart(4) stylesStart(4)
#define FF_ARSC_POOL_HEADER_SIZE 28
#define FF_ARSC_OFF_POOL_CHUNK_SIZE 4
#define FF_ARSC_OFF_POOL_COUNT 8
#define FF_ARSC_OFF_POOL_FLAGS 16
#define FF_ARSC_OFF_POOL_STRINGS_START 20
// A UTF-8 pool says so with this bit, and then holds the strings as UTF-8 rather than as UTF-16.
#define FF_ARSC_POOL_FLAG_UTF8 0x0100
// ResTable_package: header(8) id(4) name(256, a UTF-16 array) typeStrings(4) lastPublicType(4) ...
#define FF_ARSC_OFF_PACKAGE_ID 8
#define FF_ARSC_OFF_PACKAGE_TYPE_STRINGS (8 + 4 + 256)
// ResTable_type: header(8) id(1) flags(1) reserved(2) entryCount(4) entriesStart(4) config(...)
#define FF_ARSC_OFF_TYPE_ID 8
#define FF_ARSC_OFF_TYPE_FLAGS 9
#define FF_ARSC_OFF_TYPE_ENTRY_COUNT 12
#define FF_ARSC_OFF_TYPE_ENTRIES_START 16
#define FF_ARSC_OFF_TYPE_CONFIG 20
// The two shapes a type can take that this does not read: a sparse type stores its offsets as
// index-offset pairs rather than as a flat table, and a 16-bit one stores them at half width. Neither
// is in the APKs measured, and both would resolve an entry to the wrong offset rather than to none,
// so they are refused rather than read as if they were the plain shape.
#define FF_ARSC_TYPE_FLAG_SPARSE 0x01
#define FF_ARSC_TYPE_FLAG_OFFSET16 0x02
// ResTable_entry: size(2) flags(2), where size is the header's own length and the value follows it.
#define FF_ARSC_ENTRY_FLAG_COMPLEX 0x0001
// Res_value: size(2) res0(1) dataType(1) data(4). TYPE_STRING makes data an index into the global
// string pool; every other type is either a number or a reference to another resource.
#define FF_ARSC_OFF_VALUE_DATA_TYPE 3
#define FF_ARSC_OFF_VALUE_DATA 4
#define FF_ARSC_TYPE_STRING 0x03
// A type chunk's offset table has one word per entry, and this is what an entry with no value in that
// configuration looks like. It is not zero: offset zero is a real entry, at the start of the block.
#define FF_ARSC_NO_ENTRY 0xffffffffu
// The type a label resolves to. `string` is what `@string/...` compiles to, and it is what tells a
// label from the icons and banners that sit beside it in the same reply -- those point at `mipmap`
// or `drawable` entries whose value is a path inside the APK, so resolving them would hand back
// `res/9w.png` instead of a name.
#define FF_ANDROID_LABEL_STRING_TYPE "string"
static uint16_t readU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t readU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
// One ResStringPool, of which a table has three.
typedef struct FFAndroidLabelPool {
const uint8_t* table;
size_t tableSize;
size_t offset;
uint32_t chunkSize;
uint32_t count;
uint32_t headerSize;
uint32_t stringsStart;
bool utf8;
} FFAndroidLabelPool;
[[gnu::nonnull(2), nodiscard]] static bool poolInit(FFAndroidLabelPool* pool, const uint8_t* table, size_t tableSize, size_t offset) {
if (offset > tableSize || tableSize - offset < FF_ARSC_POOL_HEADER_SIZE) {
return false;
}
if (readU16(table + offset) != FF_ARSC_STRING_POOL_TYPE) {
return false;
}
pool->table = table;
pool->tableSize = tableSize;
pool->offset = offset;
pool->chunkSize = readU32(table + offset + FF_ARSC_OFF_POOL_CHUNK_SIZE);
pool->count = readU32(table + offset + FF_ARSC_OFF_POOL_COUNT);
pool->headerSize = readU16(table + offset + 2);
pool->stringsStart = readU32(table + offset + FF_ARSC_OFF_POOL_STRINGS_START);
pool->utf8 = (readU32(table + offset + FF_ARSC_OFF_POOL_FLAGS) & FF_ARSC_POOL_FLAG_UTF8) != 0;
return pool->headerSize >= FF_ARSC_POOL_HEADER_SIZE && pool->chunkSize > pool->headerSize;
}
// The length in front of a string. A UTF-8 pool writes one or two bytes, a UTF-16 pool one or two
// 16-bit words, and the top bit of the first says which of the two it is.
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength8(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position >= pool->tableSize) {
return false;
}
uint32_t value = pool->table[(*position)++];
if (value & 0x80) {
if (*position >= pool->tableSize) {
return false;
}
value = ((value & 0x7F) << 8) | pool->table[(*position)++];
}
*length = value;
return true;
}
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength16(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position + 2 > pool->tableSize) {
return false;
}
uint32_t value = readU16(pool->table + *position);
*position += 2;
if (value & 0x8000) {
if (*position + 2 > pool->tableSize) {
return false;
}
value = ((value & 0x7FFF) << 16) | readU16(pool->table + *position);
*position += 2;
}
*length = value;
return true;
}
[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) {
char encoded[4] = {};
uint32_t length = 0;
if (code < 0x80) {
encoded[length++] = (char) code;
} else if (code < 0x800) {
encoded[length++] = (char) (0xC0 | (code >> 6));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else if (code < 0x10000) {
encoded[length++] = (char) (0xE0 | (code >> 12));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else {
encoded[length++] = (char) (0xF0 | (code >> 18));
encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
}
ffStrbufAppendNS(result, length, encoded);
}
// One string out of a pool.
//
// The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they
// differ for everything outside ASCII. Decoding `characters` bytes is the trap: a label of a few
// multi-byte characters comes back as its first character followed by replacement bytes. Measured on
// the test device, where every resource table read is a UTF-8 pool, this was the difference between
// the right label and mojibake.
[[gnu::nonnull(1, 3), nodiscard]] static bool poolGet(const FFAndroidLabelPool* pool, uint32_t index, FFstrbuf* result) {
if (index >= pool->count) {
return false;
}
const size_t offsets = pool->offset + pool->headerSize;
if (offsets + ((size_t) index + 1) * 4 > pool->tableSize) {
return false;
}
size_t position = pool->offset + pool->stringsStart + readU32(pool->table + offsets + (size_t) index * 4);
if (position >= pool->tableSize) {
return false;
}
if (pool->utf8) {
uint32_t characters = 0;
uint32_t bytes = 0;
if (!readLength8(pool, &position, &characters) || !readLength8(pool, &position, &bytes)) {
return false;
}
// An empty string is reported as a miss rather than written: the caller sets its fallback
// before asking, and ffStrbufSetNS() with a length of zero *clears* the buffer, which would
// throw that fallback away instead of leaving it alone.
if (bytes == 0 || bytes > pool->tableSize - position) {
return false;
}
ffStrbufSetNS(result, bytes, (const char*) (pool->table + position));
return true;
}
uint32_t units = 0;
if (!readLength16(pool, &position, &units)) {
return false;
}
if (units == 0 || (size_t) units * 2 > pool->tableSize - position) {
return false;
}
for (uint32_t i = 0; i < units; i++) {
uint32_t code = readU16(pool->table + position + (size_t) i * 2);
// A code point outside the basic plane arrives as a surrogate pair, and the two halves are
// not characters of their own: half of a pair encoded on its own is not valid UTF-8.
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < units) {
const uint32_t low = readU16(pool->table + position + ((size_t) i + 1) * 2);
if (low >= 0xDC00 && low <= 0xDFFF) {
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
i++;
}
}
appendCodePoint(result, code);
}
return true;
}
// The one ResTable_package this table defines, with the two pools a lookup starts from.
typedef struct FFAndroidLabelTable {
const uint8_t* data;
size_t size;
FFAndroidLabelPool values;
FFAndroidLabelPool types;
size_t packageOffset;
size_t packageHeaderSize;
size_t packageSize;
uint32_t packageId;
} FFAndroidLabelTable;
[[gnu::nonnull(1), nodiscard]] static bool tableInit(FFAndroidLabelTable* table, const uint8_t* data, size_t size) {
// ResTable_header: header(8) packageCount(4). The header's own size is where the global pool
// starts, which is 12 in every table measured -- but it is the field rather than the number.
if (size < 12 || readU16(data) != FF_ARSC_TABLE_TYPE) {
return false;
}
table->data = data;
table->size = size;
const uint32_t headerSize = readU16(data + 2);
if (!poolInit(&table->values, data, size, headerSize)) {
return false;
}
size_t position = (size_t) headerSize + table->values.chunkSize;
while (position + 8 <= size) {
const uint16_t type = readU16(data + position);
const uint16_t chunkHeader = readU16(data + position + 2);
const uint32_t chunkSize = readU32(data + position + 4);
if (chunkSize < 8 || chunkSize > size - position) {
break;
}
// A table can define several packages -- an app that links a shared library's resources has
// more than one -- and the id in the resource reference is what picks the one to read.
if (type == FF_ARSC_TABLE_PACKAGE_TYPE && chunkSize >= chunkHeader && chunkHeader >= FF_ARSC_OFF_PACKAGE_TYPE_STRINGS + 4) {
table->packageOffset = position;
table->packageHeaderSize = chunkHeader;
table->packageSize = chunkSize;
table->packageId = readU32(data + position + FF_ARSC_OFF_PACKAGE_ID);
if (!poolInit(&table->types, data, size, position + readU32(data + position + FF_ARSC_OFF_PACKAGE_TYPE_STRINGS))) {
return false;
}
return true;
}
position += chunkSize;
}
return false;
}
// A type chunk whose ResTable_config is all zeroes is the one that applies when nothing about the
// device or the locale matches a more specific chunk. An app with a translation has several chunks
// for one type, and picking the first one that happens along would answer with whatever language it
// was written in.
[[gnu::nonnull(1), nodiscard]] static bool isDefaultConfig(const uint8_t* config, size_t length) {
if (length == 0) {
return false;
}
for (size_t i = 0; i < length; i++) {
if (config[i] != 0) {
return false;
}
}
return true;
}
[[gnu::nonnull(1, 5), nodiscard]] static bool resolveIn(const FFAndroidLabelTable* table, uint32_t typeId, uint32_t entryId, bool defaultOnly, FFstrbuf* result) {
const size_t end = table->packageOffset + table->packageSize;
for (size_t position = table->packageOffset + table->packageHeaderSize; position + 8 <= end;) {
const uint16_t type = readU16(table->data + position);
const uint16_t header = readU16(table->data + position + 2);
const uint32_t size = readU32(table->data + position + 4);
if (size < 8 || size > end - position) {
return false;
}
if (type == FF_ARSC_TABLE_TYPE_TYPE && header >= FF_ARSC_OFF_TYPE_CONFIG) {
const uint8_t id = table->data[position + FF_ARSC_OFF_TYPE_ID];
const uint8_t flags = table->data[position + FF_ARSC_OFF_TYPE_FLAGS];
if (id == typeId && (flags & (FF_ARSC_TYPE_FLAG_SPARSE | FF_ARSC_TYPE_FLAG_OFFSET16)) == 0 && size >= header) {
const uint32_t entryCount = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRY_COUNT);
const uint32_t entriesStart = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRIES_START);
const size_t offsets = position + header;
if (entryId < entryCount && offsets + ((size_t) entryId + 1) * 4 <= end) {
if (defaultOnly && !isDefaultConfig(table->data + position + FF_ARSC_OFF_TYPE_CONFIG, (size_t) header - FF_ARSC_OFF_TYPE_CONFIG)) {
position += size;
continue;
}
const uint32_t offset = readU32(table->data + offsets + (size_t) entryId * 4);
// `entriesStart` is relative to the chunk, not to the package, and both it and
// the entry's own offset come out of the file.
if (offset != FF_ARSC_NO_ENTRY && entriesStart <= size && offset <= size - entriesStart) {
const size_t entry = position + entriesStart + offset;
const size_t value = entry + readU16(table->data + entry);
if (value >= entry && value + 8 <= end && (readU16(table->data + entry + 2) & FF_ARSC_ENTRY_FLAG_COMPLEX) == 0
&& table->data[value + FF_ARSC_OFF_VALUE_DATA_TYPE] == FF_ARSC_TYPE_STRING) {
return poolGet(&table->values, readU32(table->data + value + FF_ARSC_OFF_VALUE_DATA), result);
}
}
}
}
}
position += size;
}
return false;
}
// Resolves one resource id to its string, and only when the type it names is a `string`.
//
// The type name is the whole reason this reads the package's type pool rather than trusting the id:
// `labelRes` and the icon, logo and banner ids that sit beside it in the same reply are all resource
// ids of the same package, and the difference between them is the type they point at. A `mipmap` id
// resolves perfectly well -- to `res/9w.png`.
[[gnu::nonnull(1, 3), nodiscard]] static bool tableResolve(const FFAndroidLabelTable* table, uint32_t resourceId, FFstrbuf* result) {
const uint32_t typeId = (resourceId >> 16) & 0xFF;
if (((resourceId >> 24) & 0xFF) != table->packageId || typeId == 0) {
return false;
}
FFstrbuf typeName;
ffStrbufInit(&typeName);
const bool isString = poolGet(&table->types, typeId - 1, &typeName) && ffStrbufEqualS(&typeName, FF_ANDROID_LABEL_STRING_TYPE);
ffStrbufDestroy(&typeName);
if (!isString) {
return false;
}
// The configuration that applies is the unqualified one; a chunk that only applies to a locale
// or a density is the fallback rather than the answer, which is what the second pass is for.
return resolveIn(table, typeId, resourceId & 0xFFFF, true, result)
|| resolveIn(table, typeId, resourceId & 0xFFFF, false, result);
}
// ---------------------------------------------------------------------------------------------
// The reply
// ---------------------------------------------------------------------------------------------
// The reply's own copy of the APK path. `sourceDir` is a string in the ApplicationInfo parcel and it
// ends in `.apk`; nothing else in the reply does, which is what makes it findable without walking
// the parcel's field order -- that would mean recovering `ApplicationInfo.writeToParcel` out of the
// device's dex and then framing sixty fields of unknown width, to reach one string that is already
// recognizable.
[[gnu::nonnull(1, 3), nodiscard]] static bool findApkPath(const uint8_t* data, size_t size, char* result, size_t capacity) {
for (size_t i = 8; i < size; i++) {
if (data[i] != '/') {
continue;
}
size_t end = i;
while (end < size && data[end] >= 0x20 && data[end] < 0x7F) {
end++;
}
const size_t length = end - i;
if (length >= 5 && length < capacity && memcmp(data + end - 4, ".apk", 4) == 0) {
memcpy(result, data + i, length);
result[length] = '\0';
return true;
}
i = end;
}
return false;
}
// ---------------------------------------------------------------------------------------------
typedef struct FFAndroidLabelMapping {
const uint8_t* data;
size_t size;
} FFAndroidLabelMapping;
[[gnu::nonnull(1)]] static void labelUnmap(FFAndroidLabelMapping* mapping) {
if (mapping->data != nullptr) {
munmap((void*) mapping->data, mapping->size);
}
}
[[gnu::nonnull(2), nodiscard]] static const char* labelMap(const char* path, FFAndroidLabelMapping* mapping) {
const int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(%s) failed: %s", path, strerror(errno));
return "open(apk) failed";
}
struct stat status = {};
const bool failed = fstat(fd, &status) != 0;
if (failed || status.st_size <= 0) {
FF_DEBUG("fstat(%s) reported %s", path, failed ? strerror(errno) : "an empty file");
close(fd);
return "fstat(apk) failed";
}
void* mapped = mmap(nullptr, (size_t) status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (mapped == MAP_FAILED) {
FF_DEBUG("mmap(%s, %lld bytes) failed: %s", path, (long long) status.st_size, strerror(errno));
return "mmap(apk) failed";
}
mapping->data = mapped;
mapping->size = (size_t) status.st_size;
return nullptr;
}
// One reply buffer for every call this file makes, and it makes one per query.
static uint8_t s_reply[FF_ANDROID_LABEL_REPLY_SIZE];
const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result) {
// The transaction code comes out of the jar rather than from a table here: it is a property of
// the `.aidl` the device's ROM was built from, and a vendor that inserts a method ahead of this
// one shifts every code behind it. See common/android/dex.h.
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
const FFDexStaticIntRequest lookup = {
.classDescriptor = FF_ANDROID_LABEL_STUB,
.fieldName = FF_ANDROID_LABEL_TRANSACTION,
.result = &transaction,
};
const char* error = ffDexStaticInts(FF_ANDROID_LABEL_JAR, &lookup, 1);
if (error != nullptr) {
return error;
}
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
return "framework.jar does not declare IPackageManager.getApplicationInfo";
}
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
error = ffBinderOpen(&binder);
if (error != nullptr) {
return error;
}
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_ANDROID_LABEL_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
if (error != nullptr) {
FF_DEBUG("Looking up \"%s\" failed: %s", FF_ANDROID_LABEL_SERVICE, error);
return error;
}
uint8_t request[FF_ANDROID_LABEL_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_ANDROID_LABEL_DESCRIPTOR);
ffBinderParcelPutString16(&parcel, packageName);
// `getApplicationInfo(String, long flags, int userId)`. The flags argument is 64-bit from Android
// 13 on and 32-bit before it, and the wider one is what works on both: a release that reads an
// int takes the low word and leaves the zero high word unread, because an AIDL stub stops
// reading once its own arguments are in. The narrow one is not symmetric -- a release that reads
// a long would take half of its own flags argument for the user id.
ffBinderParcelPutU64(&parcel, 0);
ffBinderParcelPutI32(&parcel, 0);
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
if (error != nullptr) {
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
return "The package manager rejected the request";
}
// The AIDL status first, then the presence marker of a nullable Parcelable: a package the caller
// is not allowed to see answers an empty ApplicationInfo rather than an error.
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
return "The package manager refused to describe the app";
}
if (ffBinderReadI32(reply.data, reply.size, 4) == 0) {
FF_DEBUG("The package manager described \"%s\" as an empty ApplicationInfo", packageName);
return nullptr;
}
char path[FF_ANDROID_LABEL_PATH_SIZE];
if (!findApkPath(reply.data, reply.size, path, sizeof(path))) {
return "The reply carries no APK path";
}
FF_DEBUG("\"%s\" is installed at \"%s\"", packageName, path);
[[gnu::cleanup(labelUnmap)]] FFAndroidLabelMapping mapping = {};
error = labelMap(path, &mapping);
if (error != nullptr) {
return error;
}
FFAndroidZipEntry table = {};
error = ffAndroidZipFindEntry(mapping.data, mapping.size, FF_ANDROID_LABEL_TABLE, &table);
if (error != nullptr) {
return error;
}
if (table.method != FF_ZIP_METHOD_STORED) {
FF_DEBUG("\"%s\" stores resources.arsc with compression method %u", packageName, (unsigned) table.method);
return "The resource table is stored compressed";
}
FFAndroidLabelTable resources = {};
if (!tableInit(&resources, table.payload, table.payloadSize)) {
return "The resource table could not be read";
}
// Every word of the reply shaped like one of this package's resource ids, in the order they
// appear, and the first one that resolves to a `string` is the label. See the header for what
// that rule was audited against and why it beats recovering the parcel's field order.
for (size_t offset = 8; offset + 4 <= reply.size; offset += 4) {
if (tableResolve(&resources, readU32(reply.data + offset), result)) {
FF_DEBUG("\"%s\" shows itself as \"%s\"", packageName, result->chars);
return nullptr;
}
}
FF_DEBUG("No resource id in the reply of \"%s\" resolves to a string", packageName);
return nullptr;
}
-37
View File
@@ -1,37 +0,0 @@
#pragma once
#include "fastfetch.h"
// The name an app presents to the user, which is not its package name: an app installed as
// `com.termux` presents itself as `Termux:Launcher` on the launcher, in its notifications and on
// the settings screen.
//
// Every route that answers with an *already resolved* label is closed to an app UID, and each one
// was measured on the test device rather than assumed:
//
// * `dumpsys package` needs android.permission.DUMP.
// * `cmd package dump` is filtered down to 929 bytes for an app UID, against 1.2 MB for the shell.
// * The task descriptions the activity service hands out only ever describe the caller's own task.
// * `getApplicationInfo` answers with `labelRes` -- a resource id -- and leaves `nonLocalizedLabel`
// null, so there is no string in the reply to read.
//
// What is open is the APK itself. `getApplicationInfo` carries its path (`sourceDir`), APKs are
// world readable, and the `resources.arsc` inside one is *stored* rather than deflated -- the
// platform expects to mmap it -- so resolving an id out of it needs no decompressor. That is the
// route this takes: one transaction, one mapping, no fork and no child process.
//
// The label is the resource id in the reply whose type is `string`. That is a rule about the reply
// rather than about the parcel's field order, and it was audited against the manifest's own
// `android:label` over every package installed on the test device: nine in ten have exactly one
// `string` candidate and it is the right one every time, a handful more have two and the first is
// still the right one, and the rest have none because their label is a literal string rather than a
// reference (`android:label="Example"`), so the reply carries no id at all. No package resolved to
// a wrong string; the counts behind that are in doc/app-label.md.
//
// Writes the label into `result` when it can be resolved, and leaves `result` alone otherwise --
// which includes the cases above where there is nothing to resolve. The caller sets its fallback
// first, so that an unresolved label costs nothing but the query.
//
// Returns nullptr on success, a static message when the query itself could not be made. An error is
// not a fallback: it says the label was not looked for, not that the app has none.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result);
+286 -729
View File
File diff suppressed because it is too large Load Diff
-103
View File
@@ -62,106 +62,3 @@ typedef struct FFDexStaticIntRequest {
// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened // caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
// or does not hold a dex that can be read. nullptr means the lookup ran. // or does not hold a dex that can be read. nullptr means the lookup ran.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count); [[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
// ---------------------------------------------------------------------------------------------
// Reading a Parcelable's wire layout
// ---------------------------------------------------------------------------------------------
//
// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
// that order is the only thing that says where each one is. It is not in the class's declaration: a
// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
// declaration order a reader would want is not recorded anywhere but the method body.
//
// Recovering it therefore means walking the bytecode. The compiler emits
//
// iget v0, p0, LDisplayInfo;->logicalWidth:I
// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
//
// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
// easy to get wrong in a way that still produces a plausible-looking list:
//
// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
// The table below is ART's `Instruction::SizeInCodeUnits`.
// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
// first of them rather than trying to step over it.
//
// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
// again inside the loop that writes the elements. A class cannot declare two instance fields under
// one name, so a repeat is always that reload, and the first occurrence is where the field is
// written. The reader de-duplicates by name for that reason.
//
// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
// does not. Measured over three releases' own `framework.jar`, that is four names per class.
//
// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
// ones a parser written against one release reads out of the middle of another. Reading the sequence
// from the build that is running makes every one of them optional by construction.
// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
// than truncated: half a name would silently match nothing, which is indistinguishable from the field
// not being there.
#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
typedef struct FFDexParcelField {
char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
} FFDexParcelField;
// One class, and the two things that can be asked about it: the write sequence of one of its methods,
// and which instance fields it declares. Either half may be left out, and both are answered out of
// the same `class_data_item` -- which is why they are one request rather than two calls.
//
// Asking for them together is not a convenience. Both halves are read from a walk that is dominated
// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a
// different part of a 51 MB mapping -- and the fields a class declares are listed in the very
// `class_data_item` the sequence is read out of. A second call would map the jar again, open every
// entry again and make that pass again to reach a list the first call already had in hand. Measured
// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup.
typedef struct FFDexParcelRequest {
const char* classDescriptor;
// The write sequence. Left out by a request that only wants the instance fields, in which case
// `fields`, `capacity` and `count` are not read.
const char* methodName;
FFDexParcelField* fields;
uint32_t capacity;
uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
// The instance fields. This is the question a Parcelable whose layout is not field-driven needs.
// `DisplayCutout` is one: its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors
// the compiler synthesised, so there is no `iget` to walk and no class name on the wire either.
// What does say how long its body is, is which fields the class carries -- Android 11 declares
// three, Android 13 adds `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each
// one adds a known run of writes behind the three both have. A declaration is a field set rather
// than an order, which is exactly what this answers.
const char* const* instanceFieldNames;
bool* instanceFieldResults;
uint32_t instanceFieldCount;
} FFDexParcelRequest;
// Resolves every request out of the dex entries of `jarPath`, walking `classes.dex`, `classes2.dex`,
// ... in order until one of them defines the class -- the same walk, and the same one pass over the
// type table, that ffDexStaticInts makes.
//
// Every request is written its answers before anything can fail: a count of 0 for the sequence, and
// `false` for every instance field. A class the jar does not declare, a method it does not declare,
// and a sequence longer than the caller's `capacity` all leave the count at 0, and none of them holds
// back the others: they are independent questions about the same pass. A count of 0 is not a failure
// but an answer -- this build does not write that class, so a caller reading it has nothing to walk.
// The return value is reserved for the jar itself, as in ffDexStaticInts.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);
-100
View File
@@ -1,100 +0,0 @@
#include "common/android/zip.h"
#include "common/debug.h"
#include <string.h>
static uint16_t zipU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t zipU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result) {
const size_t entryLength = strlen(entry);
if (archiveSize < FF_ZIP_EOCD_SIZE) {
return "The archive is too small to be a zip";
}
// The record is the last thing in the file, so it is searched for backwards -- over at most the
// 64 KiB a trailing comment can take, not over the whole archive.
const size_t newest = archiveSize - FF_ZIP_EOCD_SIZE;
const size_t oldest = archiveSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
? archiveSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
: 0;
size_t eocd = SIZE_MAX;
for (size_t i = newest;; --i) {
if (memcmp(archive + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
eocd = i;
break;
}
if (i == oldest) {
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
// before it stops would wrap and read the whole file backwards, which is the very scan
// this is here to avoid.
break;
}
}
if (eocd == SIZE_MAX) {
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte archive",
newest - oldest + FF_ZIP_EOCD_SIZE, archiveSize);
return "The archive has no zip directory";
}
const uint32_t directorySize = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
const uint32_t directoryOffset = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
if (directoryOffset > archiveSize || directorySize > archiveSize - directoryOffset) {
return "The zip directory is out of range";
}
// The directory is walked to its end rather than for the entry count the record carries: that
// count is a 16-bit field which a 65535-entry archive pins at 0xFFFF and moves into a zip64
// record, while the two bounds above describe the directory itself on every archive this small.
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
if (memcmp(archive + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
return "A zip directory entry is malformed";
}
const uint16_t nameLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
const uint16_t extraLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
const uint16_t commentLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
if (entrySize > directoryEnd - offset) {
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
entrySize, (size_t) (directoryEnd - offset));
return "A zip directory entry runs past the directory";
}
if (nameLength == entryLength && memcmp(archive + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
const uint32_t compressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
const uint32_t uncompressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
const uint32_t localOffset = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressedSize == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
FF_DEBUG("The zip entry \"%s\" is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
entry, compressedSize, uncompressedSize, localOffset);
return "The zip entry is stored as zip64, which is not supported";
}
if (localOffset > archiveSize || FF_ZIP_LOCAL_HEADER_SIZE > archiveSize - localOffset) {
return "A zip local header is out of range";
}
const uint16_t localNameLength = zipU16(archive + localOffset + FF_ZIP_OFF_NAME_LENGTH);
const uint16_t localExtraLength = zipU16(archive + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
if (headerSize > archiveSize - localOffset || compressedSize > archiveSize - localOffset - headerSize) {
return "The zip entry bounds are out of range";
}
result->method = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_METHOD);
result->uncompressedSize = uncompressedSize;
result->payload = archive + localOffset + headerSize;
result->payloadSize = compressedSize;
return nullptr;
}
offset += entrySize;
}
FF_DEBUG("No entry in the %zu byte archive is named \"%s\"", archiveSize, entry);
return "The archive has no entry with that name";
}
-66
View File
@@ -1,66 +0,0 @@
#pragma once
#include "fastfetch.h"
// Locating an entry inside a zip, which is what both the jar reader and the app label reader need:
// one looks for `classesN.dex`, the other for the resource table of an APK.
//
// The entry is found through the central directory rather than by scanning for its local header,
// and that is not a micro-optimisation. A local header is preceded by everything that came before
// it, so scanning costs a pass over all of it, once per lookup: `framework.jar` is 52 MB and its
// `classes2.dex` starts 9.6 MB in. Measured on the test device, where the sound module resolves
// seven codes, looking the entries up by scanning was the difference between 30 ms and 2 seconds.
//
// The sizes and the compression method are read from the directory rather than from the entry's own
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
// choose that. Only the local header's two length fields are used, because they are what says where
// the payload begins -- the local extra field does not have to match the central one.
// The zip local file header, from APPNOTE.TXT 4.3.7.
#define FF_ZIP_LOCAL_HEADER_SIZE 30
#define FF_ZIP_OFF_NAME_LENGTH 26
#define FF_ZIP_OFF_EXTRA_LENGTH 28
// The central directory.
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
#define FF_ZIP_CENTRAL_SIZE 46
#define FF_ZIP_OFF_CENTRAL_METHOD 10
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
// The end of central directory record, which points at the directory. It is the last thing in the
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
#define FF_ZIP_EOCD_SIZE 22
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
// and the APKs measured so far are orders of magnitude below that, and carrying the extra field
// reader for them is not worth it.
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
#define FF_ZIP_METHOD_STORED 0
#define FF_ZIP_METHOD_DEFLATED 8
// One entry, as the archive describes it. `payload` points into the mapping the caller passed in and
// is not a copy: for a STORED entry it is the file's own bytes, and for a DEFLATED one it is the raw
// deflate stream, which the caller inflates with `uncompressedSize` as the bound.
typedef struct FFAndroidZipEntry {
const uint8_t* payload;
size_t payloadSize;
uint32_t uncompressedSize;
uint16_t method;
} FFAndroidZipEntry;
// Resolves `entry` through the archive's central directory, writing where its payload is into
// `result`. Returns nullptr on success, a static message otherwise -- an archive that is not a zip,
// a directory that is out of range, an entry that is not there and an entry stored as zip64 are all
// reported rather than answered with a pointer into nothing.
[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result);
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>@BUNDLE_ID@</string> <string>fastfetch</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>@PROJECT_NAME@</string> <string>@PROJECT_NAME@</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
+5 -12
View File
@@ -64,11 +64,7 @@ static void getExePath(FFPlatform* platform) {
exePathLen--; // remove terminating NUL exePathLen--; // remove terminating NUL
} }
#elif defined(__OpenBSD__) #elif defined(__OpenBSD__)
#if FF_HAVE_GETEXECPATH // OpenBSD doesn't have a reliable way to get the executable path.
// https://man.openbsd.org/getexecpath.3
size_t exePathLen = getexecpath(exePath, sizeof(exePath)) == 0 ? strlen(exePath) : 0;
#else
// OpenBSD (before 8.0) doesn't have a reliable way to get the executable path.
// Current implementation uses argv[0], which can be easily spoofed. // Current implementation uses argv[0], which can be easily spoofed.
// See #2195 // See #2195
size_t exePathLen = 0; size_t exePathLen = 0;
@@ -169,7 +165,6 @@ static void getExePath(FFPlatform* platform) {
} }
} }
} }
#endif
#elif defined(__sun) #elif defined(__sun)
ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1); ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
if (exePathLen > 0) { if (exePathLen > 0) {
@@ -304,7 +299,10 @@ static void getHostName(FFPlatform* platform, const struct utsname* uts) {
} }
static void getUserShell(FFPlatform* platform, const struct passwd* pwd) { static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
const char* shell = pwd ? pwd->pw_shell : getenv("SHELL"); const char* shell = getenv("SHELL");
if (!ffStrSet(shell) && pwd) {
shell = pwd->pw_shell;
}
ffStrbufAppendS(&platform->userShell, shell); ffStrbufAppendS(&platform->userShell, shell);
} }
@@ -338,12 +336,7 @@ static void getCwd(FFPlatform* platform) {
void ffPlatformInitImpl(FFPlatform* platform) { void ffPlatformInitImpl(FFPlatform* platform) {
platform->pid = (uint32_t) getpid(); platform->pid = (uint32_t) getpid();
platform->uid = getuid(); platform->uid = getuid();
#if !__ANDROID__
struct passwd* pwd = getpwuid(platform->uid); struct passwd* pwd = getpwuid(platform->uid);
#else
// On Android, /etc/passwd is empty, and getpwuid() will return `/data` for pw_dir
struct passwd* pwd = nullptr;
#endif
struct utsname uts; struct utsname uts;
if (uname(&uts) < 0) { if (uname(&uts) < 0) {
+3 -6
View File
@@ -28,19 +28,16 @@ void ffDebugPrint(const char* file, int line, const char* format, ...) {
} }
static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER; static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER;
// Saved before the lock and restored after the unlock: FF_DEBUG() is called from error paths
// that go on to read `errno`, so nothing this function does -- neither the lock nor the
// formatting -- may be allowed to leave its own value behind.
int errno_ = errno;
ffThreadMutexLock(&debugMutex); ffThreadMutexLock(&debugMutex);
static char errmsg_[1024]; // Don't increase stack size int errno_ = errno;
static char errmsg_[1024];
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vsnprintf(errmsg_, sizeof(errmsg_), format, args); vsnprintf(errmsg_, sizeof(errmsg_), format, args);
va_end(args); va_end(args);
fprintf(stderr, "[%s%4d, %s] %s\n", ffFindFileName(file), line, ffTimeToTimeStr(ffTimeGetNow()), errmsg_); fprintf(stderr, "[%s%4d, %s] %s\n", ffFindFileName(file), line, ffTimeToTimeStr(ffTimeGetNow()), errmsg_);
ffThreadMutexUnlock(&debugMutex);
errno = errno_; errno = errno_;
ffThreadMutexUnlock(&debugMutex);
} }
#if _WIN32 #if _WIN32
+1 -1
View File
@@ -552,7 +552,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3
if (end < 0) { if (end < 0) {
end = (int32_t) tempString.length + end; end = (int32_t) tempString.length + end;
} }
if (end >= 0 && (uint32_t) end > tempString.length) { if ((uint32_t) end > tempString.length) {
end = (int32_t) tempString.length; end = (int32_t) tempString.length;
} }
if (end > start) { if (end > start) {
+23 -42
View File
@@ -17,7 +17,7 @@
#if FF_HAVE_WORDEXP #if FF_HAVE_WORDEXP
#include <wordexp.h> #include <wordexp.h>
#elif FF_HAVE_GLOB #else
#include <glob.h> #include <glob.h>
#endif #endif
@@ -112,7 +112,7 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
wordfree(&exp); wordfree(&exp);
#elif FF_HAVE_GLOB #else
glob_t gb; glob_t gb;
if (glob(in, GLOB_NOSORT if (glob(in, GLOB_NOSORT
@@ -134,10 +134,6 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
globfree(&gb); globfree(&gb);
#else
FF_UNUSED(in, out)
#endif #endif
return result; return result;
@@ -149,32 +145,6 @@ void restoreTerm(void) {
tcsetattr(ftty, TCSAFLUSH, &oldTerm); tcsetattr(ftty, TCSAFLUSH, &oldTerm);
} }
static const char* waitForTerminalResponse(void) {
#ifndef __APPLE__
const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS);
if (pollResult <= 0) {
FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s",
pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno));
return "poll(/dev/tty) timeout or failed";
}
#else
{
// On macOS, poll(/dev/tty) always returns immediately
// See also https://nathancraddock.com/blog/macos-dev-tty-polling/
fd_set rd;
FD_ZERO(&rd);
FD_SET(ftty, &rd);
const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 });
if (selectResult <= 0) {
FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s",
selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno));
return "select(/dev/tty) timeout or failed";
}
}
#endif
return nullptr;
}
const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
if (ftty < 0) { if (ftty < 0) {
ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
@@ -200,10 +170,28 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
ffWriteFDData(ftty, strlen(request), request); ffWriteFDData(ftty, strlen(request), request);
// Give the terminal some time to respond // Give the terminal some time to respond
const char* error = waitForTerminalResponse(); #ifndef __APPLE__
if (error) { const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS);
return error; if (pollResult <= 0) {
FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s",
pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno));
return "poll(/dev/tty) timeout or failed";
} }
#else
{
// On macOS, poll(/dev/tty) always returns immediately
// See also https://nathancraddock.com/blog/macos-dev-tty-polling/
fd_set rd;
FD_ZERO(&rd);
FD_SET(ftty, &rd);
const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 });
if (selectResult <= 0) {
FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s",
selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno));
return "select(/dev/tty) timeout or failed";
}
}
#endif
char buffer[1024]; char buffer[1024];
size_t bytesRead = 0; size_t bytesRead = 0;
@@ -212,13 +200,6 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
va_start(args, format); va_start(args, format);
while (true) { while (true) {
// The response may arrive in several chunks; a terminal that sends an incomplete
// response would otherwise block `read` forever
if (bytesRead > 0 && (error = waitForTerminalResponse())) {
va_end(args);
return error;
}
ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1); ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
if (nRead <= 0) { if (nRead <= 0) {
+23 -43
View File
@@ -414,35 +414,6 @@ void ffListFilesRecursively(const char* path, bool pretty) {
listFilesRecursively(folder.length, &folder, 0, nullptr, pretty); listFilesRecursively(folder.length, &folder, 0, nullptr, pretty);
} }
static const char* waitForTerminalResponse(HANDLE hInput) {
while (true) {
NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 });
if (status != STATUS_WAIT_0) {
FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status));
return "NtWaitForSingleObject() failed or timeout";
}
// Ignore all unexpected input events
INPUT_RECORD record;
DWORD len = 0;
if (!PeekConsoleInputW(hInput, &record, 1, &len)) {
return nullptr;
}
// Key-up events carry no input for `ReadFile`; leaving one in the buffer would make the
// wait return while the next `ReadFile` still blocks
if (
record.EventType == KEY_EVENT &&
record.Event.KeyEvent.bKeyDown &&
record.Event.KeyEvent.uChar.UnicodeChar != L'\r' &&
record.Event.KeyEvent.uChar.UnicodeChar != L'\n') {
return nullptr;
}
ReadConsoleInputW(hInput, &record, 1, &len);
}
}
const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE; FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE;
@@ -469,10 +440,29 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, nullptr); WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, nullptr);
} }
const char* error = waitForTerminalResponse(hInput); while (true) {
if (error) { NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 });
SetConsoleMode(hInput, inputMode); if (status != STATUS_WAIT_0) {
return error; FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status));
SetConsoleMode(hInput, inputMode);
return "NtWaitForSingleObject() failed or timeout";
}
// Ignore all unexpected input events
INPUT_RECORD record;
DWORD len = 0;
if (!PeekConsoleInputW(hInput, &record, 1, &len)) {
break;
}
if (
record.EventType == KEY_EVENT &&
record.Event.KeyEvent.uChar.UnicodeChar != L'\r' &&
record.Event.KeyEvent.uChar.UnicodeChar != L'\n') {
break;
} else {
ReadConsoleInputW(hInput, &record, 1, &len);
}
} }
va_list args; va_list args;
@@ -482,16 +472,6 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
uint32_t bytesRead = 0; uint32_t bytesRead = 0;
while (true) { while (true) {
// The response may arrive in several chunks; a terminal that sends an incomplete
// response would otherwise block `ReadFile` forever
if (bytesRead > 0 && (error = waitForTerminalResponse(hInput))) {
va_end(args);
if (hasInputMode) {
SetConsoleMode(hInput, inputMode);
}
return error;
}
DWORD bytes = 0; DWORD bytes = 0;
BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr); BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr);
if (!readOk || bytes == 0) { if (!readOk || bytes == 0) {
+2 -5
View File
@@ -88,11 +88,8 @@ void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConf
FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type; FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
uint8_t blocksPercent = 0; uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5);
if (percent > 0) { assert(blocksPercent <= options->barWidth);
double blocks = percent / 100.0 * options->barWidth + 0.5;
blocksPercent = (uint8_t) (blocks >= options->barWidth ? options->barWidth : blocks);
}
bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto"); bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
+5 -9
View File
@@ -62,7 +62,7 @@ static inline int ffPipe2(int* readEnd, int* writeEnd, int flags) {
} }
// Not thread-safe // Not thread-safe
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) { const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
FF_AUTO_CLOSE_FD int pipeRead = -1; FF_AUTO_CLOSE_FD int pipeRead = -1;
FF_AUTO_CLOSE_FD int pipeWrite = -1; FF_AUTO_CLOSE_FD int pipeWrite = -1;
if (ffPipe2(&pipeRead, &pipeWrite, O_CLOEXEC) == -1) { if (ffPipe2(&pipeRead, &pipeWrite, O_CLOEXEC) == -1) {
@@ -72,10 +72,6 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
pid_t childPid = -1; pid_t childPid = -1;
int nullFile = ffGetNullFD(); int nullFile = ffGetNullFD();
// Each stream is wired to the pipe when its bit is set and to the null device otherwise, so an
// unselected stream can neither mix into the captured text nor fill the pipe and block the child.
int stdoutFd = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? pipeWrite : nullFile;
int stderrFd = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? pipeWrite : nullFile;
#if !(__ANDROID__ || __OpenBSD__) #if !(__ANDROID__ || __OpenBSD__)
@@ -87,8 +83,8 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_init(&file_actions); posix_spawn_file_actions_init(&file_actions);
posix_spawn_file_actions_adddup2(&file_actions, stdoutFd, STDOUT_FILENO); posix_spawn_file_actions_adddup2(&file_actions, pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&file_actions, stderrFd, STDERR_FILENO); posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
if (ffIsValidNativeFD(stdinFd)) { if (ffIsValidNativeFD(stdinFd)) {
posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO); posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO);
} }
@@ -153,8 +149,8 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
if (childPid == 0) { if (childPid == 0) {
// Child process // Child process
dup2(stdoutFd, STDOUT_FILENO); dup2(pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO);
dup2(stderrFd, STDERR_FILENO); dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
if (ffIsValidNativeFD(stdinFd)) { if (ffIsValidNativeFD(stdinFd)) {
dup2(stdinFd, STDIN_FILENO); dup2(stdinFd, STDIN_FILENO);
} }
+12 -20
View File
@@ -29,7 +29,7 @@ static void argvToCmdline(char* const argv[], FFstrbuf* result) {
} }
// Add quotes around string if whitespace chars are present (with slash duplicating at the end of string) // Add quotes around string if whitespace chars are present (with slash duplicating at the end of string)
if (ffStrbufContainAnyC(&temp, " \t")) { if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) {
uint32_t pos = temp.length; uint32_t pos = temp.length;
uint32_t cnt = 0; uint32_t cnt = 0;
while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; } while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
@@ -84,7 +84,7 @@ static wchar_t* createChildEnvironment(void) {
return result; return result;
} }
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) { const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
const int32_t timeout = instance.config.general.processingTimeout; const int32_t timeout = instance.config.general.processingTimeout;
wchar_t pipeName[32]; wchar_t pipeName[32];
@@ -127,12 +127,13 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
.cb = sizeof(siStartInfo), .cb = sizeof(siStartInfo),
.dwFlags = STARTF_USESTDHANDLES, .dwFlags = STARTF_USESTDHANDLES,
}; };
if (useStdErr) {
HANDLE nullFile = ffGetNullFD(); siStartInfo.hStdOutput = ffGetNullFD();
// A stream wired to the pipe is captured; the other goes to the null device so it cannot mix siStartInfo.hStdError = hChildPipeWrite;
// into the captured text or fill the pipe and block the child. Both may share the pipe handle. } else {
siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile; siStartInfo.hStdOutput = hChildPipeWrite;
siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile; siStartInfo.hStdError = ffGetNullFD();
}
if (ffIsValidNativeFD(stdinFd)) { if (ffIsValidNativeFD(stdinFd)) {
siStartInfo.hStdInput = stdinFd; siStartInfo.hStdInput = stdinFd;
} }
@@ -141,7 +142,6 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
{ {
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
argvToCmdline(argv, &buf); argvToCmdline(argv, &buf);
FF_DEBUG("Command line: %s", buf.chars);
uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t); uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
cmdline = malloc(cmdlineBytes); cmdline = malloc(cmdlineBytes);
NTSTATUS status = RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1); NTSTATUS status = RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1);
@@ -167,12 +167,10 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
NtClose(hChildPipeWrite); NtClose(hChildPipeWrite);
if (!success) { if (!success) {
DWORD error = GetLastError(); if (GetLastError() == ERROR_FILE_NOT_FOUND) {
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
FF_DEBUG("Command not found: %s (%s)", argv[0], ffDebugWin32Error(error));
return "command not found"; return "command not found";
} }
FF_DEBUG("CreateProcessW(%s) failed: %s", argv[0], ffDebugWin32Error(error)); FF_DEBUG("CreateProcessW() failed: %s", ffDebugWin32Error(GetLastError()));
return "CreateProcessW() failed"; return "CreateProcessW() failed";
} }
@@ -265,13 +263,7 @@ exit: {
if (NT_SUCCESS(status)) { if (NT_SUCCESS(status)) {
assert(size == sizeof(info)); assert(size == sizeof(info));
if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) { if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
if (info.ExitStatus == 9009) { FF_DEBUG("Child process exited with an error");
// Note: CMD swallows 9009 errorlevel and returns 1 by default.
// One must use `& call exit %^ERRORLEVEL%` to propagate the correct error code.
FF_DEBUG("Child process exited with error code 9009 (command not found)");
return "command not found";
}
FF_DEBUG("Child process exited with an error: %x %s", (unsigned) info.ExitStatus, ffDebugNtStatus(info.ExitStatus));
return "Child process exited with an error"; return "Child process exited with an error";
} }
} else { } else {
-81
View File
@@ -1,81 +0,0 @@
#include "common/zlib.h"
#include "common/debug.h"
#if FF_HAVE_ZLIB && !FF_DISABLE_DLOPEN
struct FFZlibData zlibData;
// Opens the library and leaves the handle in zlibData, so the groups that follow resolve against the
// same mapping instead of opening their own.
static const char* openZlib(void) {
#ifdef _WIN32
FF_LIBRARY_LOAD_MESSAGE(zlib, "zlib1" FF_LIBRARY_EXTENSION, 2)
#else
FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2)
#endif
zlibData.handle = zlib;
zlib = nullptr; // the handle is kept in zlibData, so the cleanup must not close it
return nullptr;
}
const char* ffZlibLoad(FFZlibGroup group) {
FF_DEBUG("Loading zlib group: %x", group);
// Opened once, by whichever group is asked for first; the later ones reuse that mapping. A
// failed open is retried by the next group rather than cached, so a system without libz pays at
// most one attempt per group -- not one per call.
if (zlibData.handle == nullptr) {
if (zlibData.attempted == UINT8_MAX) {
return "Previous attempt to open zlib failed";
}
const char* error = openZlib();
if (error != nullptr) {
static_assert(sizeof(zlibData.attempted) == 1, "zlibData.attempted must be 1 byte");
zlibData.attempted = UINT8_MAX;
return error;
}
}
if (group & FF_ZLIB_COMPRESS) {
if (zlibData.attempted & FF_ZLIB_COMPRESS) {
if (!zlibData.ffcompress2) {
return "Previous attempt to load compress failed";
}
} else {
zlibData.attempted |= FF_ZLIB_COMPRESS;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compressBound)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compress2)
}
}
if (group & FF_ZLIB_INFLATE) {
if (zlibData.attempted & FF_ZLIB_INFLATE) {
if (!zlibData.ffinflateEnd) {
return "Previous attempt to load inflate failed";
}
} else {
zlibData.attempted |= FF_ZLIB_INFLATE;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateInit2_)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflate)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateEnd)
}
}
if (group & FF_ZLIB_GZFILE) {
if (zlibData.attempted & FF_ZLIB_GZFILE) {
if (!zlibData.ffgzclose) {
return "Previous attempt to load gzfile failed";
}
} else {
zlibData.attempted |= FF_ZLIB_GZFILE;
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzopen)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzread)
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzclose)
}
}
return nullptr;
}
#endif
-1
View File
@@ -13,7 +13,6 @@
#define LUALIB_API LUA_API #define LUALIB_API LUA_API
#define LUAMOD_API LUA_API #define LUAMOD_API LUA_API
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
#pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wunused-function"
#endif #endif
+6 -1
View File
@@ -61,7 +61,7 @@ typedef struct FFNetworkingState {
// `Content-Length` header in place of `Transfer-Encoding`. // `Content-Length` header in place of `Transfer-Encoding`.
// `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller // `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller
// passed in no longer describes the buffer afterwards. Callers must use the new one for anything // passed in no longer describes the buffer afterwards. Callers must use the new one for anything
// that addresses the body. // that addresses the body, such as ffNetworkingDecompressGzip().
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd); [[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd);
// Result of parsing a `Transfer-Encoding` header value // Result of parsing a `Transfer-Encoding` header value
@@ -77,3 +77,8 @@ typedef enum FFNetworkingTransferEncoding {
// client cannot decode. Only a lone `chunked` is accepted; everything else is reported // 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. // as unsupported so that the caller fails the response instead of returning garbage.
[[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen); [[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen);
#ifdef FF_HAVE_ZLIB
const char* ffNetworkingLoadZlibLibrary(void);
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd);
#endif
+3 -14
View File
@@ -17,29 +17,18 @@ typedef struct FFProcessHandle {
#endif #endif
} FFProcessHandle; } FFProcessHandle;
// Which of the child's two output streams are wired to the pipe that `ffProcessReadOutput()` drains.
// The stream that is not selected is pointed at the null device, so it can neither interleave with
// the captured text nor fill the pipe and block the child. This is a bitfield rather than a value
// because `BOTH` is a combination: each backend sets up the child's two stdio slots independently,
// testing one bit each.
typedef enum FFProcessOutputType: uint8_t {
FF_PROCESS_OUTPUT_STDOUT_BIT = 1 << 0,
FF_PROCESS_OUTPUT_STDERR_BIT = 1 << 1,
FF_PROCESS_OUTPUT_BOTH = FF_PROCESS_OUTPUT_STDOUT_BIT | FF_PROCESS_OUTPUT_STDERR_BIT,
} FFProcessOutputType;
// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from // `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. // 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 // 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 // service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then
// only reports `Failure calling service <name>: Failed transaction`. // only reports `Failure calling service <name>: Failed transaction`.
#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1) #define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1)
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle); const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle);
const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally
static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) { static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) {
FFProcessHandle handle; FFProcessHandle handle;
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, FF_PROCESS_INHERIT_STDIN, &handle); const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle);
if (error) { if (error) {
return error; return error;
} }
@@ -53,7 +42,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar
static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) { static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) {
FFProcessHandle handle; FFProcessHandle handle;
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDERR_BIT, FF_PROCESS_INHERIT_STDIN, &handle); const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle);
if (error) { if (error) {
return error; return error;
} }
-24
View File
@@ -719,10 +719,6 @@ static inline bool ffIsSystemBasicProcessInfoAvailable() { // Includes HandleCou
return SharedUserData->NtBuildNumber >= 26200; // MSDN says it was added in 26100.4770; ntdoc says 25H2 return SharedUserData->NtBuildNumber >= 26200; // MSDN says it was added in 26100.4770; ntdoc says 25H2
} }
static inline bool ffIsFileFsFullSizeInformationExAvailable() {
return SharedUserData->NtBuildNumber >= 17763; // Windows 10 October 2018 Update (1809, REDSTONE5)
}
NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken( NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken(
_In_ HANDLE ProcessHandle, _In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess, _In_ ACCESS_MASK DesiredAccess,
@@ -1423,23 +1419,3 @@ NTSYSAPI NTSTATUS NTAPI NtTerminateProcess(
NTSYSAPI NTSTATUS NTAPI RtlAcquirePebLock(VOID); NTSYSAPI NTSTATUS NTAPI RtlAcquirePebLock(VOID);
NTSYSAPI NTSTATUS NTAPI RtlReleasePebLock(VOID); NTSYSAPI NTSTATUS NTAPI RtlReleasePebLock(VOID);
typedef struct _FILE_FS_FULL_SIZE_INFORMATION_EX {
ULONGLONG ActualTotalAllocationUnits;
ULONGLONG ActualAvailableAllocationUnits;
ULONGLONG ActualPoolUnavailableAllocationUnits;
ULONGLONG CallerTotalAllocationUnits;
ULONGLONG CallerAvailableAllocationUnits;
ULONGLONG CallerPoolUnavailableAllocationUnits;
ULONGLONG UsedAllocationUnits;
ULONGLONG TotalReservedAllocationUnits;
ULONGLONG VolumeStorageReserveAllocationUnits;
ULONGLONG AvailableCommittedAllocationUnits;
ULONGLONG PoolAvailableAllocationUnits;
ULONG SectorsPerAllocationUnit;
ULONG BytesPerSector;
} FILE_FS_FULL_SIZE_INFORMATION_EX, *PFILE_FS_FULL_SIZE_INFORMATION_EX;
enum {
FileFsFullSizeInformationEx = 14
};
-111
View File
@@ -1,111 +0,0 @@
#pragma once
#include "fastfetch.h"
#if FF_HAVE_ZLIB
// Which set of entry points a caller needs. Resolving is per group rather than all at once so that a
// symbol nobody uses cannot make a caller fail: libz is not one library across the platforms
// fastfetch builds on, and the gz family in particular is not guaranteed to be present.
typedef enum FFZlibGroup : uint8_t {
FF_ZLIB_COMPRESS = 1 << 0, // compressBound, compress2
FF_ZLIB_INFLATE = 1 << 1, // inflateInit2_, inflate, inflateEnd
FF_ZLIB_GZFILE = 1 << 2, // gzopen, gzread, gzclose
} FFZlibGroup;
#if FF_DISABLE_DLOPEN
#include <zlib.h>
static inline const char* ffZlibLoad([[maybe_unused]] FFZlibGroup group) {
// the library is linked in: nothing to open, nothing to resolve, nothing to fail
return nullptr;
}
#else
// Hack. <zlib.h> declares everything with an extern linkage macro, which prevents us from
// implementing the functions ourselves. zlib spells it ZEXTERN/ZEXPORT and zlib-ng's
// zlib-compat header spells it Z_EXTERN/Z_EXPORT; both set theirs under `#ifndef`, so
// defining all four before the include is what makes the definitions at the bottom of this
// header legal -- they inherit the static inline linkage from the declarations zlib.h emits
// here. This header must therefore be the first thing in a translation unit to pull in
// <zlib.h>; if it is not, the definitions below stop being static and clang reports
// `static declaration follows non-static declaration` right there.
#undef ZEXTERN
#undef ZEXPORT
#undef Z_EXTERN
#undef Z_EXPORT
#define ZEXTERN static inline
#define ZEXPORT
#define Z_EXTERN static inline
#define Z_EXPORT
// Redeclaring everything as `static inline` above means <zlib.h> now defines entry points
// that a given translation unit may not call at all. Silenced for the include only: the
// pragma is popped right after, so the rest of the file -- and the rest of the translation
// unit that includes this header -- still reports unused functions.
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
#pragma GCC diagnostic ignored "-Wunused-function"
#include <zlib.h>
#include "common/library.h"
extern struct FFZlibData {
// Kept open rather than closed once opened: every group resolves against this one mapping, and
// re-opening per group would pay the cold load again.
void* handle;
FF_LIBRARY_SYMBOL(compressBound)
FF_LIBRARY_SYMBOL(compress2)
FF_LIBRARY_SYMBOL(inflateInit2_)
FF_LIBRARY_SYMBOL(inflate)
FF_LIBRARY_SYMBOL(inflateEnd)
FF_LIBRARY_SYMBOL(gzopen)
FF_LIBRARY_SYMBOL(gzread)
FF_LIBRARY_SYMBOL(gzclose)
FFZlibGroup attempted;
} zlibData;
[[gnu::always_inline]] static inline uLong(compressBound)(uLong sourceLen) {
return zlibData.ffcompressBound(sourceLen);
}
[[gnu::always_inline]] static inline int(compress2)(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen, int level) {
return zlibData.ffcompress2(dest, destLen, source, sourceLen, level);
}
[[gnu::always_inline]] static inline int(inflateInit2_)(z_streamp strm, int windowBits, const char* version, int stream_size) {
return zlibData.ffinflateInit2_(strm, windowBits, version, stream_size);
}
[[gnu::always_inline]] static inline int(inflate)(z_streamp strm, int flush) {
return zlibData.ffinflate(strm, flush);
}
[[gnu::always_inline]] static inline int(inflateEnd)(z_streamp strm) {
return zlibData.ffinflateEnd(strm);
}
[[gnu::always_inline]] static inline gzFile(gzopen)(const char* path, const char* mode) {
return zlibData.ffgzopen(path, mode);
}
[[gnu::always_inline]] static inline int(gzread)(gzFile file, voidp buf, unsigned len) {
return zlibData.ffgzread(file, buf, len);
}
[[gnu::always_inline]] static inline int(gzclose)(gzFile file) {
return zlibData.ffgzclose(file);
}
// Resolves the entry points of `group` and returns nullptr, or an error message. Call it before using
// that group's functions: until it has succeeded, the forwarders above are null pointers. The library
// itself is opened once, by whichever group is asked for first, and stays open.
const char* ffZlibLoad(FFZlibGroup group);
#endif
#endif
+3 -3
View File
@@ -12,7 +12,7 @@
// Records the largest size the camera can stream in the given pixel format. The stream configurations // 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. // are a flat array of (format, width, height, isInput) quads; a negative format matches any of them.
static void getMaxStreamSize(const ACameraMetadata* metadata, int32_t format, uint32_t* width, uint32_t* height) { static void ffCameraMaxStreamSize(const ACameraMetadata* metadata, int32_t format, uint32_t* width, uint32_t* height) {
ACameraMetadata_const_entry entry; ACameraMetadata_const_entry entry;
if (ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry) != ACAMERA_OK) { if (ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry) != ACAMERA_OK) {
return; return;
@@ -94,11 +94,11 @@ const char* ffDetectCamera(FFlist* result) {
} }
camera->width = camera->height = 0; camera->width = camera->height = 0;
getMaxStreamSize(metadata, FF_ANDROID_PIXEL_FORMAT_JPEG, &camera->width, &camera->height); ffCameraMaxStreamSize(metadata, FF_ANDROID_PIXEL_FORMAT_JPEG, &camera->width, &camera->height);
if (camera->width == 0) { if (camera->width == 0) {
// Not every camera HAL lists a JPEG format. The largest size of any output format is the // Not every camera HAL lists a JPEG format. The largest size of any output format is the
// same resolution on those devices. // same resolution on those devices.
getMaxStreamSize(metadata, -1, &camera->width, &camera->height); ffCameraMaxStreamSize(metadata, -1, &camera->width, &camera->height);
} }
ACameraMetadata_free(metadata); ACameraMetadata_free(metadata);
+1 -7
View File
@@ -15,19 +15,13 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand
return "No command text specified"; return "No command text specified";
} }
#if _WIN32
if (ffStrbufEqualS(&options->shell, "cmd.exe") && ffStrbufEqualS(&options->param, "/d/c")) {
ffStrbufAppendS(&options->text, " & exit %^errorlevel%"); // Necessary to propagate 9009 from CMD
}
#endif
return ffProcessSpawn(options->param.length ? (char* const[]) { return ffProcessSpawn(options->param.length ? (char* const[]) {
options->shell.chars, options->shell.chars,
options->param.chars, options->param.chars,
options->text.chars, options->text.chars,
nullptr } nullptr }
: (char* const[]) { options->shell.chars, options->text.chars, nullptr }, : (char* const[]) { options->shell.chars, options->text.chars, nullptr },
options->useOutput, options->useStdErr,
FF_PROCESS_INHERIT_STDIN, FF_PROCESS_INHERIT_STDIN,
handle); handle);
} }
+2
View File
@@ -22,6 +22,8 @@ const char* ffDetectDisks(FFDiskOptions* options, FFlist* disks) {
FF_LIST_FOR_EACH (FFDisk, disk, *disks) { FF_LIST_FOR_EACH (FFDisk, disk, *disks) {
if (disk->bytesTotal == 0) { if (disk->bytesTotal == 0) {
disk->type |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT; disk->type |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT;
} else {
disk->bytesUsed = disk->bytesTotal - (options->calcType == FF_DISK_CALC_TYPE_FREE ? disk->bytesFree : disk->bytesAvailable);
} }
} }
+2
View File
@@ -16,7 +16,9 @@ typedef struct FFDisk {
FFstrbuf name; FFstrbuf name;
FFDiskVolumeType type; FFDiskVolumeType type;
uint64_t bytesUsed;
uint64_t bytesFree; uint64_t bytesFree;
uint64_t bytesAvailable;
uint64_t bytesTotal; uint64_t bytesTotal;
uint32_t filesUsed; uint32_t filesUsed;
+3 -1
View File
@@ -188,7 +188,9 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); FFDisk* disk = FF_LIST_ADD(FFDisk, *disks);
disk->bytesTotal = (uint64_t) fs->f_blocks * (uint64_t) fs->f_bsize; disk->bytesTotal = (uint64_t) fs->f_blocks * (uint64_t) fs->f_bsize;
disk->bytesFree = (uint64_t) (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs->f_bfree : fs->f_bavail) * (uint64_t) fs->f_bsize; disk->bytesFree = (uint64_t) fs->f_bfree * (uint64_t) fs->f_bsize;
disk->bytesAvailable = (uint64_t) fs->f_bavail * (uint64_t) fs->f_bsize;
disk->bytesUsed = 0; // To be filled in ./disk.c
disk->filesTotal = (uint32_t) fs->f_files; disk->filesTotal = (uint32_t) fs->f_files;
disk->filesUsed = (uint32_t) fs->f_files - (uint32_t) fs->f_ffree; disk->filesUsed = (uint32_t) fs->f_files - (uint32_t) fs->f_ffree;
+2
View File
@@ -44,6 +44,8 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
disk->bytesTotal = (uint64_t) fs.total_blocks * (uint64_t) fs.block_size; disk->bytesTotal = (uint64_t) fs.total_blocks * (uint64_t) fs.block_size;
disk->bytesFree = (uint64_t) fs.free_blocks * (uint64_t) fs.block_size; disk->bytesFree = (uint64_t) fs.free_blocks * (uint64_t) fs.block_size;
disk->bytesAvailable = disk->bytesFree;
disk->bytesUsed = 0; // To be filled in ./disk.c
disk->filesTotal = (uint32_t) fs.total_nodes; disk->filesTotal = (uint32_t) fs.total_nodes;
disk->filesUsed = (uint32_t) (fs.total_nodes - fs.free_nodes); disk->filesUsed = (uint32_t) (fs.total_nodes - fs.free_nodes);
+5 -3
View File
@@ -249,14 +249,16 @@ static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mntent*
#endif #endif
static void detectStats(FFDiskOptions* options, FFDisk* disk) { static void detectStats(FFDisk* disk) {
struct statvfs fs; struct statvfs fs;
if (statvfs(disk->mountpoint.chars, &fs) != 0) { if (statvfs(disk->mountpoint.chars, &fs) != 0) {
memset(&fs, 0, sizeof(fs)); // Set all values to 0, so our values get initialized to 0 too memset(&fs, 0, sizeof(fs)); // Set all values to 0, so our values get initialized to 0 too
} }
disk->bytesTotal = fs.f_blocks * (uint64_t) fs.f_frsize; disk->bytesTotal = fs.f_blocks * (uint64_t) fs.f_frsize;
disk->bytesFree = (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs.f_bfree : fs.f_bavail) * (uint64_t) fs.f_frsize; disk->bytesFree = fs.f_bfree * (uint64_t) fs.f_frsize;
disk->bytesAvailable = fs.f_bavail * (uint64_t) fs.f_frsize;
disk->bytesUsed = 0; // To be filled in ./disk.c
if (fs.f_files >= fs.f_ffree) { if (fs.f_files >= fs.f_ffree) {
disk->filesTotal = (uint32_t) fs.f_files; disk->filesTotal = (uint32_t) fs.f_files;
@@ -328,7 +330,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
detectType(disks, disk, device); detectType(disks, disk, device);
// Detects stats // Detects stats
detectStats(options, disk); detectStats(disk);
} }
endmntent(mountsFile); endmntent(mountsFile);
+5 -3
View File
@@ -85,14 +85,16 @@ static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mnttab*
} }
} }
static void detectStats(FFDiskOptions* options, FFDisk* disk) { static void detectStats(FFDisk* disk) {
struct statvfs fs; struct statvfs fs;
if (statvfs(disk->mountpoint.chars, &fs) != 0) { if (statvfs(disk->mountpoint.chars, &fs) != 0) {
memset(&fs, 0, sizeof(fs)); memset(&fs, 0, sizeof(fs));
} }
disk->bytesTotal = fs.f_blocks * fs.f_frsize; disk->bytesTotal = fs.f_blocks * fs.f_frsize;
disk->bytesFree = (options->calcType == FF_DISK_CALC_TYPE_FREE ? fs.f_bfree : fs.f_bavail) * fs.f_frsize; disk->bytesFree = fs.f_bfree * fs.f_frsize;
disk->bytesAvailable = fs.f_bavail * fs.f_frsize;
disk->bytesUsed = 0; // To be filled in ./disk.c
disk->filesTotal = (uint32_t) fs.f_files; disk->filesTotal = (uint32_t) fs.f_files;
disk->filesUsed = (uint32_t) (disk->filesTotal - fs.f_ffree); disk->filesUsed = (uint32_t) (disk->filesTotal - fs.f_ffree);
@@ -136,7 +138,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
ffStrbufInitS(&disk->filesystem, device.mnt_fstype); ffStrbufInitS(&disk->filesystem, device.mnt_fstype);
ffStrbufInit(&disk->name); ffStrbufInit(&disk->name);
detectType(disks, disk, &device); detectType(disks, disk, &device);
detectStats(options, disk); detectStats(disk);
} }
return nullptr; return nullptr;
+8 -15
View File
@@ -75,6 +75,8 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
disk->filesTotal = 0; disk->filesTotal = 0;
disk->bytesTotal = 0; disk->bytesTotal = 0;
disk->bytesFree = 0; disk->bytesFree = 0;
disk->bytesUsed = 0; // To be filled in ./disk.c
disk->bytesAvailable = 0;
disk->createTime = 0; disk->createTime = 0;
ffStrbufInit(&disk->filesystem); ffStrbufInit(&disk->filesystem);
ffStrbufInit(&disk->name); ffStrbufInit(&disk->name);
@@ -116,21 +118,12 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
} }
} }
if (options->calcType == FF_DISK_CALC_TYPE_AVAILABLE || !ffIsFileFsFullSizeInformationExAvailable()) { FILE_FS_FULL_SIZE_INFORMATION fsFullSize;
FILE_FS_FULL_SIZE_INFORMATION fsFullSize; if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) {
if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) { uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit;
uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit; disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units;
disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units; disk->bytesFree = (uint64_t) fsFullSize.ActualAvailableAllocationUnits.QuadPart * units;
disk->bytesFree = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units; disk->bytesAvailable = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units;
}
} else {
FILE_FS_FULL_SIZE_INFORMATION_EX fsFullSizeEx;
if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSizeEx, sizeof(fsFullSizeEx), FileFsFullSizeInformationEx))) {
// Space info without considering the quota setting
uint64_t units = fsFullSizeEx.BytesPerSector * fsFullSizeEx.SectorsPerAllocationUnit;
disk->bytesTotal = (uint64_t) fsFullSizeEx.ActualTotalAllocationUnits * units;
disk->bytesFree = (uint64_t) fsFullSizeEx.ActualAvailableAllocationUnits * units;
}
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,425 +0,0 @@
#include "wayland.h"
#if defined(FF_HAVE_WAYLAND) && defined(FF_HAVE_DBUS) && defined(__linux__)
#include "common/arrutil.h"
#include "common/dbus.h"
#include "common/debug.h"
#include "common/edidHelper.h"
#include "common/strutil.h"
#include <string.h>
// Read the display configuration of GNOME / Mutter and Cinnamon / Muffin, including the raw EDID
// data, instead of guessing the DRM connector by matching the wl_output name (#...).
// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
// MetaMonitorColorMode
// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/src/backends/meta-monitor-manager-private.h
#define FF_MUTTER_COLOR_MODE_BT2100 1
// Muffin (Cinnamon) exposes the very same interface as Mutter (GNOME), only the names differ
typedef struct FFMutterDisplayConfig {
const char* busName;
const char* objectPath;
const char* interface;
} FFMutterDisplayConfig;
static const FFMutterDisplayConfig mutterDisplayConfigs[] = {
{
.busName = "org.gnome.Mutter.DisplayConfig",
.objectPath = "/org/gnome/Mutter/DisplayConfig",
.interface = "org.gnome.Mutter.DisplayConfig",
},
{
.busName = "org.cinnamon.Muffin.DisplayConfig",
.objectPath = "/org/cinnamon/Muffin/DisplayConfig",
.interface = "org.cinnamon.Muffin.DisplayConfig",
},
};
static inline DBusMessage* mutterCall(FFDBusData* dbus, const FFMutterDisplayConfig* config, const char* method) {
return ffDBusGetMethodReply(dbus, config->busName, config->objectPath, config->interface, method, nullptr, nullptr);
}
// Returns the first display configuration interface that responds, or nullptr
static const FFMutterDisplayConfig* mutterFindConfig(FFDBusData* dbus) {
for (uint32_t i = 0; i < ARRAY_SIZE(mutterDisplayConfigs); ++i) {
DBusMessage* reply = mutterCall(dbus, &mutterDisplayConfigs[i], "GetCurrentState");
if (reply != nullptr) {
dbus->lib->ffdbus_message_unref(reply);
return &mutterDisplayConfigs[i];
}
}
return nullptr;
}
static FFDisplayResult* mutterFindDisplay(FFDisplayServerResult* result, const char* connector) {
if (connector == nullptr || *connector == '\0') {
return nullptr;
}
const uint64_t id = ffWaylandGenerateIdFromName(connector);
FF_LIST_FOR_EACH (FFDisplayResult, display, result->displays) {
if (display->id == id) {
display->platformApi = "wayland-mutter";
return display;
}
}
return nullptr;
}
// Point `value` to the variant stored under `key` in the `a{sv}` dictionary `dict`
static bool mutterDictGet(FFDBusData* dbus, DBusMessageIter* dict, const char* key, DBusMessageIter* value) {
if (dbus->lib->ffdbus_message_iter_get_arg_type(dict) != DBUS_TYPE_ARRAY) {
return false;
}
DBusMessageIter entries;
dbus->lib->ffdbus_message_iter_recurse(dict, &entries);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus->lib->ffdbus_message_iter_recurse(&entries, &entry);
if (dbus->lib->ffdbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
const char* entryKey = nullptr;
dbus->lib->ffdbus_message_iter_get_basic(&entry, &entryKey);
if (entryKey && ffStrEquals(entryKey, key) && dbus->lib->ffdbus_message_iter_next(&entry)) {
*value = entry;
return true;
}
}
if (!dbus->lib->ffdbus_message_iter_next(&entries)) {
break;
}
}
return false;
}
// Whether the `au` array contained in the variant `variant` contains `needle`
static bool mutterVariantContainsUint32(FFDBusData* dbus, DBusMessageIter* variant, uint32_t needle) {
if (dbus->lib->ffdbus_message_iter_get_arg_type(variant) != DBUS_TYPE_VARIANT) {
return false;
}
DBusMessageIter array;
dbus->lib->ffdbus_message_iter_recurse(variant, &array);
if (dbus->lib->ffdbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY) {
return false;
}
DBusMessageIter items;
dbus->lib->ffdbus_message_iter_recurse(&array, &items);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&items) == DBUS_TYPE_UINT32) {
uint32_t item = 0;
dbus->lib->ffdbus_message_iter_get_basic(&items, &item);
if (item == needle) {
return true;
}
if (!dbus->lib->ffdbus_message_iter_next(&items)) {
break;
}
}
return false;
}
// Copy the `ay` byte array contained in the variant `variant` into `data`
static bool mutterVariantGetBytes(FFDBusData* dbus, DBusMessageIter* variant, uint8_t* data, uint32_t capacity, uint32_t* length) {
uint32_t count = 0;
if (dbus->lib->ffdbus_message_iter_get_arg_type(variant) == DBUS_TYPE_VARIANT) {
DBusMessageIter array;
dbus->lib->ffdbus_message_iter_recurse(variant, &array);
if (dbus->lib->ffdbus_message_iter_get_arg_type(&array) == DBUS_TYPE_ARRAY) {
DBusMessageIter items;
dbus->lib->ffdbus_message_iter_recurse(&array, &items);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&items) == DBUS_TYPE_BYTE) {
uint8_t byte = 0;
dbus->lib->ffdbus_message_iter_get_basic(&items, &byte);
if (count < capacity) {
data[count] = byte;
}
++count;
if (!dbus->lib->ffdbus_message_iter_next(&items)) {
break;
}
}
}
}
*length = count;
return count > 0 && count <= capacity;
}
static void mutterApplyEdid(FFDisplayResult* display, const uint8_t* edid, uint32_t length) {
FF_STRBUF_AUTO_DESTROY edidName = ffStrbufCreate();
if (ffEdidGetName(edid, &edidName) && edidName.length > 0) {
ffStrbufSet(&display->name, &edidName);
}
if (display->serial.length == 0) {
ffEdidGetSerial(edid, &display->serial);
}
if (display->manufactureYear == 0) {
ffEdidGetManufactureDate(edid, &display->manufactureYear, &display->manufactureWeek);
}
if (display->hdrStatus == FF_DISPLAY_HDR_STATUS_UNKNOWN) {
display->hdrStatus = ffEdidGetHdrCompatible(edid, length)
? FF_DISPLAY_HDR_STATUS_SUPPORTED
: FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
}
if (display->preferredWidth == 0) {
uint32_t width = 0, height = 0;
double refreshRate = 0;
ffEdidGetPreferredResolutionAndRefreshRate(edid, &width, &height, &refreshRate);
display->preferredWidth = width;
display->preferredHeight = height;
display->preferredRefreshRate = refreshRate;
}
}
static void mutterApplyMonitorProperties(FFDBusData* dbus, DBusMessageIter* properties, FFDisplayResult* display) {
DBusMessageIter value;
// "is-builtin" is authoritative, unlike ffdsGetDisplayType() which guesses from the connector name
if (mutterDictGet(dbus, properties, "is-builtin", &value)) {
bool builtin = false;
if (ffDBusGetBool(dbus, &value, &builtin)) {
display->type = builtin ? FF_DISPLAY_TYPE_BUILTIN : FF_DISPLAY_TYPE_EXTERNAL;
}
}
bool hdrEnabled = false;
if (mutterDictGet(dbus, properties, "color-mode", &value)) {
uint64_t colorMode = 0;
hdrEnabled = ffDBusGetUint(dbus, &value, &colorMode) && colorMode == FF_MUTTER_COLOR_MODE_BT2100;
}
bool hdrSupported = false;
if (mutterDictGet(dbus, properties, "supported-color-modes", &value)) {
hdrSupported = mutterVariantContainsUint32(dbus, &value, FF_MUTTER_COLOR_MODE_BT2100);
}
if (hdrEnabled) {
display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED;
} else if (hdrSupported && display->hdrStatus == FF_DISPLAY_HDR_STATUS_UNKNOWN) {
display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED;
}
}
static void mutterParseCurrentState(FFDBusData* dbus, DBusMessage* reply, FFDisplayServerResult* result) {
DBusMessageIter root;
if (!dbus->lib->ffdbus_message_iter_init(reply, &root)) {
return;
}
// (u serial, a(monitors) monitors, a(logical_monitors) logical_monitors, a{sv} properties)
if (!dbus->lib->ffdbus_message_iter_next(&root)) { // monitors
return;
}
if (dbus->lib->ffdbus_message_iter_get_arg_type(&root) == DBUS_TYPE_ARRAY) {
DBusMessageIter monitors;
dbus->lib->ffdbus_message_iter_recurse(&root, &monitors);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&monitors) == DBUS_TYPE_STRUCT) {
DBusMessageIter monitor;
dbus->lib->ffdbus_message_iter_recurse(&monitors, &monitor);
// ((ssss) monitor_specs, a(siiddada{sv}) modes, a{sv} properties)
if (dbus->lib->ffdbus_message_iter_get_arg_type(&monitor) == DBUS_TYPE_STRUCT) {
DBusMessageIter specs;
dbus->lib->ffdbus_message_iter_recurse(&monitor, &specs);
const char* connector = nullptr;
if (dbus->lib->ffdbus_message_iter_get_arg_type(&specs) == DBUS_TYPE_STRING) {
dbus->lib->ffdbus_message_iter_get_basic(&specs, &connector);
}
FFDisplayResult* display = mutterFindDisplay(result, connector);
if (display) {
// (connector, vendor, product, serial)
DBusMessageIter serialSpec = specs;
if (display->serial.length == 0 &&
dbus->lib->ffdbus_message_iter_next(&serialSpec) && // vendor
dbus->lib->ffdbus_message_iter_next(&serialSpec) && // product
dbus->lib->ffdbus_message_iter_next(&serialSpec) && // serial
dbus->lib->ffdbus_message_iter_get_arg_type(&serialSpec) == DBUS_TYPE_STRING) {
const char* serial = nullptr;
dbus->lib->ffdbus_message_iter_get_basic(&serialSpec, &serial);
if (serial && !ffStrEquals(serial, "unknown")) {
ffStrbufSetS(&display->serial, serial);
}
}
// Skip `modes`, then read `properties`
DBusMessageIter properties = monitor;
if (dbus->lib->ffdbus_message_iter_next(&properties) &&
dbus->lib->ffdbus_message_iter_next(&properties) &&
dbus->lib->ffdbus_message_iter_get_arg_type(&properties) == DBUS_TYPE_ARRAY) {
mutterApplyMonitorProperties(dbus, &properties, display);
}
}
}
dbus->lib->ffdbus_message_iter_next(&monitors);
}
}
// logical_monitors -> primary flag
if (!dbus->lib->ffdbus_message_iter_next(&root) ||
dbus->lib->ffdbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) {
return;
}
DBusMessageIter logicalMonitors;
dbus->lib->ffdbus_message_iter_recurse(&root, &logicalMonitors);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitors) == DBUS_TYPE_STRUCT) {
DBusMessageIter logicalMonitor;
dbus->lib->ffdbus_message_iter_recurse(&logicalMonitors, &logicalMonitor);
// (i x, i y, d scale, u transform, b primary, a(ssss) monitors, a{sv} properties)
int index = 0;
bool primary = false;
do {
const int argType = dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitor);
if (index == 4 && argType == DBUS_TYPE_BOOLEAN) {
dbus_bool_t value = 0;
dbus->lib->ffdbus_message_iter_get_basic(&logicalMonitor, &value);
primary = value != 0;
} else if (index == 5 && primary && argType == DBUS_TYPE_ARRAY) {
DBusMessageIter logicalMonitorMonitors;
dbus->lib->ffdbus_message_iter_recurse(&logicalMonitor, &logicalMonitorMonitors);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitorMonitors) == DBUS_TYPE_STRUCT) {
DBusMessageIter specs;
dbus->lib->ffdbus_message_iter_recurse(&logicalMonitorMonitors, &specs);
if (dbus->lib->ffdbus_message_iter_get_arg_type(&specs) == DBUS_TYPE_STRING) {
const char* connector = nullptr;
dbus->lib->ffdbus_message_iter_get_basic(&specs, &connector);
FFDisplayResult* display = mutterFindDisplay(result, connector);
if (display) {
display->primary = true;
}
}
dbus->lib->ffdbus_message_iter_next(&logicalMonitorMonitors);
}
}
++index;
} while (dbus->lib->ffdbus_message_iter_next(&logicalMonitor));
dbus->lib->ffdbus_message_iter_next(&logicalMonitors);
}
}
static void mutterParseResources(FFDBusData* dbus, DBusMessage* reply, FFDisplayServerResult* result) {
DBusMessageIter root;
if (!dbus->lib->ffdbus_message_iter_init(reply, &root)) {
return;
}
// (u serial, a(crtcs) crtcs, a(outputs) outputs, a(modes) modes, i max_screen_width, i max_screen_height)
DBusMessageIter outputs;
if (!dbus->lib->ffdbus_message_iter_next(&root) || // crtcs
!dbus->lib->ffdbus_message_iter_next(&root) || // outputs
dbus->lib->ffdbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) {
return;
}
dbus->lib->ffdbus_message_iter_recurse(&root, &outputs);
while (dbus->lib->ffdbus_message_iter_get_arg_type(&outputs) == DBUS_TYPE_STRUCT) {
DBusMessageIter output;
dbus->lib->ffdbus_message_iter_recurse(&outputs, &output);
// (u id, x output_id, i crtc_index, au crtcs, s name, au modes, au clones, a{sv} properties)
FFDisplayResult* display = nullptr;
int index = 0;
do {
const int argType = dbus->lib->ffdbus_message_iter_get_arg_type(&output);
if (index == 4 && argType == DBUS_TYPE_STRING) {
const char* connector = nullptr;
dbus->lib->ffdbus_message_iter_get_basic(&output, &connector);
display = mutterFindDisplay(result, connector);
} else if (index == 7 && display != nullptr && argType == DBUS_TYPE_ARRAY) {
DBusMessageIter value;
if (mutterDictGet(dbus, &output, "edid", &value)) {
uint8_t edid[1024];
uint32_t length = 0;
if (mutterVariantGetBytes(dbus, &value, edid, ARRAY_SIZE(edid), &length) && ffEdidIsValid(edid, length)) {
mutterApplyEdid(display, edid, length);
}
}
}
++index;
} while (dbus->lib->ffdbus_message_iter_next(&output));
dbus->lib->ffdbus_message_iter_next(&outputs);
}
}
// Whether org.gnome.Mutter.DisplayConfig (GNOME) or org.cinnamon.Muffin.DisplayConfig (Cinnamon)
// is reachable. Used to decide if the wl_output name based DRM connector matching
// (matchDrmConnector) can be skipped.
bool ffdsMutterAvailable(void) {
FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != nullptr) {
return false;
}
return mutterFindConfig(&dbus) != nullptr;
}
// Enrich the displays detected from wl_output with the authoritative data reported by Mutter / Muffin.
// Returns true if the display configuration could be retrieved from D-Bus.
bool ffdsConnectMutter(FFDisplayServerResult* result) {
FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
const char* error = ffDBusLoadData(DBUS_BUS_SESSION, &dbus);
if (error) {
FF_DEBUG("ffDBusLoadData() failed: %s", error);
return false;
}
const FFMutterDisplayConfig* config = mutterFindConfig(&dbus);
if (config == nullptr) {
FF_DEBUG("No Mutter / Muffin DisplayConfig interface found");
return false;
}
DBusMessage* reply = mutterCall(&dbus, config, "GetCurrentState");
if (reply == nullptr) {
FF_DEBUG("GetCurrentState() failed");
return false;
}
mutterParseCurrentState(&dbus, reply, result);
dbus.lib->ffdbus_message_unref(reply);
// GetCurrentState() does not expose the EDID data, so fall back to the legacy GetResources()
reply = mutterCall(&dbus, config, "GetResources");
if (reply != nullptr) {
mutterParseResources(&dbus, reply, result);
dbus.lib->ffdbus_message_unref(reply);
} else {
FF_DEBUG("GetResources() failed");
}
return true;
}
#endif
@@ -157,7 +157,7 @@ void ffWaylandOutputNameListener(void* data, [[maybe_unused]] void* output, cons
display->type = ffdsGetDisplayType(name); display->type = ffdsGetDisplayType(name);
#if __linux__ #if __linux__
if (!display->edidName.length && !display->parent->useMutter) { if (!display->edidName.length) {
matchDrmConnector(name, display); matchDrmConnector(name, display);
} }
#endif #endif
@@ -282,14 +282,6 @@ const char* ffdsConnectWayland(FFDisplayServerResult* result) {
waylandDetectWM(ffwl_display_get_fd(data.display), result); waylandDetectWM(ffwl_display_get_fd(data.display), result);
#if defined(FF_HAVE_DBUS) && defined(__linux__)
// gnome-shell (Mutter) and cinnamon (Muffin) expose the monitor configuration via D-Bus,
// including the raw EDID. Prefer it over matching the wl_output name against the DRM connectors.
data.useMutter =
(ffStrbufEqualS(&result->wmProcessName, "gnome-shell") || ffStrbufEqualS(&result->wmProcessName, "cinnamon")) &&
ffdsMutterAvailable();
#endif
struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, nullptr); struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, nullptr);
if (registry == nullptr) { if (registry == nullptr) {
ffwl_display_disconnect(data.display); ffwl_display_disconnect(data.display);
@@ -317,16 +309,7 @@ const char* ffdsConnectWayland(FFDisplayServerResult* result) {
data.ffwl_proxy_destroy(registry); data.ffwl_proxy_destroy(registry);
ffwl_display_disconnect(data.display); ffwl_display_disconnect(data.display);
bool primaryDetected = false; {
#if defined(FF_HAVE_DBUS) && defined(__linux__)
if (data.useMutter) {
// Mutter / Muffin reports the primary monitor and the raw EDID directly
primaryDetected = ffdsConnectMutter(result);
}
#endif
// Fallback for compositors that do not expose the monitor configuration, or when it could not be read
if (!primaryDetected) {
const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell") const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell")
? "monitors.xml" ? "monitors.xml"
: ffStrbufEqualS(&result->wmProcessName, "cinnamon") : ffStrbufEqualS(&result->wmProcessName, "cinnamon")
@@ -37,7 +37,6 @@ typedef struct WaylandData {
WaylandProtocolType protocolType; WaylandProtocolType protocolType;
struct wl_proxy* zxdgOutputManager; struct wl_proxy* zxdgOutputManager;
struct wl_proxy* wpColorManager; struct wl_proxy* wpColorManager;
bool useMutter; // gnome-shell: read the display configuration from org.gnome.Mutter.DisplayConfig instead of guessing it
} WaylandData; } WaylandData;
typedef struct WaylandDisplay { typedef struct WaylandDisplay {
@@ -93,9 +92,4 @@ const char* ffWaylandHandleKdeOutputOrder(WaylandData* wldata, struct wl_registr
const char* ffWaylandHandleZxdgOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); const char* ffWaylandHandleZxdgOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version);
const char* ffWaylandHandleWpColor(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); const char* ffWaylandHandleWpColor(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version);
#if defined(FF_HAVE_DBUS) && defined(__linux__)
bool ffdsMutterAvailable(void);
bool ffdsConnectMutter(FFDisplayServerResult* result);
#endif
#endif #endif
+1 -1
View File
@@ -113,7 +113,7 @@ static bool readHidReport(HANDLE hHidFile, uint8_t* buffer, uint32_t length, DWO
// than by handle (CancelIo would take every other read on the same handle with it), and the // than by handle (CancelIo would take every other read on the same handle with it), and the
// cancellation is waited for before returning. // cancellation is waited for before returning.
CancelIoEx(hHidFile, &overlapped); CancelIoEx(hHidFile, &overlapped);
GetOverlappedResultEx(hHidFile, &overlapped, &read, INFINITE, FALSE); GetOverlappedResult(hHidFile, &overlapped, &read, TRUE);
return false; return false;
} }
} }
-2
View File
@@ -80,8 +80,6 @@ typedef struct FFGpuDriverPciBusId {
} FFGpuDriverPciBusId; } FFGpuDriverPciBusId;
#if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__) #if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__)
#define FF_GPU_HAS_PCI_IDS 1
void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu);
void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu);
void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu); void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu);
-2
View File
@@ -394,8 +394,6 @@ const char* ffGPUDetectDriverSpecific(const FFGPUOptions* options, FFGPUResult*
}, },
soName); soName);
} }
#else
FF_UNUSED(options, gpu, pciBusId);
#endif #endif
return "No driver-specific detection function found for the GPU vendor"; return "No driver-specific detection function found for the GPU vendor";
+41 -57
View File
@@ -60,78 +60,62 @@ static const FFstrbuf* loadPciIds() {
return &pciids; return &pciids;
} }
void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
FF_DEBUG("Searching pci.ids for vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass); FF_DEBUG("Searching pci.ids for vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass);
if (content->length) { if (content->length) {
const char* const fileEnd = content->chars + content->length;
char buffer[32]; char buffer[32];
// Search for vendor // Search for vendor
uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor); uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor);
const char* start = memmem(content->chars, content->length, buffer, len); char* start = (char*) memmem(content->chars, content->length, buffer, len);
char* end = content->chars + content->length;
if (start) { if (start) {
FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor); FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor);
start += len; start += len;
const char* vendorEnd = memchr(start, '\n', (size_t) (fileEnd - start)); end = memchr(start, '\n', (uint32_t) (end - start));
if (!vendorEnd) { if (!end) {
vendorEnd = fileEnd; end = content->chars + content->length;
} }
if (!gpu->vendor.length) { if (!gpu->vendor.length) {
ffStrbufSetNS(&gpu->vendor, (uint32_t) (vendorEnd - start), start); ffStrbufSetNS(&gpu->vendor, (uint32_t) (end - start), start);
} }
// Search for device. Device lines are `\tXXXX name`; their ids only start = end; // point to '\n' of vendor
// grow inside a vendor block, so one linear walk over the block is end = start + 1; // point to start of devices
// enough: compare in place and stop either at the line starting the // find the start of next vendor
// next vendor, or -- because the ids are sorted -- right after the while (end[0] == '\t' || end[0] == '#') {
// first id greater than the one we are looking for. Doing the compare end = strchr(end, '\n');
// here also removes the need to know where the block ends, so no if (!end) {
// length is ever derived from a pointer that may have run past it. end = content->chars + content->length;
// pci.ids only asks contributors to keep the entries sorted, and it
// is strictly lowercase hex, so the byte comparison below matches the
// numeric order. A block that is not sorted (only reachable via
// FF_CUSTOM_PCI_IDS_PATH) makes us give up early and fall back to the
// synthesized name below.
char deviceId[8];
snprintf(deviceId, ARRAY_SIZE(deviceId), "%04x", device);
[[maybe_unused]] bool found = false;
for (const char* p = vendorEnd < fileEnd ? vendorEnd + 1 : fileEnd;
p < fileEnd && (*p == '\t' || *p == '#');) {
if (*p == '\t' && fileEnd - p >= 7) {
int order = memcmp(p + 1, deviceId, 4);
if (order > 0) {
break;
}
if (order == 0 && p[5] == ' ' && p[6] == ' ') {
FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device);
start = p + 7;
const char* end = memchr(start, '\n', (size_t) (fileEnd - start));
if (!end) {
end = fileEnd;
}
const char* closingBracket = end - 1;
if (*closingBracket == ']') {
const char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start));
if (openingBracket) {
openingBracket++;
ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket);
}
}
if (!gpu->name.length) {
ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start);
}
found = true;
break;
}
}
p = memchr(p, '\n', (size_t) (fileEnd - p));
if (!p) {
break; break;
} else {
end++;
} }
p++;
} }
if (!found) {
// Search for device
len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x ", device);
start = memmem(start, (size_t) (end - start), buffer, len);
if (start) {
FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device);
start += len;
end = memchr(start, '\n', (uint32_t) (end - start));
if (!end) {
end = content->chars + content->length;
}
char* closingBracket = end - 1;
if (*closingBracket == ']') {
char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start));
if (openingBracket) {
openingBracket++;
ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket);
}
}
if (!gpu->name.length) {
ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start);
}
} else {
FF_DEBUG("PCI device entry 0x%04x:0x%04x was not found in pci.ids", vendor, device); FF_DEBUG("PCI device entry 0x%04x:0x%04x was not found in pci.ids", vendor, device);
} }
} else { } else {
@@ -247,7 +231,7 @@ void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device,
return; return;
} }
#endif #endif
ffGPUParsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu);
FF_DEBUG("Resolved PCI GPU name from pci.ids: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars); FF_DEBUG("Resolved PCI GPU name from pci.ids: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars);
} }
+16 -13
View File
@@ -55,32 +55,35 @@ static const char* getSshdVersion(FFstrbuf* version) {
} }
#ifdef FF_HAVE_ZLIB #ifdef FF_HAVE_ZLIB
#include "common/library.h"
#include "common/path.h" #include "common/path.h"
#include "common/zlib.h"
#include <stdlib.h> #include <stdlib.h>
#include <zlib.h>
static const char* getSddmVersion(FFstrbuf* version) { static const char* getSddmVersion(FFstrbuf* version) {
const char* error = ffZlibLoad(FF_ZLIB_GZFILE); FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2)
if (error != nullptr) { FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzopen)
FF_DEBUG("The sddm version cannot be read: %s", error); FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzread)
return error; FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzerror)
} FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gztell)
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzrewind)
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzclose)
gzFile file = gzopen(FF_PATH_PKG_BASE "/share/man/man1/sddm.1.gz", "rb"); gzFile file = ffgzopen(FF_PATH_PKG_BASE "/share/man/man1/sddm.1.gz", "rb");
if (file == Z_NULL) { if (file == Z_NULL) {
FF_DEBUG("gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed: %s", strerror(errno)); FF_DEBUG("ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed: %s", strerror(errno));
return "gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed"; return "ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed";
} }
ffStrbufEnsureFree(version, 2047); ffStrbufEnsureFree(version, 2047);
memset(version->chars, 0, version->allocated); memset(version->chars, 0, version->allocated);
int size = gzread(file, version->chars, version->allocated - 1); int size = ffgzread(file, version->chars, version->allocated - 1);
gzclose(file); ffgzclose(file);
if (size <= 0) { if (size <= 0) {
FF_DEBUG("gzread(file, version->chars, version->length) failed"); FF_DEBUG("ffgzread(file, version->chars, version->length) failed");
return "gzread(file, version->chars, version->length) failed"; return "ffgzread(file, version->chars, version->length) failed";
} }
version->length = (uint32_t) size; version->length = (uint32_t) size;
-141
View File
@@ -1,141 +0,0 @@
#include "localip.h"
#include "common/apple/cf_helpers.h"
#include "common/debug.h"
#include <IOKit/IOBSD.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/network/IONetworkController.h>
#include <ifaddrs.h>
#include <net/if.h>
// macOS 27 hides the link-layer address from every process that is not an Apple platform
// binary, i.e. anything whose code signature has no `Platform identifier` field -- which
// includes ad-hoc and linker-signed builds. Both getifaddrs() and sysctl(NET_RT_IFLIST)
// then report the placeholder 02:00:00:00:00:00 for all interfaces. Re-signing the very
// same /sbin/ifconfig ad-hoc flips its `ether` line from the real address to the
// placeholder while every other field stays byte-identical, so the placeholder itself is
// the tell.
//
// The IORegistry is not affected. The node matching the BSD name never carries the
// property itself and the depth at which it appears depends on the driver -- en0 has it on
// the parent (IO80211Controller), en4 on the parent (AppleUSBDeviceNCM11Data), ap1 on the
// node itself -- so walk up until it shows up.
//
// This reports the *hardware* address. That differs from the address a platform binary
// sees whenever Private Wi-Fi Address is enabled (for en0 the randomized address sits one
// level further up, on IOUserNetworkWLAN), so it is only used to replace the placeholder
// and never preferred over a value read from the interface.
//
// Measured with `fastfetch -s localip --stat` on this machine: replacing the placeholders
// costs ~0.2ms for a single interface and ~1.3ms for the 21 interfaces getifaddrs()
// reports. The SystemConfiguration alternative needed a single SCNetworkInterfaceCopyAll()
// at 2.6-5.2ms (an XPC round trip to configd) and does not even list anpi0-2 or ap1, so the
// IORegistry covers strictly more interfaces for less.
#define FF_LOCALIP_REDACTED_MAC "02:00:00:00:00:00"
// The classes that expose a `BSD Name` in the IORegistry. anpi*/en* are IONetworkInterface
// subclasses, but the dext-based Wi-Fi interfaces (ap1) are IOUserNetworkWLAN and are not,
// so they need a pass of their own.
static const char* const iokitInterfaceClasses[] = {
"IONetworkInterface",
"IOUserNetworkWLAN",
};
// Looks for the first IOMACAddress on `entry` or one of its ancestors. Does not consume it.
static bool getMacFromRegistry(io_object_t entry, FFstrbuf* result) {
if (!entry) {
return false;
}
FF_IOOBJECT_AUTO_RELEASE io_object_t current = entry;
IOObjectRetain(current);
for (uint32_t depth = 0; depth < 4; ++depth) {
FF_CFTYPE_AUTO_RELEASE CFDataRef data = (CFDataRef) IORegistryEntryCreateCFProperty(current, CFSTR(kIOMACAddress), kCFAllocatorDefault, kNilOptions);
if (data) {
if (CFGetTypeID(data) != CFDataGetTypeID() || CFDataGetLength(data) < 6) {
return false;
}
const uint8_t* ptr = CFDataGetBytePtr(data);
ffStrbufSetF(result, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]);
return true;
}
io_object_t parent = IO_OBJECT_NULL;
if (IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent) != KERN_SUCCESS) {
return false;
}
IOObjectRelease(current);
current = parent;
}
return false;
}
static FFLocalIpResult* findRedacted(FFlist* results, const char* ifName) {
FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC) && ffStrbufEqualS(&item->name, ifName)) {
return item;
}
}
return nullptr;
}
// Enumerating the interface classes once and matching on `BSD Name` is measurably cheaper
// than looking every interface up by name -- 1.5ms versus 3.0ms for the 21 interfaces
// getifaddrs() reports here -- which matters as soon as defaultRouteOnly is disabled and a
// dozen interfaces are listed at once. Interfaces the enumeration does not reach are looked
// up by name afterwards, so this can only ever be faster than, never worse than, the direct
// lookup.
void ffLocalIpFixRedactedMacs(FFlist* results) {
bool hasRedacted = false;
FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) {
hasRedacted = true;
break;
}
}
if (!hasRedacted) {
return;
}
for (uint32_t c = 0; c < ARRAY_SIZE(iokitInterfaceClasses); ++c) {
FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(iokitInterfaceClasses[c]), &iterator) != KERN_SUCCESS) {
continue;
}
io_object_t entry;
while ((entry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
FF_IOOBJECT_AUTO_RELEASE io_object_t entryRef = entry;
FF_CFTYPE_AUTO_RELEASE CFStringRef bsdName = (CFStringRef) IORegistryEntryCreateCFProperty(entryRef, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kNilOptions);
if (!bsdName) {
continue;
}
char ifName[IFNAMSIZ];
if (CFGetTypeID(bsdName) != CFStringGetTypeID() || !CFStringGetCString(bsdName, ifName, sizeof(ifName), kCFStringEncodingUTF8)) {
continue;
}
FFLocalIpResult* item = findRedacted(results, ifName);
if (item && getMacFromRegistry(entryRef, &item->mac)) {
FF_DEBUG("Replaced the redacted MAC address for interface %s", ifName);
}
}
}
FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
if (!ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) {
continue;
}
FF_IOOBJECT_AUTO_RELEASE io_object_t service = IOServiceGetMatchingService(MACH_PORT_NULL, IOBSDNameMatching(MACH_PORT_NULL, 0, item->name.chars));
if (service && getMacFromRegistry(service, &item->mac)) {
FF_DEBUG("Replaced the redacted MAC address for interface %s", item->name.chars);
} else {
FF_DEBUG("No IOMACAddress in the IORegistry for interface %s", item->name.chars);
}
}
}
+6 -5
View File
@@ -4,10 +4,16 @@
#include "common/strutil.h" #include "common/strutil.h"
#include "common/debug.h" #include "common/debug.h"
#include <string.h>
#include <ctype.h>
#include <net/if.h>
#include <ifaddrs.h> #include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <stdio.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <inttypes.h> #include <inttypes.h>
#include <fcntl.h>
#ifdef __linux__ #ifdef __linux__
#include <linux/ethtool.h> #include <linux/ethtool.h>
@@ -529,11 +535,6 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) {
#endif #endif
} }
#if defined(__APPLE__)
void ffLocalIpFixRedactedMacs(FFlist* results);
ffLocalIpFixRedactedMacs(results);
#endif
FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) { FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) {
ffListDestroy(&adapter->ipv4); ffListDestroy(&adapter->ipv4);
ffListDestroy(&adapter->ipv6); ffListDestroy(&adapter->ipv6);
+13 -69
View File
@@ -1,9 +1,7 @@
#include "media.h" #include "media.h"
#include "common/android/api.h" #include "common/android/api.h"
#include "common/android/applabel.h"
#include "common/android/binder.h" #include "common/android/binder.h"
#include "common/android/dex.h" #include "common/android/dex.h"
#include "common/android/package.h"
#include "common/debug.h" #include "common/debug.h"
#include "common/io.h" #include "common/io.h"
#include "common/mallocHelper.h" #include "common/mallocHelper.h"
@@ -33,9 +31,8 @@
// service and reaches onTransact, and is then answered with a SecurityException carrying the server's // service and reaches onTransact, and is then answered with a SecurityException carrying the server's
// own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell // own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell
// UID gets the session list from the same transaction code. Nothing in this file tests the uid: the // UID gets the session list from the same transaction code. Nothing in this file tests the uid: the
// service is asked and its answer is believed, so a device whose policy is looser works without a // service is asked and its answer is believed, so a device whose policy is looser, or one where a
// change here. What the answer does depend on is the ComponentName in the request, which is not a // notification listener is registered, works without a change here.
// filter and not decoration either -- see the request build in androidDetectMediaSession().
// //
// Only one of the two routes is ever asked, and which one it is depends on the display server. The // Only one of the two routes is ever asked, and which one it is depends on the display server. The
// binder route is Android's, and the session it reads is the one the phone is playing only while // binder route is Android's, and the session it reads is the one the phone is playing only while
@@ -97,21 +94,8 @@
// strings; a reply that did not fit would be reported rather than misread. // strings; a reply that did not fit would be reported rather than misread.
#define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024) #define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024)
// ffAndroidGetOwnPackage() writes at most this many bytes, terminator included. // An interface token plus, for getSessions, two int32s.
#define FF_MEDIA_ANDROID_PACKAGE_SIZE 128 #define FF_MEDIA_ANDROID_REQUEST_SIZE 128
// The request is the interface token, the ComponentName and the user id. A ComponentName is a
// presence marker in front of the package name and the class name, so the two string16s are what
// took this past the 128 it used to be. That size had room for a seven-character package name --
// `com.termux` with an empty class is already 136 bytes -- so the size is derived from the longest
// name that can be written rather than rounded up by hand, because a request that does not fit is
// dropped and reported as truncated: that fails the whole detection instead of degrading to a call
// without a name. The empty class name costs a 4-byte length, a NUL terminator and the padding, so 8.
// clang-format off
#define FF_MEDIA_ANDROID_REQUEST_SIZE \
(12 + ((4 + 2 * sizeof(FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR) + 3) & ~3u) \
+ 4 + ((4 + 2 * FF_MEDIA_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8u + 4u)
// clang-format on
// Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a // Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a
// larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2% // larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2%
@@ -810,18 +794,12 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t tr
return; return;
} }
// `player` wants the name the app shows rather than the name it is installed under, and the // `player` wants the name the app shows, and there is no way to get one from here: `cmd package
// package name is only the fallback. The label comes out of the APK's own resource table, which // dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and
// the package manager points at with `sourceDir` and `labelRes`: the table is *stored* rather // resolving it would mean reading the app's resource table. The package name is what the
// than deflated -- the platform expects to mmap it -- so resolving an id out of it needs no // notification and the settings screen fall back to as well, so both fields get it -- the same
// decompressor. What keeps the package name are the apps whose label is a literal string in // fallback media_linux.c uses when MPRIS answers without an Identity.
// their manifest instead of a resource reference, since their reply carries no id at all; that
// is also what the notification and the settings screen fall back to. See doc/app-label.md.
ffStrbufSet(&result->player, &result->playerId); ffStrbufSet(&result->player, &result->playerId);
const char* labelError = ffAndroidGetAppLabel(result->playerId.chars, &result->player);
if (labelError != nullptr) {
FF_DEBUG("The label of \"%s\" could not be resolved: %s", result->playerId.chars, labelError);
}
} }
static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) { static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) {
@@ -881,8 +859,7 @@ static void androidReadSession(FFBinder* binder, uint32_t token, const FFMediaAn
// is the service's decision to make and it is not the only way in -- a notification listener is let // is the service's decision to make and it is not the only way in -- a notification listener is let
// through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the // through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the
// answer. That costs one transaction, and it keeps the module working wherever the check happens to // answer. That costs one transaction, and it keeps the module working wherever the check happens to
// pass, instead of hard-coding today's policy into the client. What the request has to carry for the // pass, instead of hard-coding today's policy into the client.
// check to be consulted at all is the ComponentName: see the request build below.
static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) { static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) {
// Resolved before the binder is opened, so that a jar which does not declare the methods says so // Resolved before the binder is opened, so that a jar which does not declare the methods says so
// rather than the module failing later with a message about the transport. The reader's own // rather than the module failing later with a message about the transport. The reader's own
@@ -934,38 +911,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE]; uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request)); FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR); ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR);
ffBinderParcelPutI32(&parcel, 0); // ComponentName: null asks for every session
// The ComponentName is not a filter and not decoration: it decides which branch of the
// service's permission check runs, and with a null one the branch this module needs is never
// reached. MediaSessionService.enforceMediaPermissions() admits the shell and root, a holder of
// android.permission.MEDIA_CONTENT_CONTROL, and a UID that owns an enabled notification
// listener -- but the last one is reached through isEnabledNotificationListener(compName, uid),
// which answers false for a null compName by construction. A null therefore asks for every
// session only on behalf of a UID that was going to be admitted anyway.
//
// Measured on the test device, one binary run by three UIDs: the shell gets the session list
// for every shape below; the app UID that owns an enabled listener gets -1 EX_SECURITY for a
// null ComponentName and the session list for a non-null one; an app UID that owns none gets
// -1 EX_SECURITY for both. Two more facts
// come out of the same run: the class inside the ComponentName is never validated -- a name no
// package declares and an empty string are answered exactly like a real one -- and the package
// has to be one the calling uid owns, because the service's own enforcePackageName() rejects
// anything else with EX_ILLEGAL_ARGUMENT ("packageName does not belong to the calling uid")
// before the permission check is reached.
//
// So the caller declares its own package and the class is left empty rather than invented:
// this is a standalone binary with no component of its own to name, and naming one the device
// does not have would be a claim that cannot be backed. A build whose package cannot be
// derived sends the null request, which is what this did before.
char package[FF_MEDIA_ANDROID_PACKAGE_SIZE];
if (ffAndroidGetOwnPackage(package, sizeof(package))) {
ffBinderParcelPutI32(&parcel, 1);
ffBinderParcelPutString16(&parcel, package);
ffBinderParcelPutString16(&parcel, "");
} else {
FF_DEBUG("Cannot determine the package of this process, so the request declares no component");
ffBinderParcelPutI32(&parcel, 0);
}
ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
@@ -981,13 +927,11 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
if (exception != 0) { if (exception != 0) {
// -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The // -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The
// number goes to the debug log rather than into the message: what tells the two cases apart // number goes to the debug log rather than into the message: what tells the two cases apart
// for the user is which UID is asking. On an app UID the missing privilege is named, // for the user is which UID is asking.
// because it is one the user can grant -- the request below already declares the caller,
// so whatever is left is the listener the UID does not own.
FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : ""); FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : "");
return ffAndroidIsRootOrShell(instance.state.platform.uid) return ffAndroidIsRootOrShell(instance.state.platform.uid)
? "The media session service refused the request" ? "The media session service refused the request"
: "Reading the media session from an app UID needs an enabled notification listener, which this UID does not have"; : "Reading the media session needs the shell UID or root: an app UID is refused by the media_session service";
} }
// The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order // The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order
+99 -2
View File
@@ -110,9 +110,106 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
return error; return error;
} }
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { #if !FF_MODULE_DISABLE_MEDIA
const char* error = getMediaByMediaRemote(media, saveCover); // Called from the Perl helper process through `DynaLoader::dl_install_xsub`. That mechanism installs a
// plain C function as an XSUB, so Perl calls it with the `CV*` in the first parameter slot and no Perl
// argument ever reaches a parameter — a `bool saveCover` parameter read as a pointer and was always
// truthy. The flag has to travel in the environment instead; see `getMediaByAuthorizedProcess`.
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(void) {
const char* saveCoverEnv = getenv("FF_MEDIA_REMOTE_SAVE_COVER");
bool saveCover = saveCoverEnv && saveCoverEnv[0] == '1';
FFMediaResult media = {
.status = ffStrbufCreate(),
.song = ffStrbufCreate(),
.artist = ffStrbufCreate(),
.album = ffStrbufCreate(),
.playerId = ffStrbufCreate(),
.player = ffStrbufCreate(),
.cover = ffStrbufCreate(),
};
if (getMediaByMediaRemote(&media, saveCover) != nullptr) {
return 1;
}
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.song);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.artist);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.album);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.playerId);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.player);
ffStrbufAppendC(&media.status, '\n');
if (saveCover) {
ffStrbufAppend(&media.status, &media.cover);
}
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppendUInt(&media.status, media.position);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppendUInt(&media.status, media.length);
write(STDOUT_FILENO, media.status.chars, media.status.length);
ffStrbufDestroy(&media.status);
ffStrbufDestroy(&media.song);
ffStrbufDestroy(&media.artist);
ffStrbufDestroy(&media.album);
ffStrbufDestroy(&media.playerId);
ffStrbufDestroy(&media.player);
ffStrbufDestroy(&media.cover);
return 0;
}
#endif
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
// #1737
// `DynaLoader::dl_install_xsub` hands the C function the `CV*` as its first parameter, so the flag
// cannot be passed as a Perl argument — `ffPrintMediaByMediaRemote` reads it from the environment,
// which the child process inherits.
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("$ENV{'FF_MEDIA_REMOTE_SAVE_COVER'}='%c';\
require DynaLoader;\
my $so = DynaLoader::dl_load_file('%s', 0);\
my $sym = DynaLoader::dl_find_symbol($so, 'ffPrintMediaByMediaRemote');\
DynaLoader::dl_install_xsub('fn', $sym, __FILE__);\
exit(fn())",
saveCover ? '1' : '0',
instance.state.platform.exePath.chars);
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
const char* error = ffProcessAppendStdOut(
&buffer,
(char* const[]) { "/usr/bin/perl", // Must be signed by Apple. Homebrew version doesn't work
"-e",
script.chars,
nil });
if (error) {
return error;
}
if (buffer.length == 0) {
return "No media found";
}
// status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS
FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
char* line = nullptr;
size_t len = 0;
for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
ffStrbufSetS(strList[i], line);
}
uint32_t* const numList[] = { &result->position, &result->length };
for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
*numList[i] = (uint32_t) strtoul(line, nullptr, 10);
}
return nullptr;
}
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
const char* error;
if (@available(macOS 15.4, *)) {
error = getMediaByAuthorizedProcess(media, saveCover);
} else {
error = getMediaByMediaRemote(media, saveCover);
}
if (error) { if (error) {
ffStrbufAppendS(&media->error, error); ffStrbufAppendS(&media->error, error);
} else { } else {
+2 -5
View File
@@ -6,11 +6,8 @@
#include <string.h> #include <string.h>
#if __ANDROID__ && !defined(FF_HAVE_EGL) #if __ANDROID__ && !defined(FF_HAVE_EGL)
// On Android the EGL headers ship with the NDK, but the desktop GL ones only come from mesa-dev, // On Android, installing OpenGL headers is enough (mesa-dev)
// which a plain NDK build does not have. The block below includes <GL/gl.h>, so that is the #if __has_include(<EGL/egl.h>)
// header to probe: probing <EGL/egl.h> turns this on for every NDK build and then fails to
// compile. Termux installs mesa-dev, so it still gets the EGL path.
#if __has_include(<EGL/egl.h>) && __has_include(<GL/gl.h>)
#define FF_HAVE_EGL 1 #define FF_HAVE_EGL 1
#endif #endif
#endif #endif
-5
View File
@@ -108,11 +108,6 @@ uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) {
struct linux_dirent64* entry = (struct linux_dirent64*) bytes; struct linux_dirent64* entry = (struct linux_dirent64*) bytes;
while (remaining >= nameOffset + 1) { while (remaining >= nameOffset + 1) {
if (entry->d_reclen < nameOffset + 1 || entry->d_reclen > remaining) {
remaining = 0;
break;
}
bool ok = false; bool ok = false;
if (entry->d_name[0] != '.') { if (entry->d_name[0] != '.') {
if (__builtin_expect(entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK, true)) { if (__builtin_expect(entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK, true)) {
-8
View File
@@ -46,14 +46,6 @@ void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options)
result->macports = getMacPortsPackages(&baseDir); result->macports = getMacPortsPackages(&baseDir);
} }
if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) {
const char* prefix = getenv("PKG_DBDIR");
if (ffStrSet(prefix)) {
result->pkgsrc = ffPackagesGetNumElements(prefix, true);
} else {
result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true);
}
}
if (FF_PACKAGES_IS_ENABLED(options, NIX)) { if (FF_PACKAGES_IS_ENABLED(options, NIX)) {
ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT);
result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default"); result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default");
+6 -19
View File
@@ -104,11 +104,11 @@ typedef enum FFSoundAndroidUsage : int32_t {
#define FF_SOUND_ANDROID_NAME_REPLY_SIZE 512 #define FF_SOUND_ANDROID_NAME_REPLY_SIZE 512
// `AudioDeviceInfo.TYPE_*`, as the test device's own dex declares them (read out of // `AudioDeviceInfo.TYPE_*`, as the test device's own dex declares them (read out of
// `Landroid/media/AudioDeviceInfo;` -- most recently on Android 17, which had added four). The // `Landroid/media/AudioDeviceInfo;`). The value is what `AudioDeviceAttributes.type` carries in the
// value is what `AudioDeviceAttributes.type` carries in the reply; the names are the AOSP constant // reply; the names are the AOSP constant names without the prefix, which is what goes into
// names without the prefix, which is what goes into `identifier`, and a spelled out name, which is // `identifier`, and a spelled out name, which is what the module prints. An unrecognised value is
// what the module prints. An unrecognised value is reported rather than guessed at: a vendor is // reported rather than guessed at: a vendor is free to add types, and a wrong name is worse than
// free to add types, and a wrong name is worse than "Unknown Device". // "Unknown Device".
typedef enum FFSoundAndroidDeviceType : int32_t { typedef enum FFSoundAndroidDeviceType : int32_t {
FF_SOUND_ANDROID_TYPE_UNKNOWN = 0, FF_SOUND_ANDROID_TYPE_UNKNOWN = 0,
FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE = 1, FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE = 1,
@@ -143,11 +143,6 @@ typedef enum FFSoundAndroidDeviceType : int32_t {
FF_SOUND_ANDROID_TYPE_BLE_BROADCAST = 30, FF_SOUND_ANDROID_TYPE_BLE_BROADCAST = 30,
FF_SOUND_ANDROID_TYPE_DOCK_ANALOG = 31, FF_SOUND_ANDROID_TYPE_DOCK_ANALOG = 31,
FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP = 32, FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP = 32,
FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID = 33,
FF_SOUND_ANDROID_TYPE_BLE_CENTRAL = 34,
FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST = 35,
// 36 to 79 have no constant in the Android 17 release, and 80 is the highest it declares.
FF_SOUND_ANDROID_TYPE_PROXY = 80,
} FFSoundAndroidDeviceType; } FFSoundAndroidDeviceType;
typedef struct FFSoundAndroidDeviceName { typedef struct FFSoundAndroidDeviceName {
@@ -190,10 +185,6 @@ static const FFSoundAndroidDeviceName FF_SOUND_ANDROID_DEVICE_NAMES[] = {
{ FF_SOUND_ANDROID_TYPE_BLE_BROADCAST, "BLE_BROADCAST", "Bluetooth LE Broadcast" }, { FF_SOUND_ANDROID_TYPE_BLE_BROADCAST, "BLE_BROADCAST", "Bluetooth LE Broadcast" },
{ FF_SOUND_ANDROID_TYPE_DOCK_ANALOG, "DOCK_ANALOG", "Dock (Analog)" }, { FF_SOUND_ANDROID_TYPE_DOCK_ANALOG, "DOCK_ANALOG", "Dock (Analog)" },
{ FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP, "MULTICHANNEL_GROUP", "Multichannel Group" }, { FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP, "MULTICHANNEL_GROUP", "Multichannel Group" },
{ FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID, "BLE_HEARING_AID", "Bluetooth LE Hearing Aid" },
{ FF_SOUND_ANDROID_TYPE_BLE_CENTRAL, "BLE_CENTRAL", "Bluetooth LE Central" },
{ FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST, "BLE_CENTRAL_BROADCAST", "Bluetooth LE Central Broadcast" },
{ FF_SOUND_ANDROID_TYPE_PROXY, "PROXY", "Proxy" },
}; };
static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) { static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) {
@@ -208,16 +199,12 @@ static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) {
// The types that stand for a Bluetooth device. They are the ones whose own name is worth asking the // The types that stand for a Bluetooth device. They are the ones whose own name is worth asking the
// service for: everything else is described well enough by its type, and the request is one more // service for: everything else is described well enough by its type, and the request is one more
// round trip to system_server. A hearing aid is on the list because it is a Bluetooth LE link with // round trip to system_server. A hearing aid is on the list because it is a Bluetooth LE link with
// a name of its own, the same as a headset. The other three types Android 17 added stay off it: // a name of its own, the same as a headset.
// BLE_CENTRAL and BLE_CENTRAL_BROADCAST name an audio path inside this device while it operates in
// a Bluetooth peripheral mode rather than a device on the far end of a link, and PROXY is a logical
// endpoint rather than a link at all.
static bool soundAndroidIsBluetooth(int32_t type) { static bool soundAndroidIsBluetooth(int32_t type) {
switch ((FFSoundAndroidDeviceType) type) { switch ((FFSoundAndroidDeviceType) type) {
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO: case FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO:
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP: case FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP:
case FF_SOUND_ANDROID_TYPE_HEARING_AID: case FF_SOUND_ANDROID_TYPE_HEARING_AID:
case FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID:
case FF_SOUND_ANDROID_TYPE_BLE_HEADSET: case FF_SOUND_ANDROID_TYPE_BLE_HEADSET:
case FF_SOUND_ANDROID_TYPE_BLE_SPEAKER: case FF_SOUND_ANDROID_TYPE_BLE_SPEAKER:
case FF_SOUND_ANDROID_TYPE_BLE_BROADCAST: case FF_SOUND_ANDROID_TYPE_BLE_BROADCAST:
+14 -563
View File
@@ -14,17 +14,7 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
// Linux sound comes from one of two places: the sound server a desktop runs, and the hardware // PulseAudio's native protocol ("protocol-native").
// underneath it.
//
// The server is PulseAudio -- PipeWire speaks its protocol too -- and it is asked first, because
// it is the only one of the two that knows what the desktop's volume is: the mixer the card
// exposes and the slider on screen are not the same thing. When no server answers, the cards are
// read from ALSA instead; that part lives in its own section at the end of this file.
//
// ---------------------------------------------------------------------------
// the server: PulseAudio's native protocol ("protocol-native")
// ---------------------------------------------------------------------------
// //
// Instead of linking or dlopen-ing libpulse, fastfetch talks to the daemon // Instead of linking or dlopen-ing libpulse, fastfetch talks to the daemon
// directly: the daemon speaks a small length prefixed binary protocol on its // directly: the daemon speaks a small length prefixed binary protocol on its
@@ -37,9 +27,9 @@
// of control packets always uses the channel 0xFFFFFFFF with a zero offset and // of control packets always uses the channel 0xFFFFFFFF with a zero offset and
// flag word. // flag word.
// //
// The command numbers, the tag characters and the field orders below are the ones in pulseaudio's // See doc/pulseaudio-native-protocol.md for the byte level details, for the
// own sources -- pulsecore/native-common.h, pulsecore/tagstruct.{c,h} and // command numbers and for the pulseaudio source references they were derived
// pulsecore/protocol-native.c -- and the place each of them was read from is named where it is used. // from.
typedef enum FFSoundPulseCommand : uint32_t { typedef enum FFSoundPulseCommand : uint32_t {
FF_SOUND_PA_COMMAND_ERROR = 0, FF_SOUND_PA_COMMAND_ERROR = 0,
@@ -1096,520 +1086,19 @@ static const char* fetchSinkList(FFSoundPulseConn* conn, const FFSoundOptions* o
return nullptr; return nullptr;
} }
// ---------------------------------------------------------------------------
// the hardware: ALSA
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// A machine that runs no sound server still has ALSA, and this is what answers for it: the cards // Android compiles this file next to sound_android.c, which owns ffDetectSound and calls this one
// are enumerated from /proc and their volumes read from the control device of each card. // only while the display server is not SurfaceFlinger -- a Termux:X11 or Wayland session runs a real
// // desktop on top of the device, and PulseAudio is the sound of that desktop. So the entry point has
// Nothing is linked and nothing is loaded. The values are behind four ioctls of the kernel's own // to step aside there. Same arrangement as wallpaper_linux.c and terminalfont_linux.c.
// control interface, and both the structs and the request numbers are the kernel's uapi, which const char*
// every libc ships as <sound/asound.h>. The request numbers are `_IOWR`-style, so they carry the #ifdef __ANDROID__
// size of the struct they pass -- a layout that does not match the kernel's is refused as an ffDetectSoundLinux
// unknown ioctl instead of being read as garbage. #else
// ffDetectSound
// Android is left out: it has no ALSA of the kind this reads ( /dev/snd and /proc/asound are
// closed to an app's SELinux domain ) and a session that does have a desktop on top of the device
// is running a sound server, which is the branch below. A libc that does not carry the header says
// so here, and the fallback is simply not compiled in -- the same shape as the other optional
// backends in this tree.
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<sound/asound.h>)
#define FF_SOUND_ALSA 1
#endif #endif
(FFSoundOptions* options, FFlist* devices) {
#ifdef FF_SOUND_ALSA
#include <fcntl.h>
#include <stdio.h>
// <sys/ioctl.h> before <sound/asound.h>, in this order: the uapi header spells its request
// numbers with _IOR and _IOWR and does not define either of them itself.
#include <sys/ioctl.h>
#include <sound/asound.h>
// What a mixer control *is* is written down nowhere in the control interface. The kernel describes
// a control with a type (integer, boolean, ...) and an access bitmask, and that bitmask has no
// volume and no switch bit at all -- the uapi header defines READ, WRITE, VOLATILE, TLV_*, INACTIVE,
// LOCK, OWNER and USER, and nothing else. What carries it is the control's name, which a driver
// builds by appending a suffix to the name of what is being controlled:
//
// "Master Playback Volume" the output volume of the control named "Master"
// "Capture Volume" the input volume of a control that has no name of its own
//
// So the suffix is what is matched, on one condition: it has to be the whole of the name or the
// start of a word in it. Without that, "Capture Volume" would end in " Volume" and be read as the
// directionless " Volume" -- which is the volume of a card that has only one direction, and so an
// output.
//
// The set of suffixes is the one libasound's simple mixer layer carries; the entries sit one after
// another in that library's .rodata. The order below is the order they have to be tried in, the
// longest first, or "Master Playback Volume" would be taken for a " Volume".
//
// One deliberate difference from libasound: it reads "Capture Volume" as a directionless volume,
// which is why a desktop mixer shows a "Capture" slider. This module reports output devices, and a
// capture gain is not one of them -- so a card that carries nothing else has no output to report.
typedef struct FFSoundAlsaSuffix {
const char* suffix;
bool isSwitch; // ... the suffix names a switch rather than a volume
bool playback; // ... and one of the output direction
} FFSoundAlsaSuffix;
static const FFSoundAlsaSuffix FF_SOUND_ALSA_SUFFIXES[] = {
{ "Playback Volume", false, true },
{ "Playback Switch", true, true },
{ "Playback Enum", false, false },
{ "Playback Route", false, false },
{ "Capture Volume", false, false },
{ "Capture Switch", false, false },
{ "Capture Enum", false, false },
{ "Capture Route", false, false },
{ "Volume", false, true },
{ "Switch", true, true },
{ "Enum", false, false },
{ "Route", false, false },
};
// Which control is "the volume" of a card. A desktop mixer shows one slider per output and the name
// on it is one of these. A card that has none of them -- a USB interface, a virtual card -- still
// has exactly one playback volume, and there the first one the kernel enumerates is taken. This list
// exists so that the choice does not have to come out of that order, which is the order the driver
// registered its controls in and not a ranking at all.
static const char* const FF_SOUND_ALSA_MAIN_CONTROLS[] = { "Master", "Master Mono", "PCM", "Speaker", "Headphone", "Headphones", "Front" };
enum {
// Ids asked for per ELEM_LIST call. The list is walked in batches rather than copied in one go,
// so that nothing has to be sized after a number the card reports; 64 ids is 4 KiB of stack, and
// the far side of the ioctl is a table lookup either way.
FF_SOUND_ALSA_ID_BATCH = 64,
FF_SOUND_ALSA_PRIORITY_NONE = UINT32_MAX,
};
// Whether the control name is that of a playback volume or switch, and how much of it the suffix
// takes up. Both suffixes that are accepted end in the same six character tail -- "Volume" or
// "Switch" -- which is the only thing that pairs a card's volume with its mute; nothing else in the
// control interface says the two belong together.
static bool stripAlsaNameSuffix(const char* name, uint32_t length, bool* isSwitch, uint32_t* baseLength) {
for (size_t index = 0; index < ARRAY_SIZE(FF_SOUND_ALSA_SUFFIXES); index++) {
const FFSoundAlsaSuffix* suffix = &FF_SOUND_ALSA_SUFFIXES[index];
const uint32_t suffixLength = (uint32_t) strlen(suffix->suffix);
if (length < suffixLength || memcmp(name + length - suffixLength, suffix->suffix, suffixLength) != 0) {
continue;
}
// A suffix is a word of the name: either all of it, or preceded by a space. Anything else
// is a coincidence of letters -- "Capture Volume" ends in " Volume" and is not one.
if (length > suffixLength && name[length - suffixLength - 1] != ' ') {
continue;
}
if (!suffix->playback) {
return false; // a capture or a routing control is not the card's output
}
// The space that separated the base name from the suffix belongs to neither, and a name
// that is nothing but the suffix has no base name at all.
uint32_t base = length - suffixLength;
while (base > 0 && name[base - 1] == ' ') {
base--;
}
*isSwitch = suffix->isSwitch;
*baseLength = base;
return true;
}
return false;
}
// The rank of a control's base name, or FF_SOUND_ALSA_PRIORITY_NONE when the list does not know it.
static uint32_t getAlsaControlPriority(const char* name, uint32_t baseLength) {
for (size_t index = 0; index < ARRAY_SIZE(FF_SOUND_ALSA_MAIN_CONTROLS); index++) {
if (strlen(FF_SOUND_ALSA_MAIN_CONTROLS[index]) == baseLength && memcmp(name, FF_SOUND_ALSA_MAIN_CONTROLS[index], baseLength) == 0) {
return (uint32_t) index;
}
}
return FF_SOUND_ALSA_PRIORITY_NONE;
}
// Walks the controls of one card. ELEM_LIST holds no state -- every call carries the offset it wants
// and the buffer to fill -- so a second walk of the same card is simply a second one of these.
typedef struct FFSoundAlsaControls {
int fd;
uint32_t offset;
uint32_t index;
uint32_t batch;
bool done;
struct snd_ctl_elem_id ids[FF_SOUND_ALSA_ID_BATCH];
} FFSoundAlsaControls;
// The next mixer control of the card, or nullptr at the end. A list that cannot be read ends the
// walk rather than failing it: what to do about "this card says nothing" belongs to the caller, and
// everything below treats it exactly like a card that has no volume control. The other interfaces a
// card carries (PCM, timer, raw MIDI) are skipped here so that no caller has to know about them.
static const struct snd_ctl_elem_id* nextAlsaControl(FFSoundAlsaControls* controls) {
for (;;) {
if (controls->index < controls->batch) {
const struct snd_ctl_elem_id* id = &controls->ids[controls->index++];
if (id->iface != SNDRV_CTL_ELEM_IFACE_MIXER) {
continue;
}
return id;
}
if (controls->done) {
return nullptr;
}
struct snd_ctl_elem_list list = {
.offset = controls->offset,
.space = FF_SOUND_ALSA_ID_BATCH,
.pids = controls->ids,
};
if (ioctl(controls->fd, SNDRV_CTL_IOCTL_ELEM_LIST, &list) < 0) {
FF_DEBUG("Listing the controls of an ALSA card failed: %s", strerror(errno));
controls->done = true;
return nullptr;
}
controls->offset += list.used;
controls->index = 0;
controls->batch = list.used;
if (list.used == 0) {
controls->done = true;
return nullptr;
}
}
}
// A card's volume control and its name without the suffix -- which is what a desktop mixer puts on
// the slider, "Master" rather than "Master Playback Volume".
typedef struct FFSoundAlsaVolume {
struct snd_ctl_elem_id id;
char base[SNDRV_CTL_ELEM_ID_NAME_MAXLEN];
uint32_t baseLength;
} FFSoundAlsaVolume;
static bool findAlsaVolume(FFSoundAlsaControls* controls, FFSoundAlsaVolume* volume) {
uint32_t priority = FF_SOUND_ALSA_PRIORITY_NONE;
bool found = false;
const struct snd_ctl_elem_id* id = nullptr;
while ((id = nextAlsaControl(controls)) != nullptr) {
// The name is an array of SNDRV_CTL_ELEM_ID_NAME_MAXLEN bytes with the terminator inside
// it, so its length is bounded rather than taken on trust.
const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN);
bool isSwitch = false;
uint32_t baseLength = 0;
if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || isSwitch) {
continue;
}
const uint32_t candidate = getAlsaControlPriority((const char*) id->name, baseLength);
if (found && candidate >= priority) {
continue; // a control the list knows beats one it does not, and the first of a rank wins
}
volume->id = *id;
volume->baseLength = baseLength;
memcpy(volume->base, id->name, baseLength);
volume->base[baseLength] = '\0';
priority = candidate;
found = true;
}
return found;
}
// The switch that belongs to the volume control: same base name, same index, same device. It takes
// a second walk because which base name wins is only known once the first walk is over. A card with
// no switch at all leaves the caller with "not muted", which is the answer a control without a mute
// state deserves.
static bool findAlsaSwitch(FFSoundAlsaControls* controls, const FFSoundAlsaVolume* volume, struct snd_ctl_elem_id* result) {
const struct snd_ctl_elem_id* id = nullptr;
while ((id = nextAlsaControl(controls)) != nullptr) {
if (id->index != volume->id.index || id->device != volume->id.device || id->subdevice != volume->id.subdevice) {
continue;
}
const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN);
bool isSwitch = false;
uint32_t baseLength = 0;
if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || !isSwitch) {
continue;
}
if (baseLength != volume->baseLength || memcmp(id->name, volume->base, baseLength) != 0) {
continue;
}
*result = *id;
return true;
}
return false;
}
// The output of a card that has no volume control: any playback switch, the first one the kernel
// enumerates. HDMI and DP cards are typically this shape, with an "IEC958 Playback Switch" and no
// volume to go with it, so the switch is the only thing that says the card plays anything at all.
// Returns whether the card has a playback switch, so a caller can tell "no output" from "a switch".
static bool findAlsaPlaybackSwitch(FFSoundAlsaControls* controls, FFSoundAlsaVolume* output) {
const struct snd_ctl_elem_id* id = nullptr;
while ((id = nextAlsaControl(controls)) != nullptr) {
const uint32_t length = (uint32_t) strnlen((const char*) id->name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN);
bool isSwitch = false;
uint32_t baseLength = 0;
if (!stripAlsaNameSuffix((const char*) id->name, length, &isSwitch, &baseLength) || !isSwitch) {
continue;
}
output->id = *id;
output->baseLength = baseLength;
memcpy(output->base, id->name, baseLength);
output->base[baseLength] = '\0';
return true;
}
return false;
}
// One value of a control, and the range that control's own scale puts it in. A switch is a boolean
// and shares the same union; a few DSP cards use the 64 bit integer type.
typedef struct FFSoundAlsaValue {
long long value;
long long minimum;
long long maximum;
} FFSoundAlsaValue;
static bool readAlsaValue(int fd, const struct snd_ctl_elem_id* id, FFSoundAlsaValue* result) {
struct snd_ctl_elem_info info = { .id = *id };
if (ioctl(fd, SNDRV_CTL_IOCTL_ELEM_INFO, &info) < 0) {
FF_DEBUG("Reading the type of an ALSA control failed: %s", strerror(errno));
return false;
}
struct snd_ctl_elem_value value = { .id = *id };
if (ioctl(fd, SNDRV_CTL_IOCTL_ELEM_READ, &value) < 0) {
FF_DEBUG("Reading the value of an ALSA control failed: %s", strerror(errno));
return false;
}
// Only the first channel is read, the way the PulseAudio backend reports the first channel of a
// sink's volume. A card whose channels disagree is left to the mixer to sort out.
if (info.type == SNDRV_CTL_ELEM_TYPE_INTEGER64) {
result->value = value.value.integer64.value[0];
result->minimum = info.value.integer64.min;
result->maximum = info.value.integer64.max;
return true;
}
if (info.type != SNDRV_CTL_ELEM_TYPE_INTEGER && info.type != SNDRV_CTL_ELEM_TYPE_BOOLEAN) {
FF_DEBUG("An ALSA control carries the value type %u, which is not a volume", (unsigned) info.type);
return false;
}
result->value = value.value.integer.value[0];
result->minimum = info.value.integer.min;
result->maximum = info.value.integer.max;
return true;
}
// The control's own scale as a percentage, which is the number `amixer` prints as `[n%]`. A range
// that is empty or inverted says nothing at all, and is reported as unknown rather than as 0% --
// 0% is a state a control is really in, and it is the muted one.
static uint8_t getAlsaPercent(const FFSoundAlsaValue* value) {
if (value->maximum <= value->minimum) {
return FF_SOUND_VOLUME_UNKNOWN;
}
if (value->value <= value->minimum) {
return 0;
}
if (value->value >= value->maximum) {
return 100;
}
return (uint8_t) (((value->value - value->minimum) * 100 + (value->maximum - value->minimum) / 2) / (value->maximum - value->minimum));
}
// Appends the output of one card. Returns whether the card has an output to report at all, which is
// not the same as whether a device was appended: `options` filters what is kept, while the main
// device of a machine is the first card that has an output whatever the filter then does with it.
//
// A card whose control device cannot be opened is reported without a volume rather than dropped.
// The devices are root:audio behind an ACL for the seat, so a session over ssh can name a card it
// may not read, and the name comes from /proc/asound/cards, which is world readable.
//
// The reported name is the card's own name from /proc/asound/cards followed by the control's base
// name -- "HDA Intel PCH Master" rather than a bare "Master" -- so the list says which card each
// volume belongs to. A card that *can* be read and has no playback control at all is dropped
// instead, because that is a card with nothing to play through. An HDMI or DP card often has no
// volume control but does have an "IEC958 Playback Switch"; the switch alone is still an output, so
// such a card is reported, with no percentage to go with it.
static bool appendAlsaCard(uint32_t card, const FFstrbuf* cardName, bool main, const FFSoundOptions* options, FFlist* devices) {
// "/dev/snd/controlC", at most ten digits of the card index, and the terminator.
char path[sizeof("/dev/snd/controlC") + 11];
snprintf(path, sizeof(path), "/dev/snd/controlC%u", card);
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateCopy(cardName);
// A card whose control device cannot be opened is not a card without an output, so it still
// gets a device -- without a percentage.
uint8_t volume = FF_SOUND_VOLUME_UNKNOWN;
bool found = false;
bool active = true;
const int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
// Only read access is asked for: a volume is being read, never written, and read permission
// is the smaller of the two things to need.
FF_DEBUG("Opening \"%s\" failed: %s", path, strerror(errno));
found = true;
} else {
FFSoundAlsaControls controls = { .fd = fd };
FFSoundAlsaVolume control = {};
if (findAlsaVolume(&controls, &control)) {
found = true;
// A control whose name is nothing but the suffix -- a codec that has one volume and no
// name for it registers "Playback Volume" -- leaves the card's own name in place, which
// is the only thing left to call the device.
if (control.baseLength > 0) {
ffStrbufAppendC(&name, ' ');
ffStrbufAppendNS(&name, control.baseLength, control.base);
}
FFSoundAlsaValue value = {};
if (readAlsaValue(fd, &control.id, &value)) {
volume = getAlsaPercent(&value);
}
FFSoundAlsaControls switches = { .fd = fd };
struct snd_ctl_elem_id switchId = {};
FFSoundAlsaValue switchValue = {};
if (findAlsaSwitch(&switches, &control, &switchId) && readAlsaValue(fd, &switchId, &switchValue)) {
active = switchValue.value != 0;
if (!active) {
// A muted control is reported as 0% rather than as the value it still holds,
// which is what the PulseAudio backend does with a muted sink.
volume = 0;
}
}
} else {
// No volume control: an HDMI or DP card is usually this shape, its only output control
// an "IEC958 Playback Switch". The switch alone is still an output to play through, so
// the card is reported -- without a percentage, because there is no volume to read.
FFSoundAlsaControls switches = { .fd = fd };
FFSoundAlsaVolume switchControl = {};
if (findAlsaPlaybackSwitch(&switches, &switchControl)) {
found = true;
if (switchControl.baseLength > 0) {
ffStrbufAppendC(&name, ' ');
ffStrbufAppendNS(&name, switchControl.baseLength, switchControl.base);
}
FFSoundAlsaValue switchValue = {};
if (readAlsaValue(fd, &switchControl.id, &switchValue)) {
active = switchValue.value != 0;
if (!active) {
volume = 0;
}
}
}
}
close(fd);
}
if (!found) {
FF_DEBUG("Card %u has no playback control", card);
return false;
}
const FFSoundType type = (FFSoundType) ((main ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | (active ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE));
if ((options->soundType & FF_SOUND_TYPE_MAIN) && !(type & FF_SOUND_TYPE_MAIN)) {
return true;
}
if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !(type & FF_SOUND_TYPE_ACTIVE)) {
return true;
}
FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices);
// "hw:0" is how the card is named everywhere else -- `amixer -c 0`, and the name after a colon
// in every ALSA configuration -- so it is what identifies the device here.
ffStrbufInitF(&device->identifier, "hw:%u", card);
ffStrbufInitMove(&device->name, &name);
ffStrbufInitStatic(&device->platformApi, "ALSA");
device->volume = volume;
device->type = type;
FF_DEBUG("Reporting \"%s\" (hw:%u) at %u%%%s", device->name.chars, card, device->volume, active ? "" : ", muted");
return true;
}
static const char* detectAlsa(FFSoundOptions* options, FFlist* devices) {
FF_STRBUF_AUTO_DESTROY cards = ffStrbufCreate();
if (!ffReadFileBuffer("/proc/asound/cards", &cards)) {
FF_DEBUG("Reading \"/proc/asound/cards\" failed: %s", strerror(errno));
return "Failed to read the ALSA card list";
}
bool main = true; // the machine's main output is the first card that has one
bool output = false;
for (const char* line = cards.chars; *line;) {
const char* newline = strchr(line, '\n');
const uint32_t length = newline ? (uint32_t) (newline - line) : (uint32_t) strlen(line);
FF_STRBUF_AUTO_DESTROY record = ffStrbufCreateNS(length, line);
line = newline ? newline + 1 : line + length;
// 0 [PCH ]: HDA-Intel - HDA Intel PCH
// HDA Intel PCH at 0xdf240000 irq 125
//
// One line per card, with the index in the first column, and the card's long name on the
// line below -- which does not start with a digit, and is what this skips. The name that is
// reported is the one behind the dash: the driver's name for the card, without the
// "HDA-Intel - " in front of it.
char* indexEnd = nullptr;
const long number = strtol(record.chars, &indexEnd, 10);
if (indexEnd == record.chars || number < 0) {
continue;
}
const char* close = strchr(indexEnd, ']');
const char* dash = close ? strstr(close, " - ") : nullptr;
if (dash == nullptr) {
FF_DEBUG("The ALSA card list line \"%s\" is not in the expected form", record.chars);
continue;
}
FF_STRBUF_AUTO_DESTROY cardName = ffStrbufCreateS(dash + strlen(" - "));
ffStrbufTrimRightSpace(&cardName);
if (appendAlsaCard((uint32_t) number, &cardName, main, options, devices)) {
main = false;
output = true;
}
}
if (!output) {
return "No ALSA card with a playback control was found";
}
return nullptr;
}
#endif // FF_SOUND_ALSA
// ---------------------------------------------------------------------------
// the server
// ---------------------------------------------------------------------------
static const char* detectPulseAudio(FFSoundOptions* options, FFlist* devices) {
FF_STRBUF_AUTO_DESTROY defaultSocketPath = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY defaultSocketPath = ffStrbufCreate();
getDefaultSocketPath(&defaultSocketPath); getDefaultSocketPath(&defaultSocketPath);
FF_DEBUG("Default pulseaudio socket path: %s", defaultSocketPath.chars); FF_DEBUG("Default pulseaudio socket path: %s", defaultSocketPath.chars);
@@ -1687,41 +1176,3 @@ static const char* detectPulseAudio(FFSoundOptions* options, FFlist* devices) {
return nullptr; return nullptr;
} }
// ---------------------------------------------------------------------------
// Android compiles this file next to sound_android.c, which owns ffDetectSound and calls this one
// only while the display server is not SurfaceFlinger -- a Termux:X11 or Wayland session runs a real
// desktop on top of the device, and PulseAudio is the sound of that desktop. So the entry point has
// to step aside there. Same arrangement as wallpaper_linux.c and terminalfont_linux.c.
const char*
#ifdef __ANDROID__
ffDetectSoundLinux
#else
ffDetectSound
#endif
(FFSoundOptions* options, FFlist* devices) {
const char* error = detectPulseAudio(options, devices);
if (error == nullptr) {
return nullptr;
}
#ifdef FF_SOUND_ALSA
// No server answered. A machine that runs none still has the hardware, and that is what is
// asked here -- but only ever after the server, never instead of it: on a machine that runs
// one, the server's answer is the desktop's, and the mixer behind it is not the same number.
FF_DEBUG("PulseAudio is not answering (%s), so the ALSA cards are read instead", error);
const uint32_t detected = devices->length;
const char* alsaError = detectAlsa(options, devices);
if (alsaError == nullptr || devices->length > detected) {
// ALSA answered. Its list may still be empty after the filter, which is the same answer
// the server path gives when nothing matches, and not a failure.
return nullptr;
}
FF_DEBUG("Reading the ALSA cards failed as well: %s", alsaError);
#endif
return error;
}
+1 -1
View File
@@ -57,7 +57,7 @@ static void enumerate_props(FFSoundDeviceBundle* bundle, struct sioctl_desc* des
} }
} }
const char* ffDetectSound([[maybe_unused]] FFSoundOptions* options, FFlist* devices) { const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) {
[[gnu::cleanup(close_hdl)]] struct sioctl_hdl* hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0); [[gnu::cleanup(close_hdl)]] struct sioctl_hdl* hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0);
if (!hdl) { if (!hdl) {
FF_DEBUG("sio_open() failed"); FF_DEBUG("sio_open() failed");
@@ -1,16 +1,11 @@
#include "terminalshell.h" #include "terminalshell.h"
#include "common/FFcache.h" #include "common/FFcache.h"
#include "common/debug.h"
#include "common/io.h" #include "common/io.h"
#include "common/parsing.h" #include "common/parsing.h"
#include "common/processing.h" #include "common/processing.h"
#include "common/thread.h" #include "common/thread.h"
#include "common/strutil.h" #include "common/strutil.h"
#ifdef __ANDROID__
#include "common/android/applabel.h"
#endif
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
@@ -329,18 +324,8 @@ static void setTerminalInfoDetails(FFTerminalResult* result) {
#if defined(__ANDROID__) #if defined(__ANDROID__)
else if (ffStrbufEqualS(&result->processName, "com.termux")) { else if (ffStrbufEqualS(&result->processName, "com.termux"))
// The app is installed under a label rather than under its package name, and the label is
// what every screen the user sees calls it -- "Termux:Launcher" on the test device, not
// "Termux". The package name is the fallback for a build whose label cannot be read, which
// includes an app whose label is a literal string in its manifest rather than a resource
// reference; see common/android/applabel.h for what that read costs and when it fails.
ffStrbufInitStatic(&result->prettyName, "Termux"); ffStrbufInitStatic(&result->prettyName, "Termux");
const char* labelError = ffAndroidGetAppLabel("com.termux", &result->prettyName);
if (labelError != nullptr) {
FF_DEBUG("The label of com.termux could not be resolved: %s", labelError);
}
}
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) #elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__)
+11 -29
View File
@@ -1,37 +1,19 @@
#include "uptime.h" #include "uptime.h"
#include "common/debug.h"
#include "common/time.h" #include "common/time.h"
#include <kstat.h> #include <utmpx.h>
static inline void kstatFreeWrap(kstat_ctl_t** pkc) {
assert(pkc);
if (*pkc) {
kstat_close(*pkc);
}
}
const char* ffDetectUptime(FFUptimeResult* result) { const char* ffDetectUptime(FFUptimeResult* result) {
[[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); struct utmpx* ut;
if (!kc) {
FF_DEBUG("kstat_open() failed"); setutxent();
return "kstat_open() failed"; while (nullptr != (ut = getutxent())) {
if (ut->ut_type == BOOT_TIME) {
result->bootTime = (uint64_t) ut->ut_tv.tv_sec * 1000 + (uint64_t) ut->ut_tv.tv_usec / 1000000;
result->uptime = ffTimeGetNow() - result->bootTime;
break;
}
} }
endutxent();
kstat_t* ks = kstat_lookup(kc, "unix", 0, "system_misc");
if (!ks || kstat_read(kc, ks, nullptr) < 0) {
FF_DEBUG("kstat_lookup(unix:0:system_misc) failed");
return "kstat_lookup(unix:0:system_misc) failed";
}
kstat_named_t* bootTime = (kstat_named_t*) kstat_data_lookup(ks, "boot_time");
if (!bootTime) {
FF_DEBUG("kstat_data_lookup(boot_time) failed");
return "kstat_data_lookup(boot_time) failed";
}
result->bootTime = (uint64_t) bootTime->value.ui32 * 1000;
result->uptime = ffTimeGetNow() - result->bootTime;
return nullptr; return nullptr;
} }
+25 -30
View File
@@ -109,18 +109,17 @@
// more accurate -- pays nothing for it. // more accurate -- pays nothing for it.
// //
// Three things about the call are positional rather than negotiated, and all three were measured // Three things about the call are positional rather than negotiated, and all three were measured
// across the Android 11, 13, 16 and 17 devices: // on an Android 16 device and an Android 11 device:
// //
// * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on // * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on
// Android 11, 36 on Android 13 and 41 on Android 16 and 17) -- see common/android/dex.h. // 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 // * The reply layout drifts, and not only by release. Android 12 dropped the duplicate length word
// that Android 11 and 10 wrote in front of the SSID octets, and the Android 17 device writes a // that Android 11 and 10 wrote in front of the SSID octets, and the Android 16 device fills an
// word of its own between the link speed and the transmit speed that the Android 11 and 13 // int between the transmit and the receive speed that the Android 11 device leaves out -- while
// devices leave out -- a vendor addition rather than a release difference, since AOSP's own // AOSP's own Android 16 writes the head exactly the Android 11 way, so that one int is a vendor
// Android 16 writes the head the Android 11 way. Frequency is the only channel centre in the // addition rather than a release difference. Frequency is the only channel centre in the head, so
// head, so it is located by value, with the whole result validated before it is used, and the // it is located by value, with the whole result validated before it is used. A wrong guess shows
// two direction specific speeds are read as its neighbours. A wrong guess shows up as a // up as a rejected layout, not as a plausible wrong number.
// 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 // * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every
// vendor: the Android 11 device carries three words there that AOSP's own Android 11 // vendor: the Android 11 device carries three words there that AOSP's own Android 11
// does not. It is located by shape for that reason. // does not. It is located by shape for that reason.
@@ -172,14 +171,13 @@ typedef enum FFWifiAndroidRadioState : int32_t
} FFWifiAndroidRadioState; } FFWifiAndroidRadioState;
// An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker; // 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 are the first three // WifiInfo itself then starts at 8. The network id, the RSSI and the link speed have held those
// fields `writeToParcel` writes, so nothing a release or a vendor adds behind them can move them -- // offsets from Android 9 to 16, and the transmit speed joined them in Android 10 without moving
// they have held those offsets from Android 9 to 17 and are read there. The two direction specific // since, so all four are read at fixed offsets. Everything after them is located by shape.
// speeds sit behind them but are located from the frequency instead, because a vendor does write a
// word of its own in front of that pair: see the note at FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED.
#define FF_WIFI_ANDROID_OFF_NET_ID 8 #define FF_WIFI_ANDROID_OFF_NET_ID 8
#define FF_WIFI_ANDROID_OFF_RSSI 12 #define FF_WIFI_ANDROID_OFF_RSSI 12
#define FF_WIFI_ANDROID_OFF_LINK_SPEED 16 #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 // 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 // always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot
@@ -208,15 +206,11 @@ typedef enum FFWifiAndroidRadioState : int32_t
#define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08 #define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08
#define FF_WIFI_ANDROID_SSID_LENGTH 0x04 #define FF_WIFI_ANDROID_SSID_LENGTH 0x04
// The transmit speed and the receive speed are the two words in front of Frequency, in that order: // The receive speed is the word in front of Frequency; the transmit speed is the word after the link
// `mTxLinkSpeed, mRxLinkSpeed, mFrequency` are written back to back by every head this file has // speed. The Android 16 device writes one extra word between those two that AOSP does not -- it repeats the
// been checked against, from Android 11 through 17. The pair is read from Frequency rather than // transmit speed -- so the receive speed can only be reached from Frequency, and Frequency cannot be
// stepped to from the link speed, because the Android 17 device writes a word of its own between // reached by stepping over the two link speeds. Both were checked against `cmd wifi status` at the
// the link speed and the transmit speed: its dex names that word `mCurrentLinkSpeed`, so the word // same moment: 131/219 on the Android 16 device, 86/-1 on the Android 11 device.
// after the link speed is not the transmit speed there and the pair sits one word further along.
// Both were checked against `cmd wifi status` at the same moment: 131/219 on the Android 16 device,
// 86/-1 on the Android 11 device.
#define FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED 0x08
#define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04 #define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04
#define FF_WIFI_ANDROID_SSID 0x08 // Android 12 and later #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_LEGACY 0x0c // Android 11 and older
@@ -649,7 +643,7 @@ static void debugDumpReply(const uint8_t* data, size_t size) {
#endif #endif
static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) { static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
if (size < FF_WIFI_ANDROID_FREQ_SCAN_FROM + 4) { if (size < FF_WIFI_ANDROID_OFF_TX_LINK_SPEED + 4) {
FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size); FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size);
return "The reply is too short to hold a WifiInfo"; return "The reply is too short to hold a WifiInfo";
} }
@@ -672,11 +666,12 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA
FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size); FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size);
return nullptr; return nullptr;
} }
FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d", FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d, tx link speed %d",
size, size,
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID), ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID),
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI), ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI),
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED)); 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 // 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 // older, not at all on Android 12 and later, which leaves the octets one word earlier. The one
@@ -760,7 +755,7 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA
connection->frequency = frequency; connection->frequency = frequency;
connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI); connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED); connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
connection->txLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_TX_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); connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED);
findStrings(data, size, stringsFrom, connection); findStrings(data, size, stringsFrom, connection);
@@ -782,9 +777,9 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA
if (!findSecurityType(data, size, connection)) { if (!findSecurityType(data, size, connection)) {
FF_DEBUG("No security type could be located in this reply, so the field stays empty"); FF_DEBUG("No security type could be located in this reply, so the field stays empty");
} }
FF_DEBUG("Standard %d (%s), tx link speed %d, rx link speed %d, connected %s", FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s",
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->txLinkSpeed, connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed,
connection->rxLinkSpeed, connection->connected ? "yes" : "no"); connection->connected ? "yes" : "no");
return nullptr; return nullptr;
} }
+4 -4
View File
@@ -52,15 +52,15 @@ const char* ffDetectWifi(FFlist* result) {
if (inf.ssid) { // https://developer.apple.com/forums/thread/732431 if (inf.ssid) { // https://developer.apple.com/forums/thread/732431
ffStrbufAppendS(&item->conn.ssid, inf.ssid.UTF8String); ffStrbufAppendS(&item->conn.ssid, inf.ssid.UTF8String);
} else if (networkProfile.ssid) { // Dead in macOS 27.2 } else if (networkProfile.ssid) {
ffStrbufSetStatic(&item->conn.ssid, networkProfile.ssid.UTF8String); ffStrbufSetStatic(&item->conn.ssid, inf.configuration.networkProfiles.firstObject.ssid.UTF8String);
} else { } else {
ffStrbufSetStatic(&item->conn.ssid, "<redacted>"); ffStrbufSetStatic(&item->conn.ssid, "<redacted>"); // https://developer.apple.com/forums/thread/732431
} }
if (inf.bssid) { if (inf.bssid) {
ffStrbufAppendS(&item->conn.bssid, inf.bssid.UTF8String); ffStrbufAppendS(&item->conn.bssid, inf.bssid.UTF8String);
} else if (networkProfile.bssidList) { // Dead in macOS 27.2 } else if (networkProfile.bssidList) {
ffStrbufSetStatic(&item->conn.bssid, [networkProfile.bssidList.firstObject[@"BSSID"] UTF8String]); ffStrbufSetStatic(&item->conn.bssid, [networkProfile.bssidList.firstObject[@"BSSID"] UTF8String]);
} else { } else {
ffStrbufSetStatic(&item->conn.bssid, "<redacted>"); ffStrbufSetStatic(&item->conn.bssid, "<redacted>");
-11
View File
@@ -85,17 +85,6 @@ static const FFlogo T[] = {
}, },
}, },
#endif #endif
#ifdef FASTFETCH_DATATEXT_LOGO_TINYCORE
// TinyCore
{
.names = { "TinyCore" },
.lines = FASTFETCH_DATATEXT_LOGO_TINYCORE,
.colors = {
FF_COLOR_FG_BLUE,
FF_COLOR_FG_LIGHT_WHITE,
},
},
#endif
#ifdef FASTFETCH_DATATEXT_LOGO_TORIZONCORE #ifdef FASTFETCH_DATATEXT_LOGO_TORIZONCORE
// Torizon OS // Torizon OS
{ {
-10
View File
@@ -1,10 +0,0 @@
$1 .-~~~~~~~-.
$1 .' $2tiny$1 '.
$1 / / \
$1 ; / ;
$1 | / |
$1 | / |
$1 ; / $2micro$1 ;
$1 \ / /
$1 '. .'
$1 '-.......-'
+13 -9
View File
@@ -489,7 +489,6 @@ static bool printImageKittyDirect(bool printError) {
#include <fcntl.h> #include <fcntl.h>
#include "common/time.h" #include "common/time.h"
#include "common/debug.h"
#ifndef _WIN32 #ifndef _WIN32
#include <sys/ioctl.h> #include <sys/ioctl.h>
@@ -499,14 +498,19 @@ static bool printImageKittyDirect(bool printError) {
#endif #endif
#ifdef FF_HAVE_ZLIB #ifdef FF_HAVE_ZLIB
#include "common/zlib.h" #include <zlib.h>
static bool compressBlob(void** blob, size_t* length) { static bool compressBlob(void** blob, size_t* length) {
const char* error = ffZlibLoad(FF_ZLIB_COMPRESS); FF_LIBRARY_LOAD(zlib, false,
if (error != nullptr) { #ifdef _WIN32
FF_DEBUG("The logo stays uncompressed: %s", error); "zlib1"
return false; #else
} "libz"
#endif
FF_LIBRARY_EXTENSION,
2)
FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false)
FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false)
#if _WIN32 #if _WIN32
// zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be // zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be
@@ -516,7 +520,7 @@ static bool compressBlob(void** blob, size_t* length) {
} }
#endif #endif
uLong compressedLength = compressBound((uLong) *length); uLong compressedLength = ffcompressBound((uLong) *length);
void* compressed = malloc(compressedLength); void* compressed = malloc(compressedLength);
if (compressed == nullptr) { if (compressed == nullptr) {
return false; return false;
@@ -530,7 +534,7 @@ static bool compressBlob(void** blob, size_t* length) {
// The gap is far wider on Windows, where the same frames cost 37.7 ms each at level 9 against // 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 // 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. // slowdown there (398 ms -> 1692 ms). Re-measure both platforms before touching it.
if (compress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) { if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) {
free(compressed); free(compressed);
return false; return false;
} }
+2 -5
View File
@@ -143,13 +143,10 @@ static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bo
if (doColorReplacement && *data == '$') { if (doColorReplacement && *data == '$') {
++data; ++data;
// `$$` is an escaped dollar; a `$` ending a line or the logo is a plain one if (*data == '$' || *data == '\0') {
if (*data == '$' || *data == '\0' || *data == '\n' || *data == '\r') {
ffStrbufAppendC(&line, '$'); ffStrbufAppendC(&line, '$');
++lineWidth; ++lineWidth;
if (*data == '$') { ++data;
++data;
}
continue; continue;
} }
+1 -1
View File
@@ -221,7 +221,7 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = {
.pl = "Adapter Bluetooth", .pl = "Adapter Bluetooth",
.pt = "Adaptador Bluetooth", .pt = "Adaptador Bluetooth",
.ru = "Bluetooth-адаптер", .ru = "Bluetooth-адаптер",
.tr = "Bluetooth Adaptörü", .tr = "Bluetooth Radyosu",
.uk = "Радіомодуль Bluetooth", .uk = "Радіомодуль Bluetooth",
.vi = "Radio Bluetooth", .vi = "Radio Bluetooth",
.zh_CN = "蓝牙适配器", .zh_CN = "蓝牙适配器",
+4 -35
View File
@@ -85,24 +85,7 @@ void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) {
} }
if (unsafe_yyjson_equals_str(key, "useStdErr")) { if (unsafe_yyjson_equals_str(key, "useStdErr")) {
options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT; options->useStdErr = yyjson_get_bool(val);
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead");
continue;
}
if (unsafe_yyjson_equals_str(key, "useOutput")) {
int value;
const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
{ "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT },
{ "stderr", FF_PROCESS_OUTPUT_STDERR_BIT },
{ "both", FF_PROCESS_OUTPUT_BOTH },
{},
});
if (error) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
} else {
options->useOutput = (FFProcessOutputType) value;
}
continue; continue;
} }
@@ -127,19 +110,7 @@ void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc,
yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell); yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell);
yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param); yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param);
yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text); yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text);
switch (options->useOutput) { yyjson_mut_obj_add_bool(doc, module, "useStdErr", options->useStdErr);
case FF_PROCESS_OUTPUT_STDOUT_BIT:
yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout");
break;
case FF_PROCESS_OUTPUT_STDERR_BIT:
yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr");
break;
case FF_PROCESS_OUTPUT_BOTH:
yyjson_mut_obj_add_str(doc, module, "useOutput", "both");
break;
default:
break;
}
yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel); yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel);
yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines); yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines);
} }
@@ -191,15 +162,13 @@ void ffInitCommandOptions(FFCommandOptions* options) {
); );
ffStrbufInitStatic(&options->param, ffStrbufInitStatic(&options->param,
#ifdef _WIN32 #ifdef _WIN32
"/d/c" "/c"
#else #else
"-c" "-c"
#endif #endif
); );
ffStrbufInit(&options->text); ffStrbufInit(&options->text);
// Capture both streams by default: most tools log to stdout or stderr without saying which, and options->useStdErr = false;
// the previous single-stream default silently dropped one of them.
options->useOutput = FF_PROCESS_OUTPUT_BOTH;
options->parallel = true; options->parallel = true;
options->splitLines = false; options->splitLines = false;
} }
+1 -2
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "common/option.h" #include "common/option.h"
#include "common/processing.h" // FFProcessOutputType
typedef struct FFCommandOptions { typedef struct FFCommandOptions {
FFModuleArgs moduleArgs; FFModuleArgs moduleArgs;
@@ -9,7 +8,7 @@ typedef struct FFCommandOptions {
FFstrbuf shell; FFstrbuf shell;
FFstrbuf param; FFstrbuf param;
FFstrbuf text; FFstrbuf text;
FFProcessOutputType useOutput; bool useStdErr;
bool parallel; bool parallel;
bool splitLines; bool splitLines;
} FFCommandOptions; } FFCommandOptions;
+12 -11
View File
@@ -15,8 +15,6 @@
static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index) { static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index) {
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
uint64_t bytesUsed = disk->bytesTotal - disk->bytesFree;
if (options->moduleArgs.key.length == 0) { if (options->moduleArgs.key.length == 0) {
ffStrbufSetF(&key, "%s (%s)", FF_MODULE_GET_DISPLAY_NAME(Disk), disk->mountpoint.chars); ffStrbufSetF(&key, "%s (%s)", FF_MODULE_GET_DISPLAY_NAME(Disk), disk->mountpoint.chars);
} else { } else {
@@ -52,12 +50,12 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index
} }
FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
ffSizeAppendNum(bytesUsed, &usedPretty); ffSizeAppendNum(disk->bytesUsed, &usedPretty);
FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
ffSizeAppendNum(disk->bytesTotal, &totalPretty); ffSizeAppendNum(disk->bytesTotal, &totalPretty);
double bytesPercentage = disk->bytesTotal > 0 ? (double) bytesUsed / (double) disk->bytesTotal * 100.0 : 0; double bytesPercentage = disk->bytesTotal > 0 ? (double) disk->bytesUsed / (double) disk->bytesTotal * 100.0 : 0;
FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type; FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
if (options->moduleArgs.outputFormat.length == 0) { if (options->moduleArgs.outputFormat.length == 0) {
@@ -138,6 +136,9 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index
FF_STRBUF_AUTO_DESTROY freePretty = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY freePretty = ffStrbufCreate();
ffSizeAppendNum(disk->bytesFree, &freePretty); ffSizeAppendNum(disk->bytesFree, &freePretty);
FF_STRBUF_AUTO_DESTROY availPretty = ffStrbufCreate();
ffSizeAppendNum(disk->bytesAvailable, &availPretty);
uint64_t now = ffTimeGetNow(); uint64_t now = ffTimeGetNow();
uint64_t duration = now - disk->createTime; uint64_t duration = now - disk->createTime;
uint32_t milliseconds = (uint32_t) (duration % 1000); uint32_t milliseconds = (uint32_t) (duration % 1000);
@@ -177,8 +178,7 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index
FF_ARG(age.daysOfYear, "days-of-year"), FF_ARG(age.daysOfYear, "days-of-year"),
FF_ARG(age.yearsFraction, "years-fraction"), FF_ARG(age.yearsFraction, "years-fraction"),
FF_ARG(freePretty, "size-free"), FF_ARG(freePretty, "size-free"),
FF_ARG(bytesUsed, "bytes-used"), FF_ARG(availPretty, "size-available"),
FF_ARG(disk->bytesTotal, "bytes-total"),
})); }));
} }
} }
@@ -374,8 +374,10 @@ bool ffGenerateDiskJsonResult(FFDiskOptions* options, yyjson_mut_doc* doc, yyjso
yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr); yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
yyjson_mut_val* bytes = yyjson_mut_obj_add_obj(doc, obj, "bytes"); yyjson_mut_val* bytes = yyjson_mut_obj_add_obj(doc, obj, "bytes");
yyjson_mut_obj_add_uint(doc, bytes, "available", item->bytesAvailable);
yyjson_mut_obj_add_uint(doc, bytes, "free", item->bytesFree); yyjson_mut_obj_add_uint(doc, bytes, "free", item->bytesFree);
yyjson_mut_obj_add_uint(doc, bytes, "total", item->bytesTotal); yyjson_mut_obj_add_uint(doc, bytes, "total", item->bytesTotal);
yyjson_mut_obj_add_uint(doc, bytes, "used", item->bytesUsed);
yyjson_mut_val* files = yyjson_mut_obj_add_obj(doc, obj, "files"); yyjson_mut_val* files = yyjson_mut_obj_add_obj(doc, obj, "files");
if (item->filesTotal == 0 && item->filesUsed == 0) { if (item->filesTotal == 0 && item->filesUsed == 0) {
@@ -485,8 +487,8 @@ FFModuleBaseInfo ffDiskModuleInfo = {
.generateJsonResult = (void*) ffGenerateDiskJsonResult, .generateJsonResult = (void*) ffGenerateDiskJsonResult,
.generateJsonConfig = (void*) ffGenerateDiskJsonConfig, .generateJsonConfig = (void*) ffGenerateDiskJsonConfig,
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
{ "Size used (formatted)", "size-used" }, { "Size used", "size-used" },
{ "Size total (formatted)", "size-total" }, { "Size total", "size-total" },
{ "Size percentage num", "size-percentage" }, { "Size percentage num", "size-percentage" },
{ "Files used", "files-used" }, { "Files used", "files-used" },
{ "Files total", "files-total" }, { "Files total", "files-total" },
@@ -509,9 +511,8 @@ FFModuleBaseInfo ffDiskModuleInfo = {
{ "Years integer after creation", "years" }, { "Years integer after creation", "years" },
{ "Days of year after creation", "days-of-year" }, { "Days of year after creation", "days-of-year" },
{ "Years fraction after creation", "years-fraction" }, { "Years fraction after creation", "years-fraction" },
{ "Size free (formatted)", "size-free" }, { "Size free", "size-free" },
{ "Size used (in bytes)", "bytes-used" }, { "Size available", "size-available" },
{ "Size total (in bytes)", "bytes-total" },
})), })),
.defaultOrder = 42, .defaultOrder = 42,
}; };
+1 -1
View File
@@ -156,7 +156,7 @@ FFModuleBaseInfo ffHostModuleInfo = {
.pl = "Host", .pl = "Host",
.pt = "Host", .pt = "Host",
.ru = "Модель", .ru = "Модель",
.tr = "Cihaz", .tr = "Ana Bilgisayar",
.uk = "Хост", .uk = "Хост",
.vi = "Máy chủ", .vi = "Máy chủ",
.zh_CN = "主机", .zh_CN = "主机",
+1 -1
View File
@@ -123,7 +123,7 @@ FFModuleBaseInfo ffLMModuleInfo = {
.pl = "Menedżer logowania", .pl = "Menedżer logowania",
.pt = "Gerenciador de login", .pt = "Gerenciador de login",
.ru = "Менеджер входа", .ru = "Менеджер входа",
.tr = "Giriş Yöneticisi", .tr = "Oturum Açma Yöneticisi",
.uk = "Менеджер входу", .uk = "Менеджер входу",
.vi = "Trình quản lý đăng nhập", .vi = "Trình quản lý đăng nhập",
.zh_CN = "登录管理器", .zh_CN = "登录管理器",
+1 -1
View File
@@ -283,7 +283,7 @@ FFModuleBaseInfo ffTopModuleInfo = {
.pl = "Najaktywniejsze procesy", .pl = "Najaktywniejsze procesy",
.pt = "Processos principais", .pt = "Processos principais",
.ru = "Топ процессов", .ru = "Топ процессов",
.tr = "En Yoğun Süreçler", .tr = "En yoğun işlemler",
.uk = "Найактивніші процеси", .uk = "Найактивніші процеси",
.vi = "Tiến trình hàng đầu", .vi = "Tiến trình hàng đầu",
.zh_CN = "进程排行", .zh_CN = "进程排行",
+1 -1
View File
@@ -60,7 +60,7 @@ void ffGenerateUptimeJsonConfig(FFUptimeOptions* options, yyjson_mut_doc* doc, y
} }
bool ffGenerateUptimeJsonResult([[maybe_unused]] FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { bool ffGenerateUptimeJsonResult([[maybe_unused]] FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
FFUptimeResult result = {}; FFUptimeResult result;
const char* error = ffDetectUptime(&result); const char* error = ffDetectUptime(&result);
if (error) { if (error) {
-1
View File
@@ -1060,7 +1060,6 @@ static const char* optionGetLanguageString(uint32_t offset) {
case offsetof(FFModuleDisplayName, cs): return "cs"; case offsetof(FFModuleDisplayName, cs): return "cs";
case offsetof(FFModuleDisplayName, de): return "de"; case offsetof(FFModuleDisplayName, de): return "de";
case offsetof(FFModuleDisplayName, es): return "es"; case offsetof(FFModuleDisplayName, es): return "es";
case offsetof(FFModuleDisplayName, fi): return "fi";
case offsetof(FFModuleDisplayName, fr): return "fr"; case offsetof(FFModuleDisplayName, fr): return "fr";
case offsetof(FFModuleDisplayName, gl): return "gl"; case offsetof(FFModuleDisplayName, gl): return "gl";
case offsetof(FFModuleDisplayName, he): return "he"; case offsetof(FFModuleDisplayName, he): return "he";
-1
View File
@@ -96,7 +96,6 @@ int main(void) {
VERIFY("output({1~10,10})", "12345 67890", "output()"); VERIFY("output({1~10,10})", "12345 67890", "output()");
VERIFY("output({1~10,5})", "12345 67890", "output()"); VERIFY("output({1~10,5})", "12345 67890", "output()");
VERIFY("output({1~3,-3})", "12345 67890", "output(45 67)"); VERIFY("output({1~3,-3})", "12345 67890", "output(45 67)");
VERIFY("output({1~0,-12})", "12345 67890", "output()");
VERIFY("output({1~-5,-3})", "12345 67890", "output(67)"); VERIFY("output({1~-5,-3})", "12345 67890", "output(67)");
VERIFY("output({1~-0,10})", "12345 67890", "output(12345 6789)"); VERIFY("output({1~-0,10})", "12345 67890", "output(12345 6789)");
VERIFY("output({1~-3,-5})", "12345 67890", "output()"); VERIFY("output({1~-3,-5})", "12345 67890", "output()");
-201
View File
@@ -1,201 +0,0 @@
// Tests for ffGPUParsePciIdsFile(), the pci.ids lookup that resolves a PCI GPU name on Linux and
// the BSDs.
//
// The parser takes the file content as a buffer instead of opening the file itself, which is
// exactly what makes it testable: a small hand written pci.ids excerpt is enough, and the
// expectations stay readable.
//
// The function is only compiled where libfastfetch ships the pci.ids based detection, so on other
// platforms this test has nothing to link against and only reports success.
#include "common/textModifier.h"
#include "detection/gpu/gpu.h"
#include <stdio.h>
#include <stdlib.h>
#if FF_GPU_HAS_PCI_IDS
static void verifyPciEx(
const char* content, uint8_t subclass, uint16_t vendor, uint16_t device,
const char* presetVendor, const char* presetName,
const char* expectedVendor, const char* expectedName,
int lineNo
) {
FFGPUResult gpu = {};
ffStrbufInit(&gpu.vendor);
ffStrbufInit(&gpu.name);
// A caller may already know the vendor (for example from the PCI subsystem), in which case the
// parser must not clobber it.
if (presetVendor) {
ffStrbufSetS(&gpu.vendor, presetVendor);
}
if (presetName) {
ffStrbufSetS(&gpu.name, presetName);
}
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS(content);
ffGPUParsePciIdsFile(&buffer, subclass, vendor, device, &gpu);
if (!ffStrbufEqualS(&gpu.vendor, expectedVendor) || !ffStrbufEqualS(&gpu.name, expectedName)) {
fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffGPUParsePciIdsFile(vendor=0x%04x, device=0x%04x, subclass=%u): expected vendor=\"%s\" name=\"%s\", got vendor=\"%s\" name=\"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET,
lineNo, vendor, device, (unsigned) subclass, expectedVendor, expectedName, gpu.vendor.chars, gpu.name.chars);
exit(1);
}
ffStrbufDestroy(&gpu.vendor);
ffStrbufDestroy(&gpu.name);
}
#define VERIFY_PCI(content, subclass, vendor, device, expectedVendor, expectedName) \
verifyPciEx((content), (subclass), (vendor), (device), nullptr, nullptr, (expectedVendor), (expectedName), __LINE__)
#define VERIFY_PCI_EX(content, subclass, vendor, device, presetVendor, presetName, expectedVendor, expectedName) \
verifyPciEx((content), (subclass), (vendor), (device), (presetVendor), (presetName), (expectedVendor), (expectedName), __LINE__)
// A miniature pci.ids. Vendors are ordered, device ids grow within a block, and the block is closed
// by a blank line -- the same shape the real file has.
static const char PCI_IDS[] =
"# pci.ids excerpt used by the tests\n"
"#\n"
"1a2b Vendor Alpha\n"
"\t0001 Alpha One\n"
"\t0005 Alpha Five\n"
"\t\t0005 0abc Subsystem that must be ignored\n"
"\t00ff Alpha FF [Alias]\n"
"\t1000 Alpha Thousand\n"
"\n"
"1a2c Vendor Beta\n"
"\t0001 Beta One\n"
"\t0002 Beta Two\n"
"\t1234 Beta TwelveThirtyFour\n"
"\n"
"abcd Vendor Delta\n"
"\t0e1f Delta Device\n"
"\n"
"ffff Vendor NoDevices\n"
"\n"
"C 00 Unclassified device\n";
// The last line has no terminating newline, so the parser has to fall back to the end of the
// buffer.
static const char PCI_IDS_NO_TRAILING_NL[] =
"#\n"
"1a2b Vendor Alpha\n"
"\t0001 Alpha One";
// A vendor with no device lines at all, again without a terminating newline.
static const char PCI_IDS_VENDOR_ONLY[] =
"#\n"
"1a2b Vendor Alpha";
int main(void) {
// An empty buffer is not an error: the name is synthesized from the vendor and device id.
{
VERIFY_PCI("", 0, 0x1a2b, 0x0001, "", "Unknown Device 0001 (VGA compatible)");
VERIFY_PCI("", 2, 0x1a2b, 0x0001, "", "Unknown Device 0001 (3D)");
}
// The happy path: vendor and device are both present.
{
VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One");
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0005, "Vendor Alpha", "Alpha Five");
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1000, "Vendor Alpha", "Alpha Thousand");
}
// Hex letters in the ids work on both sides.
{
VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0x0e1f, "Vendor Delta", "Delta Device");
}
// A name of the form `... [Alias]` is reduced to the alias. Only the trailing bracket pair is
// considered, and a `[` without a matching `]` at the end is kept verbatim.
{
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x00ff, "Vendor Alpha", "Alias");
}
// The lookup must stay inside the vendor block. `Beta` also has a 0x1234, but a lookup under
// `Alpha` must not find it -- `Alpha` has no 0x1234 of its own.
{
VERIFY_PCI(PCI_IDS, 0, 0x1a2c, 0x1234, "Vendor Beta", "Beta TwelveThirtyFour");
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1234, "Vendor Alpha", "Vendor Alpha Device 1234 (VGA compatible)");
}
// An unknown device under a known vendor synthesizes a name from the vendor.
{
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)");
VERIFY_PCI(PCI_IDS, 1, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (XGA compatible)");
VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (3D)");
VERIFY_PCI(PCI_IDS, 5, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002");
}
// The synthesized name uses the upper case form of the device id, while the lookup needle is
// lower case.
{
VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0xbeef, "Vendor Delta", "Vendor Delta Device BEEF (VGA compatible)");
}
// A device id below the first entry, between two entries and above the last one all miss. The
// first case is also what the "stop once the ids grow past the target" shortcut relies on.
{
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0000, "Vendor Alpha", "Vendor Alpha Device 0000 (VGA compatible)");
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0020, "Vendor Alpha", "Vendor Alpha Device 0020 (VGA compatible)");
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x9999, "Vendor Alpha", "Vendor Alpha Device 9999 (VGA compatible)");
}
// A subsystem id lives at twice the indentation and must never be mistaken for a device id,
// even when it is the value being searched for.
{
VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0abc, "Vendor Alpha", "Vendor Alpha Device 0ABC (VGA compatible)");
}
// A vendor with no device lines falls back to the synthesized name, and the vendor is still
// reported.
{
VERIFY_PCI(PCI_IDS, 0, 0xffff, 0x0001, "Vendor NoDevices", "Vendor NoDevices Device 0001 (VGA compatible)");
}
// An unknown vendor leaves the vendor buffer empty, so the synthesized name says "Unknown".
{
VERIFY_PCI(PCI_IDS, 0, 0x1234, 0x5678, "", "Unknown Device 5678 (VGA compatible)");
}
// Lines beyond the vendor section (the class list) are not reachable from a vendor block.
{
VERIFY_PCI(PCI_IDS, 0, 0x000c, 0x0000, "", "Unknown Device 0000 (VGA compatible)");
}
// A device line as the very last line of the buffer, with no trailing newline.
{
VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One");
VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)");
}
// A vendor line as the very last line of the buffer, with no device lines after it.
{
VERIFY_PCI(PCI_IDS_VENDOR_ONLY, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Vendor Alpha Device 0001 (VGA compatible)");
}
// A known vendor is not overwritten, but resolving the device name still happens.
{
VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, "Custom Vendor", nullptr, "Custom Vendor", "Alpha One");
}
// Likewise a name the caller already resolved is kept when the entry is a plain one.
{
VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, nullptr, "Custom Name", "Vendor Alpha", "Custom Name");
}
puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET);
return 0;
}
#else // FF_TEST_HAS_PCI_IDS
int main(void) {
puts("\033[32mTests skipped. (pci.ids parser not available on this platform)" FASTFETCH_TEXT_MODIFIER_RESET);
return 0;
}
#endif
-10
View File
@@ -1,6 +1,5 @@
#include "logo/logo.h" #include "logo/logo.h"
#include "common/textModifier.h" #include "common/textModifier.h"
#include "fastfetch.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -56,15 +55,6 @@ int main(void) {
VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_TOP), "top") == 0); VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_TOP), "top") == 0);
VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_RIGHT), "right") == 0); VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_RIGHT), "right") == 0);
// A `$` ending a line or the logo is a plain dollar: it keeps the line break and the terminator.
instance.config.display.pipe = true;
ffLogoPrintChars("ab$\ncd", true);
VERIFY(instance.state.logoHeight == 1 && instance.state.logoWidth == 3);
ffLogoPrintChars("ab$", true);
VERIFY(instance.state.logoHeight == 0 && instance.state.logoWidth == 3);
ffLogoPrintChars("a$1b$$c", true);
VERIFY(instance.state.logoWidth == 4);
puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET);
return 0; return 0;
} }
-11
View File
@@ -180,17 +180,6 @@ int main(void) {
VERIFY_BAR(14, config, "[ ■--------- ]"); VERIFY_BAR(14, config, "[ ■--------- ]");
VERIFY_BAR(15, config, "[ ■■-------- ]"); VERIFY_BAR(15, config, "[ ■■-------- ]");
// A value above 100 saturates the bar. That is reachable from real data: the load average is
// normalized against the number of cores, so it exceeds 100% as soon as the load does.
VERIFY_BAR(101, config, "[ ■■■■■■■■■■ ]");
VERIFY_BAR(150, config, "[ ■■■■■■■■■■ ]");
// Narrowing to `uint8_t` before clamping would wrap around here and silently render 4 blocks
VERIFY_BAR(2600, config, "[ ■■■■■■■■■■ ]");
// A negative value fills nothing
VERIFY_BAR(-60, config, "[ ---------- ]");
options->barWidth = 4; options->barWidth = 4;
VERIFY_BAR(0, config, "[ ---- ]"); VERIFY_BAR(0, config, "[ ---- ]");
VERIFY_BAR(25, config, "[ ■--- ]"); VERIFY_BAR(25, config, "[ ■--- ]");