mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-29 11:10:59 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fac492ac9 |
@@ -59,14 +59,13 @@
|
||||
.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-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-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; }
|
||||
.empty { padding: 2rem 0; color: var(--muted); }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
|
||||
<script src="data.js"></script>
|
||||
<script src="https://unpkg.com/chart.js@latest/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -74,11 +73,11 @@
|
||||
<h1>Fastfetch Benchmark</h1>
|
||||
<button id="theme-toggle" class="theme-toggle" type="button"></button>
|
||||
</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>
|
||||
</header>
|
||||
<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>
|
||||
(() => {
|
||||
const benchmarkData = window.BENCHMARK_DATA || {};
|
||||
@@ -104,12 +103,11 @@
|
||||
}
|
||||
|
||||
function entryMetadata(entry) {
|
||||
const raw = entry.commit?.timestamp ?? entry.date;
|
||||
const raw = entry.commit && entry.commit.timestamp || entry.date;
|
||||
const date = new Date(raw);
|
||||
return {
|
||||
date: Number.isNaN(date.valueOf()) ? String(entry.date ?? '') : date.toISOString(),
|
||||
sha: entry.commit?.id ?? 'unknown',
|
||||
message: entry.commit?.message?.trim() ?? ''
|
||||
date: Number.isNaN(date.valueOf()) ? String(entry.date || '') : date.toISOString(),
|
||||
sha: entry.commit && entry.commit.id ? entry.commit.id : 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,8 +131,6 @@
|
||||
'#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2',
|
||||
'#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 panels = document.getElementById('panels');
|
||||
const root = document.documentElement;
|
||||
@@ -143,13 +139,6 @@
|
||||
let theme = savedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
const chartsByInstance = [];
|
||||
|
||||
if (!window.Chart) {
|
||||
tabs.hidden = true;
|
||||
panels.innerHTML = '<p class="empty">Failed to load Chart.js.</p>';
|
||||
return;
|
||||
}
|
||||
panels.replaceChildren();
|
||||
|
||||
function chartColors() {
|
||||
const styles = getComputedStyle(root);
|
||||
return {
|
||||
@@ -178,12 +167,6 @@
|
||||
const row = chart._benchmarkRows[tooltip.dataPoints[0].dataIndex];
|
||||
title.textContent = `${new Date(row.date).toLocaleString()} · ${row.sha}`;
|
||||
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');
|
||||
bodyElement.className = 'chart-tooltip-body';
|
||||
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 });
|
||||
}
|
||||
|
||||
// 'Other' is intentionally not rendered: it only catches stray series (e.g. the retired
|
||||
// 'macos universal' records) that should not show up as their own tab
|
||||
const familyOrder = ['Intel', 'ARM'];
|
||||
const familyOrder = ['Intel', 'ARM', 'Other'];
|
||||
const availableFamilies = familyOrder.filter(family => families.has(family));
|
||||
|
||||
for (const [index, family] of availableFamilies.entries()) {
|
||||
@@ -330,7 +311,7 @@
|
||||
pointRadius: 3,
|
||||
tension: 0.2,
|
||||
spanGaps: true,
|
||||
hidden: hiddenByDefault.has(metric)
|
||||
hidden: metric !== 'total process time'
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
@@ -392,7 +373,7 @@
|
||||
chart.update();
|
||||
});
|
||||
addLegendAction('Reset', () => {
|
||||
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, !hiddenByDefault.has(metric)));
|
||||
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, metric === 'total process time'));
|
||||
refreshLegend();
|
||||
chart.update();
|
||||
});
|
||||
@@ -414,7 +395,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!availableFamilies.length) {
|
||||
if (!groups.size) {
|
||||
tabs.hidden = true;
|
||||
panels.innerHTML = '<p class="empty">No benchmark data available.</p>';
|
||||
}
|
||||
|
||||
@@ -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-*.*
|
||||
@@ -1,7 +1,14 @@
|
||||
name: Reusable macOS Universal
|
||||
name: Reusable macOS Hosts
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
arch:
|
||||
required: true
|
||||
type: string
|
||||
runs-on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -11,7 +18,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -19,20 +26,34 @@ jobs:
|
||||
- name: 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
|
||||
if: inputs.arch == 'aarch64'
|
||||
run: |
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew update
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk 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
|
||||
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
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
- name: verify universal binary
|
||||
run: lipo -archs fastfetch
|
||||
|
||||
- name: list features
|
||||
run: ./fastfetch --list-features
|
||||
|
||||
@@ -51,7 +72,7 @@ jobs:
|
||||
run: |
|
||||
real_seconds=$(awk '$1 == "real" { print $2 }' fastfetch.time)
|
||||
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") | {
|
||||
name: ("fastfetch " + $platform + " " + $arch + " module " + .type),
|
||||
unit: "ms",
|
||||
@@ -76,11 +97,11 @@ jobs:
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fastfetch-macos-universal
|
||||
name: fastfetch-macos-${{ inputs.arch }}
|
||||
path: ./fastfetch-*.*
|
||||
|
||||
- name: upload benchmark data
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: benchmark-macos-universal
|
||||
name: benchmark-macos-${{ inputs.arch }}
|
||||
path: benchmark.json
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
msystem: ${{ inputs.msystem }}
|
||||
update: true
|
||||
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-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
|
||||
run: uname -a
|
||||
|
||||
+10
-16
@@ -72,27 +72,22 @@ jobs:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-musl-amd64.yml
|
||||
|
||||
android:
|
||||
macos-hosts:
|
||||
needs: no-features-test
|
||||
name: Android-${{ matrix.abi }}
|
||||
name: macOS-${{ matrix.arch }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
abi:
|
||||
- arm64-v8a
|
||||
- armeabi-v7a
|
||||
- x86_64
|
||||
uses: ./.github/workflows/build-android.yml
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
|
||||
macos-hosts:
|
||||
needs: no-features-test
|
||||
name: macOS-universal
|
||||
permissions:
|
||||
contents: read
|
||||
include:
|
||||
- arch: amd64
|
||||
runs-on: macos-15-intel
|
||||
- arch: aarch64
|
||||
runs-on: macos-latest
|
||||
uses: ./.github/workflows/build-macos-hosts.yml
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
||||
omnios-amd64:
|
||||
needs: no-features-test
|
||||
@@ -195,7 +190,6 @@ jobs:
|
||||
- linux-loong64
|
||||
- linux-vms
|
||||
- musl-amd64
|
||||
- android
|
||||
- macos-hosts
|
||||
- freebsd-amd64
|
||||
- openbsd-amd64
|
||||
|
||||
+1
-23
@@ -2,29 +2,14 @@
|
||||
|
||||
Changes:
|
||||
* LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits.
|
||||
* Interfaces that carry no unicast address are no longer reported, since both the addresses and the interface properties are now enumerated from the unicast address table. (LocalIP, Windows)
|
||||
* libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package.
|
||||
* QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General)
|
||||
* The Command module option `useStdErr` has been replaced with `useOutput`, which selects which of the command's output streams to capture: `"stdout"`, `"stderr"` or `"both"`.
|
||||
* It defaults to `"both"`, so a command that logs to stderr now has that output captured instead of discarded.
|
||||
* Set it to `"stdout"` or `"stderr"` to restore the old single-stream behavior. (Command)
|
||||
* The Shell module now reports the shell from the password database (`/etc/passwd`) instead of the `$SHELL` environment variable, which may differ when the login shell was changed after the session started. (Title)
|
||||
* The Disk custom format no longer provides `{size-available}`. `{size-free}` now reports whichever of the two the `disk.useAvailable` option selects, and the raw byte counts are available as `{bytes-used}` and `{bytes-total}`. (Disk)
|
||||
* This also means `{size-free}` now changes with `disk.useAvailable`; it used to always report the file system's free size.
|
||||
* The Disk JSON output no longer includes `bytes.available` and `bytes.used`. (Disk)
|
||||
* `bytes.free` and `bytes.total` are unchanged; the used size can be derived as `total - free`.
|
||||
|
||||
Features:
|
||||
* Added Sound detection on Android. (Sound, Android)
|
||||
* Added display detection on Android over binder, which needs neither a child process nor a permission. (Display, Android)
|
||||
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
|
||||
* Added media detection on Android when fastfetch runs as an app UID. (Media, Android)
|
||||
* Requires `Notification access` permission
|
||||
* Added app display-name detection on Android, so the media module names the player the way the app names itself instead of by its package name, and the terminal module reports the label Termux is installed under. (Media, Terminal, Android)
|
||||
* Performance improvements & internal cleanups
|
||||
* Added Finnish language support. (General)
|
||||
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
|
||||
* Added an ALSA fallback to Sound on Linux, so a machine that runs no sound server reports its output device and volume instead of an error. (Sound, Linux)
|
||||
* Added Finnish Language Support
|
||||
|
||||
Bugfixes:
|
||||
* Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash)
|
||||
@@ -33,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 `/boot` being hidden by default on Haiku, where it is the system volume. (Disk, Haiku)
|
||||
* Improved Snapdragon X1/X2 model name detection on Linux. (#2625, CPU, Linux)
|
||||
* Fixed the Android build failing with `GL/gl.h` not found, because the NDK ships the EGL headers that turned OpenGL detection on but not the desktop GL headers it includes. (OpenGL, Android)
|
||||
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
|
||||
* Fixed assertion failure when percentage exceeds 100. (Percent)
|
||||
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
|
||||
* Fixed Android display detection on devices running Android 17. (Display, Android)
|
||||
* Fixed Windows volumes with a per-user quota (such as network shares) reporting a wrong total size and a wrapped-around used size, because the total size was quota-limited while the free size was not. (#2651, Disk, Windows)
|
||||
* On Windows 10 1809 (build 17763) and later, the whole volume's capacity and free space are now reported by default. Set `disk.useAvailable` to `true` to keep reporting the space that is available to the current user, quota included.
|
||||
|
||||
Logos:
|
||||
* Added filled Artix logo (#2631)
|
||||
|
||||
+9
-106
@@ -60,7 +60,6 @@ endif()
|
||||
|
||||
include(CheckIncludeFile)
|
||||
include(CheckFunctionExists)
|
||||
include(CheckSymbolExists)
|
||||
|
||||
if(LINUX)
|
||||
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_datatext.h.in fastfetch_datatext.h @ONLY)
|
||||
if(APPLE)
|
||||
# `mediaremoted` serves now-playing data only to clients whose bundle id starts
|
||||
# with "com.apple.": -[MRDMediaRemoteClient isAllowedAccessToDataFromPlayerPath:]
|
||||
# accepts the 0x200 entitlement bit either for the private
|
||||
# com.apple.mediaremote.now-playing-read-access entitlement or for that prefix,
|
||||
# and it derives the client's identity from the code signature / XPC audit token
|
||||
# rather than from the process's own NSBundle. Both the embedded
|
||||
# CFBundleIdentifier and the ad-hoc signing identifier below therefore have to
|
||||
# carry the prefix; keep them in sync through these variables.
|
||||
set(FASTFETCH_BUNDLE_ID "com.apple.fastfetch")
|
||||
set(FLASHFETCH_BUNDLE_ID "com.apple.flashfetch")
|
||||
# configure_file() inputs are not configure dependencies on their own, so
|
||||
# without this an edit to Info.plist.in never even regenerates Info.plist.
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/common/apple/Info.plist.in")
|
||||
# Each executable gets its own plist: the file is embedded with -sectcreate and read
|
||||
# back by the system as the bundle id, so two targets sharing one would leave one of
|
||||
# them advertising an identifier that its code signature does not match.
|
||||
set(BUNDLE_ID "${FASTFETCH_BUNDLE_ID}")
|
||||
configure_file(src/common/apple/Info.plist.in Info.plist @ONLY)
|
||||
if(BUILD_FLASHFETCH)
|
||||
set(BUNDLE_ID "${FLASHFETCH_BUNDLE_ID}")
|
||||
configure_file(src/common/apple/Info.plist.in Info.plist.flashfetch @ONLY)
|
||||
endif()
|
||||
unset(BUNDLE_ID)
|
||||
endif()
|
||||
|
||||
####################
|
||||
@@ -529,7 +506,6 @@ set(LIBFASTFETCH_SRC
|
||||
src/common/impl/FFPlatform.c
|
||||
src/common/impl/smbios.c
|
||||
src/common/impl/strutil.c
|
||||
src/common/impl/zlib.c
|
||||
src/detection/bluetoothradio/bluetoothradio.c
|
||||
src/detection/bootmgr/bootmgr.c
|
||||
src/detection/chassis/chassis.c
|
||||
@@ -678,11 +654,9 @@ if(LINUX)
|
||||
)
|
||||
elseif(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/android/applabel.c
|
||||
src/common/android/binder.c
|
||||
src/common/android/dex.c
|
||||
src/common/android/package.c
|
||||
src/common/android/zip.c
|
||||
src/common/impl/dbus.c
|
||||
src/common/impl/io_unix.c
|
||||
src/common/impl/netif_linux.c
|
||||
@@ -1076,7 +1050,6 @@ elseif(APPLE)
|
||||
src/detection/libc/libc_apple.c
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/localip/localip_apple.c
|
||||
src/detection/gamepad/gamepad_apple.m
|
||||
src/detection/media/media_apple.m
|
||||
src/detection/memory/memory_apple.c
|
||||
@@ -1575,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")
|
||||
endif()
|
||||
|
||||
# Termux installs the libraries fastfetch dlopens -- freetype among them -- under TARGET_DIR_ROOT, which
|
||||
# is not on the Android linker's default search path (DT_RUNPATH, LD_LIBRARY_PATH and the default
|
||||
# namespace). Termux's own clang injects -rpath=$PREFIX/lib into every binary it links, so a build made
|
||||
# inside Termux finds them; a plain NDK build, which is what the CI does, does not.
|
||||
if(ANDROID)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${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()
|
||||
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()
|
||||
|
||||
@@ -1691,13 +1660,6 @@ if(FreeBSD OR OpenBSD OR NetBSD)
|
||||
unset(CMAKE_REQUIRED_DEFINITIONS)
|
||||
endif()
|
||||
|
||||
if(OpenBSD)
|
||||
check_symbol_exists("getexecpath" "unistd.h" HAVE_GETEXECPATH)
|
||||
if(HAVE_GETEXECPATH)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GETEXECPATH=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAVE_PIPE2)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_PIPE2)
|
||||
endif()
|
||||
@@ -1773,18 +1735,9 @@ function(ff_lib_enable VARNAME PKGCONFIG_NAMES CMAKE_NAME) # [CMAKE_TARGET_NAME]
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
# find_package modules disagree on how they name their result variables. Newer ones
|
||||
# export <PackageName>_* (FindVulkan sets Vulkan_INCLUDE_DIRS), but older ones only
|
||||
# set the upper-case form: FindFreetype sets FREETYPE_INCLUDE_DIRS and
|
||||
# FREETYPE_LIBRARIES and has no Freetype_INCLUDE_DIRS at all. Assigning
|
||||
# unconditionally would therefore wipe the include path the module had just
|
||||
# provided, and the package would be "found" with nothing to compile against.
|
||||
# Fill in only the gaps.
|
||||
foreach(_FF_SUFFIX INCLUDE_DIRS LIBRARIES CFLAGS_OTHER)
|
||||
if(NOT DEFINED ${VARNAME}_${_FF_SUFFIX} OR "${${VARNAME}_${_FF_SUFFIX}}" STREQUAL "")
|
||||
set(${VARNAME}_${_FF_SUFFIX} ${${CMAKE_NAME}_${_FF_SUFFIX}})
|
||||
endif()
|
||||
endforeach()
|
||||
set(${VARNAME}_INCLUDE_DIRS ${${CMAKE_NAME}_INCLUDE_DIRS})
|
||||
set(${VARNAME}_LIBRARIES ${${CMAKE_NAME}_LIBRARIES})
|
||||
set(${VARNAME}_CFLAGS_OTHER ${${CMAKE_NAME}_CFLAGS_OTHER})
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -1903,11 +1856,9 @@ ff_lib_enable(OPENCL
|
||||
"OpenCL"
|
||||
"OpenCL"
|
||||
)
|
||||
# `Freetype` is the module CMake ships (Modules/FindFreetype.cmake); there is no FindFreeType2, so
|
||||
# this name never resolved and only pkg-config could ever turn freetype on.
|
||||
ff_lib_enable(FREETYPE
|
||||
"freetype2"
|
||||
"Freetype"
|
||||
"FreeType2"
|
||||
)
|
||||
ff_lib_enable(ELF
|
||||
"libelf"
|
||||
@@ -2206,17 +2157,7 @@ if(NOT WIN32)
|
||||
endif()
|
||||
endif()
|
||||
if(NOT ENABLE_WORDEXP)
|
||||
# wordexp.h is not the only thing that can be missing. bionic declares glob() and globfree()
|
||||
# only from API 28, so an NDK build aimed at 24 has neither and a fallback that assumed glob
|
||||
# would have nothing to call. The symbol is what has to be checked, not the header: <glob.h>
|
||||
# is present at every level, it just declares nothing below 28.
|
||||
check_symbol_exists("glob" "glob.h" HAVE_GLOB)
|
||||
if(HAVE_GLOB)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GLOB=1)
|
||||
message(STATUS "wordexp.h not found or disabled, glob used instead")
|
||||
else()
|
||||
message(STATUS "neither wordexp.h nor glob.h is usable, config paths will not be expanded")
|
||||
endif()
|
||||
message(STATUS "wordexp.h not found or disabled, glob used instead")
|
||||
endif()
|
||||
if(ENABLE_THREADS AND CMAKE_USE_PTHREADS_INIT)
|
||||
CHECK_INCLUDE_FILE("pthread_np.h" HAVE_PTHREAD_NP)
|
||||
@@ -2275,15 +2216,6 @@ set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "")
|
||||
set_target_properties(fastfetch PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
if(WIN32)
|
||||
# version.rc embeds manifest.xml (RT_MANIFEST) and logo.ico (ICON) as *resource
|
||||
# references*, not as #includes, so the resource compiler never reports them and
|
||||
# CMake would keep a stale .res when only the manifest or the icon changes. Both
|
||||
# are plain files rather than generated ones, so declare them by hand; the source
|
||||
# file property is per-path, which is why flashfetch below needs no repeat.
|
||||
set_property(SOURCE src/common/windows/version.rc APPEND PROPERTY OBJECT_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/manifest.xml"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/logo.ico"
|
||||
)
|
||||
target_sources(fastfetch
|
||||
PRIVATE src/common/windows/version.rc
|
||||
)
|
||||
@@ -2291,15 +2223,6 @@ elseif(APPLE)
|
||||
target_link_options(fastfetch
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
|
||||
)
|
||||
# `LINKER:` options are handed to ld verbatim and are not registered as build
|
||||
# dependencies, so without this ninja never relinks when Info.plist changes.
|
||||
set_property(TARGET fastfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist")
|
||||
# ld derives the ad-hoc code-signing identifier from the output file name and
|
||||
# ignores the embedded CFBundleIdentifier, so set it explicitly.
|
||||
add_custom_command(TARGET fastfetch POST_BUILD
|
||||
COMMAND codesign --force --sign - --identifier "${FASTFETCH_BUNDLE_ID}" "$<TARGET_FILE:fastfetch>"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
if(BINARY_LINK_TYPE STREQUAL "static")
|
||||
@@ -2325,15 +2248,8 @@ if (BUILD_FLASHFETCH)
|
||||
PRIVATE src/common/windows/version.rc
|
||||
)
|
||||
elseif(APPLE)
|
||||
# flashfetch embeds its own plist so that its CFBundleIdentifier matches the
|
||||
# identifier it is signed with below (see the APPLE block near the top).
|
||||
target_link_options(flashfetch
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist.flashfetch
|
||||
)
|
||||
set_property(TARGET flashfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist.flashfetch")
|
||||
add_custom_command(TARGET flashfetch POST_BUILD
|
||||
COMMAND codesign --force --sign - --identifier "${FLASHFETCH_BUNDLE_ID}" "$<TARGET_FILE:flashfetch>"
|
||||
VERBATIM
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -2473,13 +2389,6 @@ if (BUILD_TESTS)
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-gpu-pci
|
||||
tests/gpu_pci.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-gpu-pci
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME test-cache COMMAND fastfetch-test-cache)
|
||||
add_test(NAME test-logo COMMAND fastfetch-test-logo)
|
||||
@@ -2499,7 +2408,6 @@ if (BUILD_TESTS)
|
||||
add_test(NAME test-temps COMMAND fastfetch-test-temps)
|
||||
add_test(NAME test-endian COMMAND fastfetch-test-endian)
|
||||
add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian)
|
||||
add_test(NAME test-gpu-pci COMMAND fastfetch-test-gpu-pci)
|
||||
endif()
|
||||
|
||||
##################
|
||||
@@ -2561,12 +2469,7 @@ install(
|
||||
|
||||
set(CPACK_GENERATOR "TGZ;ZIP")
|
||||
if(APPLE)
|
||||
if(CMAKE_OSX_ARCHITECTURES MATCHES ";") # multiple architectures, e.g. arm64;x86_64
|
||||
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
|
||||
string(TOLOWER "${CMAKE_PROJECT_NAME}-macos-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME) # use macos instead of darwin
|
||||
elseif(IS_MUSL)
|
||||
string(TOLOWER "${CMAKE_PROJECT_NAME}-musl-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME)
|
||||
else()
|
||||
|
||||
+1
-1
@@ -680,7 +680,7 @@ cmake --build build
|
||||
cd build && ctest --output-on-failure
|
||||
```
|
||||
|
||||
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), 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.**
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
[](https://formulae.brew.sh/formula/fastfetch#default)
|
||||
[](https://github.com/fastfetch-cli/fastfetch/releases)
|
||||
[](https://github.com/fastfetch-cli/fastfetch/releases)
|
||||
[](https://repology.org/project/fastfetch/versions)
|
||||
[](https://repology.org/project/fastfetch/versions)
|
||||
[](https://repology.amdmi3.ru/project/fastfetch/versions)
|
||||
[](https://repology.amdmi3.ru/project/fastfetch/versions)
|
||||
[](https://deepwiki.com/fastfetch-cli/fastfetch)
|
||||
[](README-cn.md)
|
||||
|
||||
@@ -36,8 +36,8 @@ There are [screenshots on different platforms](https://github.com/fastfetch-cli/
|
||||
|
||||
Some distributions package outdated versions of fastfetch. Older versions receive no support, so please always try to use the latest version.
|
||||
|
||||
<a href="https://repology.org/project/fastfetch/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right">
|
||||
<a href="https://repology.amdmi3.ru/project/fastfetch/versions">
|
||||
<img src="https://repology.amdmi3.ru/badge/vertical-allrepos/fastfetch.svg?columns=2" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
* Ubuntu: [`ppa:zhangsongcui3371/fastfetch`](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) (Ubuntu 22.04 or newer; latest version)
|
||||
|
||||
+9
-23
@@ -277,7 +277,7 @@
|
||||
|
||||
|
||||
"batteryFormat": {
|
||||
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
|
||||
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
|
||||
"type": "string"
|
||||
},
|
||||
"biosFormat": {
|
||||
@@ -325,7 +325,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"cpuFormat": {
|
||||
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count",
|
||||
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name (x86 only)\n 14. {technology}: CPU technology (x86 only)",
|
||||
"type": "string"
|
||||
},
|
||||
"cpucacheFormat": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"diskFormat": {
|
||||
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used (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"
|
||||
},
|
||||
"diskioFormat": {
|
||||
@@ -465,7 +465,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"poweradapterFormat": {
|
||||
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model 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"
|
||||
},
|
||||
"processesFormat": {
|
||||
@@ -2381,24 +2381,10 @@
|
||||
"description": "Command text to execute",
|
||||
"type": "string"
|
||||
},
|
||||
"useOutput": {
|
||||
"description": "Which of the command's output streams to capture\nThe stream that is not captured is discarded",
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "stdout",
|
||||
"description": "Capture stdout only and discard stderr"
|
||||
},
|
||||
{
|
||||
"const": "stderr",
|
||||
"description": "Capture stderr only and discard stdout"
|
||||
},
|
||||
{
|
||||
"const": "both",
|
||||
"description": "Capture both stdout and stderr"
|
||||
}
|
||||
],
|
||||
"default": "both"
|
||||
"useStdErr": {
|
||||
"description": "Whether to use stderr instead of stdout for command output",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"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",
|
||||
@@ -2716,7 +2702,7 @@
|
||||
},
|
||||
"useAvailable": {
|
||||
"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
|
||||
},
|
||||
"percent": {
|
||||
|
||||
@@ -466,12 +466,6 @@ static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainAnyC(const FFstrbuf* strbuf, const char* cs) {
|
||||
assert(cs != nullptr);
|
||||
|
||||
return strpbrk(strbuf->chars, cs) != nullptr;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
|
||||
assert(start <= strbuf->length);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// or does not hold a dex that can be read. nullptr means the lookup ran.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Reading a Parcelable's wire layout
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//
|
||||
// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
|
||||
// that order is the only thing that says where each one is. It is not in the class's declaration: a
|
||||
// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
|
||||
// declaration order a reader would want is not recorded anywhere but the method body.
|
||||
//
|
||||
// Recovering it therefore means walking the bytecode. The compiler emits
|
||||
//
|
||||
// iget v0, p0, LDisplayInfo;->logicalWidth:I
|
||||
// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
|
||||
//
|
||||
// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
|
||||
// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
|
||||
// easy to get wrong in a way that still produces a plausible-looking list:
|
||||
//
|
||||
// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
|
||||
// The table below is ART's `Instruction::SizeInCodeUnits`.
|
||||
// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
|
||||
// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
|
||||
// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
|
||||
// first of them rather than trying to step over it.
|
||||
//
|
||||
// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
|
||||
// again inside the loop that writes the elements. A class cannot declare two instance fields under
|
||||
// one name, so a repeat is always that reload, and the first occurrence is where the field is
|
||||
// written. The reader de-duplicates by name for that reason.
|
||||
//
|
||||
// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
|
||||
// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
|
||||
// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
|
||||
// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
|
||||
// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
|
||||
// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
|
||||
// does not. Measured over three releases' own `framework.jar`, that is four names per class.
|
||||
//
|
||||
// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
|
||||
// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
|
||||
// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
|
||||
// ones a parser written against one release reads out of the middle of another. Reading the sequence
|
||||
// from the build that is running makes every one of them optional by construction.
|
||||
|
||||
// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
|
||||
// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
|
||||
// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
|
||||
// than truncated: half a name would silently match nothing, which is indistinguishable from the field
|
||||
// not being there.
|
||||
#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
|
||||
#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
|
||||
|
||||
// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
|
||||
// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
|
||||
typedef struct FFDexParcelField {
|
||||
char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
|
||||
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
|
||||
} FFDexParcelField;
|
||||
|
||||
// One class, and the two things that can be asked about it: the write sequence of one of its methods,
|
||||
// and which instance fields it declares. Either half may be left out, and both are answered out of
|
||||
// the same `class_data_item` -- which is why they are one request rather than two calls.
|
||||
//
|
||||
// Asking for them together is not a convenience. Both halves are read from a walk that is dominated
|
||||
// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a
|
||||
// different part of a 51 MB mapping -- and the fields a class declares are listed in the very
|
||||
// `class_data_item` the sequence is read out of. A second call would map the jar again, open every
|
||||
// entry again and make that pass again to reach a list the first call already had in hand. Measured
|
||||
// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup.
|
||||
typedef struct FFDexParcelRequest {
|
||||
const char* classDescriptor;
|
||||
|
||||
// The write sequence. Left out by a request that only wants the instance fields, in which case
|
||||
// `fields`, `capacity` and `count` are not read.
|
||||
const char* methodName;
|
||||
FFDexParcelField* fields;
|
||||
uint32_t capacity;
|
||||
uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
|
||||
|
||||
// The instance fields. This is the question a Parcelable whose layout is not field-driven needs.
|
||||
// `DisplayCutout` is one: its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors
|
||||
// the compiler synthesised, so there is no `iget` to walk and no class name on the wire either.
|
||||
// What does say how long its body is, is which fields the class carries -- Android 11 declares
|
||||
// three, Android 13 adds `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each
|
||||
// one adds a known run of writes behind the three both have. A declaration is a field set rather
|
||||
// than an order, which is exactly what this answers.
|
||||
const char* const* instanceFieldNames;
|
||||
bool* instanceFieldResults;
|
||||
uint32_t instanceFieldCount;
|
||||
} FFDexParcelRequest;
|
||||
|
||||
// Resolves every request out of the dex entries of `jarPath`, walking `classes.dex`, `classes2.dex`,
|
||||
// ... in order until one of them defines the class -- the same walk, and the same one pass over the
|
||||
// type table, that ffDexStaticInts makes.
|
||||
//
|
||||
// Every request is written its answers before anything can fail: a count of 0 for the sequence, and
|
||||
// `false` for every instance field. A class the jar does not declare, a method it does not declare,
|
||||
// and a sequence longer than the caller's `capacity` all leave the count at 0, and none of them holds
|
||||
// back the others: they are independent questions about the same pass. A count of 0 is not a failure
|
||||
// but an answer -- this build does not write that class, so a caller reading it has nothing to walk.
|
||||
// The return value is reserved for the jar itself, as in ffDexStaticInts.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);
|
||||
|
||||
@@ -1,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";
|
||||
}
|
||||
@@ -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);
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>@BUNDLE_ID@</string>
|
||||
<string>fastfetch</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>@PROJECT_NAME@</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -64,11 +64,7 @@ static void getExePath(FFPlatform* platform) {
|
||||
exePathLen--; // remove terminating NUL
|
||||
}
|
||||
#elif defined(__OpenBSD__)
|
||||
#if FF_HAVE_GETEXECPATH
|
||||
// https://man.openbsd.org/getexecpath.3
|
||||
size_t exePathLen = getexecpath(exePath, sizeof(exePath)) == 0 ? strlen(exePath) : 0;
|
||||
#else
|
||||
// OpenBSD (before 8.0) doesn't have a reliable way to get the executable path.
|
||||
// OpenBSD doesn't have a reliable way to get the executable path.
|
||||
// Current implementation uses argv[0], which can be easily spoofed.
|
||||
// See #2195
|
||||
size_t exePathLen = 0;
|
||||
@@ -169,7 +165,6 @@ static void getExePath(FFPlatform* platform) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#elif defined(__sun)
|
||||
ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
|
||||
if (exePathLen > 0) {
|
||||
@@ -304,7 +299,10 @@ static void getHostName(FFPlatform* platform, const struct utsname* uts) {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -338,12 +336,7 @@ static void getCwd(FFPlatform* platform) {
|
||||
void ffPlatformInitImpl(FFPlatform* platform) {
|
||||
platform->pid = (uint32_t) getpid();
|
||||
platform->uid = getuid();
|
||||
#if !__ANDROID__
|
||||
struct passwd* pwd = getpwuid(platform->uid);
|
||||
#else
|
||||
// On Android, /etc/passwd is empty, and getpwuid() will return `/data` for pw_dir
|
||||
struct passwd* pwd = nullptr;
|
||||
#endif
|
||||
|
||||
struct utsname uts;
|
||||
if (uname(&uts) < 0) {
|
||||
|
||||
@@ -28,19 +28,16 @@ void ffDebugPrint(const char* file, int line, const char* format, ...) {
|
||||
}
|
||||
|
||||
static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER;
|
||||
// Saved before the lock and restored after the unlock: FF_DEBUG() is called from error paths
|
||||
// that go on to read `errno`, so nothing this function does -- neither the lock nor the
|
||||
// formatting -- may be allowed to leave its own value behind.
|
||||
int errno_ = errno;
|
||||
ffThreadMutexLock(&debugMutex);
|
||||
static char errmsg_[1024]; // Don't increase stack size
|
||||
int errno_ = errno;
|
||||
static char errmsg_[1024];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(errmsg_, sizeof(errmsg_), format, args);
|
||||
va_end(args);
|
||||
fprintf(stderr, "[%s%4d, %s] %s\n", ffFindFileName(file), line, ffTimeToTimeStr(ffTimeGetNow()), errmsg_);
|
||||
ffThreadMutexUnlock(&debugMutex);
|
||||
errno = errno_;
|
||||
ffThreadMutexUnlock(&debugMutex);
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#if FF_HAVE_WORDEXP
|
||||
#include <wordexp.h>
|
||||
#elif FF_HAVE_GLOB
|
||||
#else
|
||||
#include <glob.h>
|
||||
#endif
|
||||
|
||||
@@ -112,7 +112,7 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
|
||||
|
||||
wordfree(&exp);
|
||||
|
||||
#elif FF_HAVE_GLOB
|
||||
#else
|
||||
|
||||
glob_t gb;
|
||||
if (glob(in, GLOB_NOSORT
|
||||
@@ -134,10 +134,6 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
|
||||
|
||||
globfree(&gb);
|
||||
|
||||
#else
|
||||
|
||||
FF_UNUSED(in, out)
|
||||
|
||||
#endif
|
||||
|
||||
return result;
|
||||
|
||||
@@ -88,11 +88,8 @@ void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConf
|
||||
|
||||
FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
|
||||
|
||||
uint8_t blocksPercent = 0;
|
||||
if (percent > 0) {
|
||||
double blocks = percent / 100.0 * options->barWidth + 0.5;
|
||||
blocksPercent = (uint8_t) (blocks >= options->barWidth ? options->barWidth : blocks);
|
||||
}
|
||||
uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5);
|
||||
assert(blocksPercent <= options->barWidth);
|
||||
|
||||
bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ static inline int ffPipe2(int* readEnd, int* writeEnd, int flags) {
|
||||
}
|
||||
|
||||
// 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 pipeWrite = -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;
|
||||
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__)
|
||||
|
||||
@@ -87,8 +83,8 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
|
||||
|
||||
posix_spawn_file_actions_t 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, stderrFd, STDERR_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO);
|
||||
}
|
||||
@@ -153,8 +149,8 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
|
||||
|
||||
if (childPid == 0) {
|
||||
// Child process
|
||||
dup2(stdoutFd, STDOUT_FILENO);
|
||||
dup2(stderrFd, STDERR_FILENO);
|
||||
dup2(pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO);
|
||||
dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
dup2(stdinFd, STDIN_FILENO);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
if (ffStrbufContainAnyC(&temp, " \t")) {
|
||||
if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) {
|
||||
uint32_t pos = temp.length;
|
||||
uint32_t cnt = 0;
|
||||
while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
|
||||
@@ -84,7 +84,7 @@ static wchar_t* createChildEnvironment(void) {
|
||||
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;
|
||||
|
||||
wchar_t pipeName[32];
|
||||
@@ -127,12 +127,13 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
|
||||
.cb = sizeof(siStartInfo),
|
||||
.dwFlags = STARTF_USESTDHANDLES,
|
||||
};
|
||||
|
||||
HANDLE nullFile = ffGetNullFD();
|
||||
// A stream wired to the pipe is captured; the other goes to the null device so it cannot mix
|
||||
// into the captured text or fill the pipe and block the child. Both may share the pipe handle.
|
||||
siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile;
|
||||
siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile;
|
||||
if (useStdErr) {
|
||||
siStartInfo.hStdOutput = ffGetNullFD();
|
||||
siStartInfo.hStdError = hChildPipeWrite;
|
||||
} else {
|
||||
siStartInfo.hStdOutput = hChildPipeWrite;
|
||||
siStartInfo.hStdError = ffGetNullFD();
|
||||
}
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
siStartInfo.hStdInput = stdinFd;
|
||||
}
|
||||
@@ -141,7 +142,6 @@ const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FF
|
||||
{
|
||||
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
||||
argvToCmdline(argv, &buf);
|
||||
FF_DEBUG("Command line: %s", buf.chars);
|
||||
uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
|
||||
cmdline = malloc(cmdlineBytes);
|
||||
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);
|
||||
if (!success) {
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
|
||||
FF_DEBUG("Command not found: %s (%s)", argv[0], ffDebugWin32Error(error));
|
||||
if (GetLastError() == ERROR_FILE_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";
|
||||
}
|
||||
|
||||
@@ -265,13 +263,7 @@ exit: {
|
||||
if (NT_SUCCESS(status)) {
|
||||
assert(size == sizeof(info));
|
||||
if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
|
||||
if (info.ExitStatus == 9009) {
|
||||
// Note: CMD swallows 9009 errorlevel and returns 1 by default.
|
||||
// One must use `& call exit %^ERRORLEVEL%` to propagate the correct error code.
|
||||
FF_DEBUG("Child process exited with error code 9009 (command not found)");
|
||||
return "command not found";
|
||||
}
|
||||
FF_DEBUG("Child process exited with an error: %x %s", (unsigned) info.ExitStatus, ffDebugNtStatus(info.ExitStatus));
|
||||
FF_DEBUG("Child process exited with an error");
|
||||
return "Child process exited with an error";
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
@@ -13,7 +13,6 @@
|
||||
#define LUALIB_API LUA_API
|
||||
#define LUAMOD_API LUA_API
|
||||
|
||||
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ typedef struct FFNetworkingState {
|
||||
// `Content-Length` header in place of `Transfer-Encoding`.
|
||||
// `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller
|
||||
// passed in no longer describes the buffer afterwards. Callers must use the new one for anything
|
||||
// that addresses the body.
|
||||
// that addresses the body, such as ffNetworkingDecompressGzip().
|
||||
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd);
|
||||
|
||||
// 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
|
||||
// as unsupported so that the caller fails the response instead of returning garbage.
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen);
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* ffNetworkingLoadZlibLibrary(void);
|
||||
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd);
|
||||
#endif
|
||||
|
||||
+3
-14
@@ -17,29 +17,18 @@ typedef struct FFProcessHandle {
|
||||
#endif
|
||||
} FFProcessHandle;
|
||||
|
||||
// Which of the child's two output streams are wired to the pipe that `ffProcessReadOutput()` drains.
|
||||
// The stream that is not selected is pointed at the null device, so it can neither interleave with
|
||||
// the captured text nor fill the pipe and block the child. This is a bitfield rather than a value
|
||||
// because `BOTH` is a combination: each backend sets up the child's two stdio slots independently,
|
||||
// testing one bit each.
|
||||
typedef enum FFProcessOutputType: uint8_t {
|
||||
FF_PROCESS_OUTPUT_STDOUT_BIT = 1 << 0,
|
||||
FF_PROCESS_OUTPUT_STDERR_BIT = 1 << 1,
|
||||
FF_PROCESS_OUTPUT_BOTH = FF_PROCESS_OUTPUT_STDOUT_BIT | FF_PROCESS_OUTPUT_STDERR_BIT,
|
||||
} FFProcessOutputType;
|
||||
|
||||
// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from
|
||||
// our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do.
|
||||
// Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the
|
||||
// service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then
|
||||
// only reports `Failure calling service <name>: Failed transaction`.
|
||||
#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1)
|
||||
const char* ffProcessSpawn(char* const argv[], 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
|
||||
|
||||
static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) {
|
||||
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) {
|
||||
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[]) {
|
||||
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) {
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
static inline bool ffIsFileFsFullSizeInformationExAvailable() {
|
||||
return SharedUserData->NtBuildNumber >= 17763; // Windows 10 October 2018 Update (1809, REDSTONE5)
|
||||
}
|
||||
|
||||
NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken(
|
||||
_In_ HANDLE ProcessHandle,
|
||||
_In_ ACCESS_MASK DesiredAccess,
|
||||
@@ -1423,23 +1419,3 @@ NTSYSAPI NTSTATUS NTAPI NtTerminateProcess(
|
||||
|
||||
NTSYSAPI NTSTATUS NTAPI RtlAcquirePebLock(VOID);
|
||||
NTSYSAPI NTSTATUS NTAPI RtlReleasePebLock(VOID);
|
||||
|
||||
typedef struct _FILE_FS_FULL_SIZE_INFORMATION_EX {
|
||||
ULONGLONG ActualTotalAllocationUnits;
|
||||
ULONGLONG ActualAvailableAllocationUnits;
|
||||
ULONGLONG ActualPoolUnavailableAllocationUnits;
|
||||
ULONGLONG CallerTotalAllocationUnits;
|
||||
ULONGLONG CallerAvailableAllocationUnits;
|
||||
ULONGLONG CallerPoolUnavailableAllocationUnits;
|
||||
ULONGLONG UsedAllocationUnits;
|
||||
ULONGLONG TotalReservedAllocationUnits;
|
||||
ULONGLONG VolumeStorageReserveAllocationUnits;
|
||||
ULONGLONG AvailableCommittedAllocationUnits;
|
||||
ULONGLONG PoolAvailableAllocationUnits;
|
||||
ULONG SectorsPerAllocationUnit;
|
||||
ULONG BytesPerSector;
|
||||
} FILE_FS_FULL_SIZE_INFORMATION_EX, *PFILE_FS_FULL_SIZE_INFORMATION_EX;
|
||||
|
||||
enum {
|
||||
FileFsFullSizeInformationEx = 14
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
// Records the largest size the camera can stream in the given pixel format. The stream configurations
|
||||
// are a flat array of (format, width, height, isInput) quads; a negative format matches any of them.
|
||||
static void 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;
|
||||
if (ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry) != ACAMERA_OK) {
|
||||
return;
|
||||
@@ -94,11 +94,11 @@ const char* ffDetectCamera(FFlist* result) {
|
||||
}
|
||||
|
||||
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) {
|
||||
// Not every camera HAL lists a JPEG format. The largest size of any output format is the
|
||||
// same resolution on those devices.
|
||||
getMaxStreamSize(metadata, -1, &camera->width, &camera->height);
|
||||
ffCameraMaxStreamSize(metadata, -1, &camera->width, &camera->height);
|
||||
}
|
||||
|
||||
ACameraMetadata_free(metadata);
|
||||
|
||||
@@ -15,19 +15,13 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand
|
||||
return "No command text specified";
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
if (ffStrbufEqualS(&options->shell, "cmd.exe") && ffStrbufEqualS(&options->param, "/d/c")) {
|
||||
ffStrbufAppendS(&options->text, " & exit %^errorlevel%"); // Necessary to propagate 9009 from CMD
|
||||
}
|
||||
#endif
|
||||
|
||||
return ffProcessSpawn(options->param.length ? (char* const[]) {
|
||||
options->shell.chars,
|
||||
options->param.chars,
|
||||
options->text.chars,
|
||||
nullptr }
|
||||
: (char* const[]) { options->shell.chars, options->text.chars, nullptr },
|
||||
options->useOutput,
|
||||
options->useStdErr,
|
||||
FF_PROCESS_INHERIT_STDIN,
|
||||
handle);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ const char* ffDetectDisks(FFDiskOptions* options, FFlist* disks) {
|
||||
FF_LIST_FOR_EACH (FFDisk, disk, *disks) {
|
||||
if (disk->bytesTotal == 0) {
|
||||
disk->type |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT;
|
||||
} else {
|
||||
disk->bytesUsed = disk->bytesTotal - (options->calcType == FF_DISK_CALC_TYPE_FREE ? disk->bytesFree : disk->bytesAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ typedef struct FFDisk {
|
||||
FFstrbuf name;
|
||||
FFDiskVolumeType type;
|
||||
|
||||
uint64_t bytesUsed;
|
||||
uint64_t bytesFree;
|
||||
uint64_t bytesAvailable;
|
||||
uint64_t bytesTotal;
|
||||
|
||||
uint32_t filesUsed;
|
||||
|
||||
@@ -188,7 +188,9 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
|
||||
FFDisk* disk = FF_LIST_ADD(FFDisk, *disks);
|
||||
|
||||
disk->bytesTotal = (uint64_t) fs->f_blocks * (uint64_t) fs->f_bsize;
|
||||
disk->bytesFree = (uint64_t) (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->filesUsed = (uint32_t) fs->f_files - (uint32_t) fs->f_ffree;
|
||||
|
||||
@@ -44,6 +44,8 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
|
||||
|
||||
disk->bytesTotal = (uint64_t) fs.total_blocks * (uint64_t) fs.block_size;
|
||||
disk->bytesFree = (uint64_t) fs.free_blocks * (uint64_t) fs.block_size;
|
||||
disk->bytesAvailable = disk->bytesFree;
|
||||
disk->bytesUsed = 0; // To be filled in ./disk.c
|
||||
|
||||
disk->filesTotal = (uint32_t) fs.total_nodes;
|
||||
disk->filesUsed = (uint32_t) (fs.total_nodes - fs.free_nodes);
|
||||
|
||||
@@ -249,14 +249,16 @@ static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mntent*
|
||||
|
||||
#endif
|
||||
|
||||
static void detectStats(FFDiskOptions* options, FFDisk* disk) {
|
||||
static void detectStats(FFDisk* disk) {
|
||||
struct statvfs fs;
|
||||
if (statvfs(disk->mountpoint.chars, &fs) != 0) {
|
||||
memset(&fs, 0, sizeof(fs)); // Set all values to 0, so our values get initialized to 0 too
|
||||
}
|
||||
|
||||
disk->bytesTotal = fs.f_blocks * (uint64_t) fs.f_frsize;
|
||||
disk->bytesFree = (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) {
|
||||
disk->filesTotal = (uint32_t) fs.f_files;
|
||||
@@ -328,7 +330,7 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
|
||||
detectType(disks, disk, device);
|
||||
|
||||
// Detects stats
|
||||
detectStats(options, disk);
|
||||
detectStats(disk);
|
||||
}
|
||||
|
||||
endmntent(mountsFile);
|
||||
|
||||
@@ -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;
|
||||
if (statvfs(disk->mountpoint.chars, &fs) != 0) {
|
||||
memset(&fs, 0, sizeof(fs));
|
||||
}
|
||||
|
||||
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->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);
|
||||
ffStrbufInit(&disk->name);
|
||||
detectType(disks, disk, &device);
|
||||
detectStats(options, disk);
|
||||
detectStats(disk);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -75,6 +75,8 @@ const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) {
|
||||
disk->filesTotal = 0;
|
||||
disk->bytesTotal = 0;
|
||||
disk->bytesFree = 0;
|
||||
disk->bytesUsed = 0; // To be filled in ./disk.c
|
||||
disk->bytesAvailable = 0;
|
||||
disk->createTime = 0;
|
||||
ffStrbufInit(&disk->filesystem);
|
||||
ffStrbufInit(&disk->name);
|
||||
@@ -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;
|
||||
if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) {
|
||||
uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit;
|
||||
disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units;
|
||||
disk->bytesFree = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units;
|
||||
}
|
||||
} else {
|
||||
FILE_FS_FULL_SIZE_INFORMATION_EX fsFullSizeEx;
|
||||
if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSizeEx, sizeof(fsFullSizeEx), FileFsFullSizeInformationEx))) {
|
||||
// Space info without considering the quota setting
|
||||
uint64_t units = fsFullSizeEx.BytesPerSector * fsFullSizeEx.SectorsPerAllocationUnit;
|
||||
disk->bytesTotal = (uint64_t) fsFullSizeEx.ActualTotalAllocationUnits * units;
|
||||
disk->bytesFree = (uint64_t) fsFullSizeEx.ActualAvailableAllocationUnits * units;
|
||||
}
|
||||
FILE_FS_FULL_SIZE_INFORMATION fsFullSize;
|
||||
if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) {
|
||||
uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit;
|
||||
disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units;
|
||||
disk->bytesFree = (uint64_t) fsFullSize.ActualAvailableAllocationUnits.QuadPart * units;
|
||||
disk->bytesAvailable = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// cancellation is waited for before returning.
|
||||
CancelIoEx(hHidFile, &overlapped);
|
||||
GetOverlappedResultEx(hHidFile, &overlapped, &read, INFINITE, FALSE);
|
||||
GetOverlappedResult(hHidFile, &overlapped, &read, TRUE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,6 @@ typedef struct FFGpuDriverPciBusId {
|
||||
} FFGpuDriverPciBusId;
|
||||
|
||||
#if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__)
|
||||
#define FF_GPU_HAS_PCI_IDS 1
|
||||
void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu);
|
||||
void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu);
|
||||
void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu);
|
||||
|
||||
|
||||
@@ -394,8 +394,6 @@ const char* ffGPUDetectDriverSpecific(const FFGPUOptions* options, FFGPUResult*
|
||||
},
|
||||
soName);
|
||||
}
|
||||
#else
|
||||
FF_UNUSED(options, gpu, pciBusId);
|
||||
#endif
|
||||
|
||||
return "No driver-specific detection function found for the GPU vendor";
|
||||
|
||||
+41
-57
@@ -60,78 +60,62 @@ static const FFstrbuf* loadPciIds() {
|
||||
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);
|
||||
if (content->length) {
|
||||
const char* const fileEnd = content->chars + content->length;
|
||||
char buffer[32];
|
||||
|
||||
// Search for vendor
|
||||
uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor);
|
||||
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) {
|
||||
FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor);
|
||||
start += len;
|
||||
const char* vendorEnd = memchr(start, '\n', (size_t) (fileEnd - start));
|
||||
if (!vendorEnd) {
|
||||
vendorEnd = fileEnd;
|
||||
end = memchr(start, '\n', (uint32_t) (end - start));
|
||||
if (!end) {
|
||||
end = content->chars + content->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
|
||||
// grow inside a vendor block, so one linear walk over the block is
|
||||
// enough: compare in place and stop either at the line starting the
|
||||
// next vendor, or -- because the ids are sorted -- right after the
|
||||
// first id greater than the one we are looking for. Doing the compare
|
||||
// here also removes the need to know where the block ends, so no
|
||||
// length is ever derived from a pointer that may have run past it.
|
||||
// pci.ids only asks contributors to keep the entries sorted, and it
|
||||
// is strictly lowercase hex, so the byte comparison below matches the
|
||||
// numeric order. A block that is not sorted (only reachable via
|
||||
// FF_CUSTOM_PCI_IDS_PATH) makes us give up early and fall back to the
|
||||
// synthesized name below.
|
||||
char deviceId[8];
|
||||
snprintf(deviceId, ARRAY_SIZE(deviceId), "%04x", device);
|
||||
[[maybe_unused]] bool found = false;
|
||||
for (const char* p = vendorEnd < fileEnd ? vendorEnd + 1 : fileEnd;
|
||||
p < fileEnd && (*p == '\t' || *p == '#');) {
|
||||
if (*p == '\t' && fileEnd - p >= 7) {
|
||||
int order = memcmp(p + 1, deviceId, 4);
|
||||
if (order > 0) {
|
||||
break;
|
||||
}
|
||||
if (order == 0 && p[5] == ' ' && p[6] == ' ') {
|
||||
FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device);
|
||||
start = p + 7;
|
||||
const char* end = memchr(start, '\n', (size_t) (fileEnd - start));
|
||||
if (!end) {
|
||||
end = fileEnd;
|
||||
}
|
||||
|
||||
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) {
|
||||
start = end; // point to '\n' of vendor
|
||||
end = start + 1; // point to start of devices
|
||||
// find the start of next vendor
|
||||
while (end[0] == '\t' || end[0] == '#') {
|
||||
end = strchr(end, '\n');
|
||||
if (!end) {
|
||||
end = content->chars + content->length;
|
||||
break;
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
@@ -247,7 +231,7 @@ void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device,
|
||||
return;
|
||||
}
|
||||
#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);
|
||||
}
|
||||
|
||||
|
||||
+16
-13
@@ -55,32 +55,35 @@ static const char* getSshdVersion(FFstrbuf* version) {
|
||||
}
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include "common/library.h"
|
||||
#include "common/path.h"
|
||||
#include "common/zlib.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <zlib.h>
|
||||
|
||||
static const char* getSddmVersion(FFstrbuf* version) {
|
||||
const char* error = ffZlibLoad(FF_ZLIB_GZFILE);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The sddm version cannot be read: %s", error);
|
||||
return error;
|
||||
}
|
||||
FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzopen)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzread)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzerror)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gztell)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzrewind)
|
||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzclose)
|
||||
|
||||
gzFile file = 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) {
|
||||
FF_DEBUG("gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed: %s", strerror(errno));
|
||||
return "gzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed";
|
||||
FF_DEBUG("ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed: %s", strerror(errno));
|
||||
return "ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed";
|
||||
}
|
||||
|
||||
ffStrbufEnsureFree(version, 2047);
|
||||
memset(version->chars, 0, version->allocated);
|
||||
int size = gzread(file, version->chars, version->allocated - 1);
|
||||
gzclose(file);
|
||||
int size = ffgzread(file, version->chars, version->allocated - 1);
|
||||
ffgzclose(file);
|
||||
|
||||
if (size <= 0) {
|
||||
FF_DEBUG("gzread(file, version->chars, version->length) failed");
|
||||
return "gzread(file, version->chars, version->length) failed";
|
||||
FF_DEBUG("ffgzread(file, version->chars, version->length) failed");
|
||||
return "ffgzread(file, version->chars, version->length) failed";
|
||||
}
|
||||
|
||||
version->length = (uint32_t) size;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,16 @@
|
||||
#include "common/strutil.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <net/if.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <inttypes.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <linux/ethtool.h>
|
||||
@@ -529,11 +535,6 @@ const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
void ffLocalIpFixRedactedMacs(FFlist* results);
|
||||
ffLocalIpFixRedactedMacs(results);
|
||||
#endif
|
||||
|
||||
FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) {
|
||||
ffListDestroy(&adapter->ipv4);
|
||||
ffListDestroy(&adapter->ipv6);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#include "media.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/android/applabel.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/android/package.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/mallocHelper.h"
|
||||
@@ -33,9 +31,8 @@
|
||||
// service and reaches onTransact, and is then answered with a SecurityException carrying the server's
|
||||
// own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell
|
||||
// UID gets the session list from the same transaction code. Nothing in this file tests the uid: the
|
||||
// service is asked and its answer is believed, so a device whose policy is looser works without a
|
||||
// change here. What the answer does depend on is the ComponentName in the request, which is not a
|
||||
// filter and not decoration either -- see the request build in androidDetectMediaSession().
|
||||
// service is asked and its answer is believed, so a device whose policy is looser, or one where a
|
||||
// notification listener is registered, works without a change here.
|
||||
//
|
||||
// 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
|
||||
@@ -97,21 +94,8 @@
|
||||
// strings; a reply that did not fit would be reported rather than misread.
|
||||
#define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024)
|
||||
|
||||
// ffAndroidGetOwnPackage() writes at most this many bytes, terminator included.
|
||||
#define FF_MEDIA_ANDROID_PACKAGE_SIZE 128
|
||||
|
||||
// The request is the interface token, the ComponentName and the user id. A ComponentName is a
|
||||
// presence marker in front of the package name and the class name, so the two string16s are what
|
||||
// took this past the 128 it used to be. That size had room for a seven-character package name --
|
||||
// `com.termux` with an empty class is already 136 bytes -- so the size is derived from the longest
|
||||
// name that can be written rather than rounded up by hand, because a request that does not fit is
|
||||
// dropped and reported as truncated: that fails the whole detection instead of degrading to a call
|
||||
// without a name. The empty class name costs a 4-byte length, a NUL terminator and the padding, so 8.
|
||||
// clang-format off
|
||||
#define FF_MEDIA_ANDROID_REQUEST_SIZE \
|
||||
(12 + ((4 + 2 * sizeof(FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR) + 3) & ~3u) \
|
||||
+ 4 + ((4 + 2 * FF_MEDIA_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8u + 4u)
|
||||
// clang-format on
|
||||
// An interface token plus, for getSessions, two int32s.
|
||||
#define FF_MEDIA_ANDROID_REQUEST_SIZE 128
|
||||
|
||||
// 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%
|
||||
@@ -810,18 +794,12 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t tr
|
||||
return;
|
||||
}
|
||||
|
||||
// `player` wants the name the app shows rather than the name it is installed under, and the
|
||||
// package name is only the fallback. The label comes out of the APK's own resource table, which
|
||||
// the package manager points at with `sourceDir` and `labelRes`: the table is *stored* rather
|
||||
// than deflated -- the platform expects to mmap it -- so resolving an id out of it needs no
|
||||
// decompressor. What keeps the package name are the apps whose label is a literal string in
|
||||
// their manifest instead of a resource reference, since their reply carries no id at all; that
|
||||
// is also what the notification and the settings screen fall back to. See doc/app-label.md.
|
||||
// `player` wants the name the app shows, and there is no way to get one from here: `cmd package
|
||||
// dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and
|
||||
// resolving it would mean reading the app's resource table. The package name is what the
|
||||
// notification and the settings screen fall back to as well, so both fields get it -- the same
|
||||
// fallback media_linux.c uses when MPRIS answers without an Identity.
|
||||
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) {
|
||||
@@ -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
|
||||
// through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the
|
||||
// answer. That costs one transaction, and it keeps the module working wherever the check happens to
|
||||
// pass, instead of hard-coding today's policy into the client. What the request has to carry for the
|
||||
// check to be consulted at all is the ComponentName: see the request build below.
|
||||
// pass, instead of hard-coding today's policy into the client.
|
||||
static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) {
|
||||
// Resolved before the binder is opened, so that a jar which does not declare the methods says so
|
||||
// rather than the module failing later with a message about the transport. The reader's own
|
||||
@@ -934,38 +911,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
|
||||
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR);
|
||||
|
||||
// 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); // ComponentName: null asks for every session
|
||||
ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has
|
||||
|
||||
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
|
||||
@@ -981,13 +927,11 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
|
||||
if (exception != 0) {
|
||||
// -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The
|
||||
// number goes to the debug log rather than into the message: what tells the two cases apart
|
||||
// for the user is which UID is asking. On an app UID the missing privilege is named,
|
||||
// because it is one the user can grant -- the request below already declares the caller,
|
||||
// so whatever is left is the listener the UID does not own.
|
||||
// for the user is which UID is asking.
|
||||
FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : "");
|
||||
return ffAndroidIsRootOrShell(instance.state.platform.uid)
|
||||
? "The media session service refused the request"
|
||||
: "Reading the media session 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
|
||||
|
||||
@@ -110,9 +110,106 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
|
||||
return error;
|
||||
}
|
||||
|
||||
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
|
||||
const char* error = getMediaByMediaRemote(media, saveCover);
|
||||
#if !FF_MODULE_DISABLE_MEDIA
|
||||
// Called from the Perl helper process through `DynaLoader::dl_install_xsub`. That mechanism installs a
|
||||
// plain C function as an XSUB, so Perl calls it with the `CV*` in the first parameter slot and no Perl
|
||||
// argument ever reaches a parameter — a `bool saveCover` parameter read as a pointer and was always
|
||||
// truthy. The flag has to travel in the environment instead; see `getMediaByAuthorizedProcess`.
|
||||
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(void) {
|
||||
const char* saveCoverEnv = getenv("FF_MEDIA_REMOTE_SAVE_COVER");
|
||||
bool saveCover = saveCoverEnv && saveCoverEnv[0] == '1';
|
||||
|
||||
FFMediaResult media = {
|
||||
.status = ffStrbufCreate(),
|
||||
.song = ffStrbufCreate(),
|
||||
.artist = ffStrbufCreate(),
|
||||
.album = ffStrbufCreate(),
|
||||
.playerId = ffStrbufCreate(),
|
||||
.player = ffStrbufCreate(),
|
||||
.cover = ffStrbufCreate(),
|
||||
};
|
||||
if (getMediaByMediaRemote(&media, saveCover) != nullptr) {
|
||||
return 1;
|
||||
}
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppend(&media.status, &media.song);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppend(&media.status, &media.artist);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppend(&media.status, &media.album);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppend(&media.status, &media.playerId);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppend(&media.status, &media.player);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
if (saveCover) {
|
||||
ffStrbufAppend(&media.status, &media.cover);
|
||||
}
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppendUInt(&media.status, media.position);
|
||||
ffStrbufAppendC(&media.status, '\n');
|
||||
ffStrbufAppendUInt(&media.status, media.length);
|
||||
write(STDOUT_FILENO, media.status.chars, media.status.length);
|
||||
ffStrbufDestroy(&media.status);
|
||||
ffStrbufDestroy(&media.song);
|
||||
ffStrbufDestroy(&media.artist);
|
||||
ffStrbufDestroy(&media.album);
|
||||
ffStrbufDestroy(&media.playerId);
|
||||
ffStrbufDestroy(&media.player);
|
||||
ffStrbufDestroy(&media.cover);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
|
||||
// #1737
|
||||
// `DynaLoader::dl_install_xsub` hands the C function the `CV*` as its first parameter, so the flag
|
||||
// cannot be passed as a Perl argument — `ffPrintMediaByMediaRemote` reads it from the environment,
|
||||
// which the child process inherits.
|
||||
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("$ENV{'FF_MEDIA_REMOTE_SAVE_COVER'}='%c';\
|
||||
require DynaLoader;\
|
||||
my $so = DynaLoader::dl_load_file('%s', 0);\
|
||||
my $sym = DynaLoader::dl_find_symbol($so, 'ffPrintMediaByMediaRemote');\
|
||||
DynaLoader::dl_install_xsub('fn', $sym, __FILE__);\
|
||||
exit(fn())",
|
||||
saveCover ? '1' : '0',
|
||||
instance.state.platform.exePath.chars);
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
|
||||
const char* error = ffProcessAppendStdOut(
|
||||
&buffer,
|
||||
(char* const[]) { "/usr/bin/perl", // Must be signed by Apple. Homebrew version doesn't work
|
||||
"-e",
|
||||
script.chars,
|
||||
nil });
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
if (buffer.length == 0) {
|
||||
return "No media found";
|
||||
}
|
||||
|
||||
// status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS
|
||||
FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
|
||||
char* line = nullptr;
|
||||
size_t len = 0;
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
|
||||
ffStrbufSetS(strList[i], line);
|
||||
}
|
||||
uint32_t* const numList[] = { &result->position, &result->length };
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
|
||||
*numList[i] = (uint32_t) strtoul(line, nullptr, 10);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
|
||||
const char* error;
|
||||
if (@available(macOS 15.4, *)) {
|
||||
error = getMediaByAuthorizedProcess(media, saveCover);
|
||||
} else {
|
||||
error = getMediaByMediaRemote(media, saveCover);
|
||||
}
|
||||
if (error) {
|
||||
ffStrbufAppendS(&media->error, error);
|
||||
} else {
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
#include <string.h>
|
||||
|
||||
#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,
|
||||
// which a plain NDK build does not have. The block below includes <GL/gl.h>, so that is the
|
||||
// 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>)
|
||||
// On Android, installing OpenGL headers is enough (mesa-dev)
|
||||
#if __has_include(<EGL/egl.h>)
|
||||
#define FF_HAVE_EGL 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -108,11 +108,6 @@ uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) {
|
||||
struct linux_dirent64* entry = (struct linux_dirent64*) bytes;
|
||||
|
||||
while (remaining >= nameOffset + 1) {
|
||||
if (entry->d_reclen < nameOffset + 1 || entry->d_reclen > remaining) {
|
||||
remaining = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (entry->d_name[0] != '.') {
|
||||
if (__builtin_expect(entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK, true)) {
|
||||
|
||||
@@ -46,14 +46,6 @@ void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options)
|
||||
|
||||
result->macports = getMacPortsPackages(&baseDir);
|
||||
}
|
||||
if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) {
|
||||
const char* prefix = getenv("PKG_DBDIR");
|
||||
if (ffStrSet(prefix)) {
|
||||
result->pkgsrc = ffPackagesGetNumElements(prefix, true);
|
||||
} else {
|
||||
result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true);
|
||||
}
|
||||
}
|
||||
if (FF_PACKAGES_IS_ENABLED(options, NIX)) {
|
||||
ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT);
|
||||
result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default");
|
||||
|
||||
@@ -104,11 +104,11 @@ typedef enum FFSoundAndroidUsage : int32_t {
|
||||
#define FF_SOUND_ANDROID_NAME_REPLY_SIZE 512
|
||||
|
||||
// `AudioDeviceInfo.TYPE_*`, as the test device's own dex declares them (read out of
|
||||
// `Landroid/media/AudioDeviceInfo;` -- most recently on Android 17, which had added four). The
|
||||
// value is what `AudioDeviceAttributes.type` carries in the reply; the names are the AOSP constant
|
||||
// names without the prefix, which is what goes into `identifier`, and a spelled out name, which is
|
||||
// what the module prints. An unrecognised value is reported rather than guessed at: a vendor is
|
||||
// free to add types, and a wrong name is worse than "Unknown Device".
|
||||
// `Landroid/media/AudioDeviceInfo;`). The value is what `AudioDeviceAttributes.type` carries in the
|
||||
// reply; the names are the AOSP constant names without the prefix, which is what goes into
|
||||
// `identifier`, and a spelled out name, which is what the module prints. An unrecognised value is
|
||||
// reported rather than guessed at: a vendor is free to add types, and a wrong name is worse than
|
||||
// "Unknown Device".
|
||||
typedef enum FFSoundAndroidDeviceType : int32_t {
|
||||
FF_SOUND_ANDROID_TYPE_UNKNOWN = 0,
|
||||
FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE = 1,
|
||||
@@ -143,11 +143,6 @@ typedef enum FFSoundAndroidDeviceType : int32_t {
|
||||
FF_SOUND_ANDROID_TYPE_BLE_BROADCAST = 30,
|
||||
FF_SOUND_ANDROID_TYPE_DOCK_ANALOG = 31,
|
||||
FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP = 32,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID = 33,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_CENTRAL = 34,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST = 35,
|
||||
// 36 to 79 have no constant in the Android 17 release, and 80 is the highest it declares.
|
||||
FF_SOUND_ANDROID_TYPE_PROXY = 80,
|
||||
} FFSoundAndroidDeviceType;
|
||||
|
||||
typedef struct FFSoundAndroidDeviceName {
|
||||
@@ -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_DOCK_ANALOG, "DOCK_ANALOG", "Dock (Analog)" },
|
||||
{ FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP, "MULTICHANNEL_GROUP", "Multichannel Group" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID, "BLE_HEARING_AID", "Bluetooth LE Hearing Aid" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_CENTRAL, "BLE_CENTRAL", "Bluetooth LE Central" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_CENTRAL_BROADCAST, "BLE_CENTRAL_BROADCAST", "Bluetooth LE Central Broadcast" },
|
||||
{ FF_SOUND_ANDROID_TYPE_PROXY, "PROXY", "Proxy" },
|
||||
};
|
||||
|
||||
static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) {
|
||||
@@ -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
|
||||
// service for: everything else is described well enough by its type, and the request is one more
|
||||
// round trip to system_server. A hearing aid is on the list because it is a Bluetooth LE link with
|
||||
// a name of its own, the same as a headset. The other three types Android 17 added stay off it:
|
||||
// BLE_CENTRAL and BLE_CENTRAL_BROADCAST name an audio path inside this device while it operates in
|
||||
// a Bluetooth peripheral mode rather than a device on the far end of a link, and PROXY is a logical
|
||||
// endpoint rather than a link at all.
|
||||
// a name of its own, the same as a headset.
|
||||
static bool soundAndroidIsBluetooth(int32_t type) {
|
||||
switch ((FFSoundAndroidDeviceType) type) {
|
||||
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO:
|
||||
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP:
|
||||
case FF_SOUND_ANDROID_TYPE_HEARING_AID:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_HEARING_AID:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_HEADSET:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_SPEAKER:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_BROADCAST:
|
||||
|
||||
@@ -14,17 +14,7 @@
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Linux sound comes from one of two places: the sound server a desktop runs, and the hardware
|
||||
// underneath it.
|
||||
//
|
||||
// The server is PulseAudio -- PipeWire speaks its protocol too -- and it is asked first, because
|
||||
// it is the only one of the two that knows what the desktop's volume is: the mixer the card
|
||||
// exposes and the slider on screen are not the same thing. When no server answers, the cards are
|
||||
// read from ALSA instead; that part lives in its own section at the end of this file.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// the server: PulseAudio's native protocol ("protocol-native")
|
||||
// ---------------------------------------------------------------------------
|
||||
// PulseAudio's native protocol ("protocol-native").
|
||||
//
|
||||
// Instead of linking or dlopen-ing libpulse, fastfetch talks to the daemon
|
||||
// directly: the daemon speaks a small length prefixed binary protocol on its
|
||||
@@ -37,9 +27,9 @@
|
||||
// of control packets always uses the channel 0xFFFFFFFF with a zero offset and
|
||||
// flag word.
|
||||
//
|
||||
// The command numbers, the tag characters and the field orders below are the ones in pulseaudio's
|
||||
// own sources -- pulsecore/native-common.h, pulsecore/tagstruct.{c,h} and
|
||||
// pulsecore/protocol-native.c -- and the place each of them was read from is named where it is used.
|
||||
// See doc/pulseaudio-native-protocol.md for the byte level details, for the
|
||||
// command numbers and for the pulseaudio source references they were derived
|
||||
// from.
|
||||
|
||||
typedef enum FFSoundPulseCommand : uint32_t {
|
||||
FF_SOUND_PA_COMMAND_ERROR = 0,
|
||||
@@ -1096,520 +1086,19 @@ static const char* fetchSinkList(FFSoundPulseConn* conn, const FFSoundOptions* o
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the hardware: ALSA
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A machine that runs no sound server still has ALSA, and this is what answers for it: the cards
|
||||
// are enumerated from /proc and their volumes read from the control device of each card.
|
||||
//
|
||||
// Nothing is linked and nothing is loaded. The values are behind four ioctls of the kernel's own
|
||||
// control interface, and both the structs and the request numbers are the kernel's uapi, which
|
||||
// every libc ships as <sound/asound.h>. The request numbers are `_IOWR`-style, so they carry the
|
||||
// size of the struct they pass -- a layout that does not match the kernel's is refused as an
|
||||
// unknown ioctl instead of being read as garbage.
|
||||
//
|
||||
// Android is left out: it has no ALSA of the kind this reads ( /dev/snd and /proc/asound are
|
||||
// closed to an app's SELinux domain ) and a session that does have a desktop on top of the device
|
||||
// is running a sound server, which is the branch below. A libc that does not carry the header says
|
||||
// so here, and the fallback is simply not compiled in -- the same shape as the other optional
|
||||
// backends in this tree.
|
||||
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<sound/asound.h>)
|
||||
#define FF_SOUND_ALSA 1
|
||||
// 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
|
||||
|
||||
#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) {
|
||||
(FFSoundOptions* options, FFlist* devices) {
|
||||
FF_STRBUF_AUTO_DESTROY defaultSocketPath = ffStrbufCreate();
|
||||
getDefaultSocketPath(&defaultSocketPath);
|
||||
FF_DEBUG("Default pulseaudio socket path: %s", defaultSocketPath.chars);
|
||||
@@ -1687,41 +1176,3 @@ static const char* detectPulseAudio(FFSoundOptions* options, FFlist* devices) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
if (!hdl) {
|
||||
FF_DEBUG("sio_open() failed");
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
#include "terminalshell.h"
|
||||
#include "common/FFcache.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/parsing.h"
|
||||
#include "common/processing.h"
|
||||
#include "common/thread.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "common/android/applabel.h"
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
@@ -329,18 +324,8 @@ static void setTerminalInfoDetails(FFTerminalResult* result) {
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
|
||||
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.
|
||||
else if (ffStrbufEqualS(&result->processName, "com.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__)
|
||||
|
||||
|
||||
@@ -109,18 +109,17 @@
|
||||
// more accurate -- pays nothing for it.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// that Android 11 and 10 wrote in front of the SSID octets, and the Android 17 device writes a
|
||||
// word of its own between the link speed and the transmit speed that the Android 11 and 13
|
||||
// devices leave out -- a vendor addition rather than a release difference, since AOSP's own
|
||||
// Android 16 writes the head the Android 11 way. Frequency is the only channel centre in the
|
||||
// head, so it is located by value, with the whole result validated before it is used, and the
|
||||
// two direction specific speeds are read as its neighbours. A wrong guess shows up as a
|
||||
// rejected layout, not as a plausible wrong number.
|
||||
// that Android 11 and 10 wrote in front of the SSID octets, and the Android 16 device fills an
|
||||
// int between the transmit and the receive speed that the Android 11 device leaves out -- while
|
||||
// AOSP's own Android 16 writes the head exactly the Android 11 way, so that one int is a vendor
|
||||
// addition rather than a release difference. Frequency is the only channel centre in the head, so
|
||||
// it is located by value, with the whole result validated before it is used. A wrong guess shows
|
||||
// up as a rejected layout, not as a plausible wrong number.
|
||||
// * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every
|
||||
// vendor: the Android 11 device carries three words there that AOSP's own Android 11
|
||||
// does not. It is located by shape for that reason.
|
||||
@@ -172,14 +171,13 @@ typedef enum FFWifiAndroidRadioState : int32_t
|
||||
} FFWifiAndroidRadioState;
|
||||
|
||||
// An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker;
|
||||
// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed are the first three
|
||||
// fields `writeToParcel` writes, so nothing a release or a vendor adds behind them can move them --
|
||||
// they have held those offsets from Android 9 to 17 and are read there. The two direction specific
|
||||
// speeds sit behind them but are located from the frequency instead, because a vendor does write a
|
||||
// word of its own in front of that pair: see the note at FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED.
|
||||
// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed have held those
|
||||
// offsets from Android 9 to 16, and the transmit speed joined them in Android 10 without moving
|
||||
// since, so all four are read at fixed offsets. Everything after them is located by shape.
|
||||
#define FF_WIFI_ANDROID_OFF_NET_ID 8
|
||||
#define FF_WIFI_ANDROID_OFF_RSSI 12
|
||||
#define FF_WIFI_ANDROID_OFF_LINK_SPEED 16
|
||||
#define FF_WIFI_ANDROID_OFF_TX_LINK_SPEED 20
|
||||
|
||||
// A `WifiInfo` with no connection to describe carries the sentinels for the two fields that are
|
||||
// always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot
|
||||
@@ -208,15 +206,11 @@ typedef enum FFWifiAndroidRadioState : int32_t
|
||||
#define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08
|
||||
#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:
|
||||
// `mTxLinkSpeed, mRxLinkSpeed, mFrequency` are written back to back by every head this file has
|
||||
// been checked against, from Android 11 through 17. The pair is read from Frequency rather than
|
||||
// stepped to from the link speed, because the Android 17 device writes a word of its own between
|
||||
// the link speed and the transmit speed: its dex names that word `mCurrentLinkSpeed`, so the word
|
||||
// after the link speed is not the transmit speed there and the pair sits one word further along.
|
||||
// Both were checked against `cmd wifi status` at the same moment: 131/219 on the Android 16 device,
|
||||
// 86/-1 on the Android 11 device.
|
||||
#define FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED 0x08
|
||||
// The receive speed is the word in front of Frequency; the transmit speed is the word after the link
|
||||
// speed. The Android 16 device writes one extra word between those two that AOSP does not -- it repeats the
|
||||
// transmit speed -- so the receive speed can only be reached from Frequency, and Frequency cannot be
|
||||
// reached by stepping over the two link speeds. Both were checked against `cmd wifi status` at the
|
||||
// same moment: 131/219 on the Android 16 device, 86/-1 on the Android 11 device.
|
||||
#define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04
|
||||
#define FF_WIFI_ANDROID_SSID 0x08 // Android 12 and later
|
||||
#define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older
|
||||
@@ -649,7 +643,7 @@ static void debugDumpReply(const uint8_t* data, size_t size) {
|
||||
#endif
|
||||
|
||||
static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
|
||||
if (size < FF_WIFI_ANDROID_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);
|
||||
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);
|
||||
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,
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED));
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED));
|
||||
|
||||
// The two orders WifiSsid has written its length in: once before the octets on Android 11 and
|
||||
// older, not at all on Android 12 and later, which leaves the octets one word earlier. The one
|
||||
@@ -760,7 +755,7 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA
|
||||
connection->frequency = frequency;
|
||||
connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
|
||||
connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
|
||||
connection->txLinkSpeed = ffBinderReadI32(data, size, 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);
|
||||
|
||||
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)) {
|
||||
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",
|
||||
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->txLinkSpeed,
|
||||
connection->rxLinkSpeed, connection->connected ? "yes" : "no");
|
||||
FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s",
|
||||
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed,
|
||||
connection->connected ? "yes" : "no");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
+13
-9
@@ -489,7 +489,6 @@ static bool printImageKittyDirect(bool printError) {
|
||||
#include <fcntl.h>
|
||||
|
||||
#include "common/time.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/ioctl.h>
|
||||
@@ -499,14 +498,19 @@ static bool printImageKittyDirect(bool printError) {
|
||||
#endif
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include "common/zlib.h"
|
||||
#include <zlib.h>
|
||||
|
||||
static bool compressBlob(void** blob, size_t* length) {
|
||||
const char* error = ffZlibLoad(FF_ZLIB_COMPRESS);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The logo stays uncompressed: %s", error);
|
||||
return false;
|
||||
}
|
||||
FF_LIBRARY_LOAD(zlib, false,
|
||||
#ifdef _WIN32
|
||||
"zlib1"
|
||||
#else
|
||||
"libz"
|
||||
#endif
|
||||
FF_LIBRARY_EXTENSION,
|
||||
2)
|
||||
FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false)
|
||||
FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false)
|
||||
|
||||
#if _WIN32
|
||||
// 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
|
||||
|
||||
uLong compressedLength = compressBound((uLong) *length);
|
||||
uLong compressedLength = ffcompressBound((uLong) *length);
|
||||
void* compressed = malloc(compressedLength);
|
||||
if (compressed == nullptr) {
|
||||
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
|
||||
// 6.4 at level 6: raising this back to 9 now that the DLL name above actually loads is a 4.2x
|
||||
// slowdown there (398 ms -> 1692 ms). Re-measure both platforms before touching it.
|
||||
if (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);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = {
|
||||
.pl = "Adapter Bluetooth",
|
||||
.pt = "Adaptador Bluetooth",
|
||||
.ru = "Bluetooth-адаптер",
|
||||
.tr = "Bluetooth Adaptörü",
|
||||
.tr = "Bluetooth Radyosu",
|
||||
.uk = "Радіомодуль Bluetooth",
|
||||
.vi = "Radio Bluetooth",
|
||||
.zh_CN = "蓝牙适配器",
|
||||
|
||||
@@ -85,24 +85,7 @@ void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) {
|
||||
}
|
||||
|
||||
if (unsafe_yyjson_equals_str(key, "useStdErr")) {
|
||||
options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT;
|
||||
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unsafe_yyjson_equals_str(key, "useOutput")) {
|
||||
int value;
|
||||
const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
|
||||
{ "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT },
|
||||
{ "stderr", FF_PROCESS_OUTPUT_STDERR_BIT },
|
||||
{ "both", FF_PROCESS_OUTPUT_BOTH },
|
||||
{},
|
||||
});
|
||||
if (error) {
|
||||
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
|
||||
} else {
|
||||
options->useOutput = (FFProcessOutputType) value;
|
||||
}
|
||||
options->useStdErr = yyjson_get_bool(val);
|
||||
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, "param", &options->param);
|
||||
yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text);
|
||||
switch (options->useOutput) {
|
||||
case FF_PROCESS_OUTPUT_STDOUT_BIT:
|
||||
yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout");
|
||||
break;
|
||||
case FF_PROCESS_OUTPUT_STDERR_BIT:
|
||||
yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr");
|
||||
break;
|
||||
case FF_PROCESS_OUTPUT_BOTH:
|
||||
yyjson_mut_obj_add_str(doc, module, "useOutput", "both");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
yyjson_mut_obj_add_bool(doc, module, "useStdErr", options->useStdErr);
|
||||
yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel);
|
||||
yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines);
|
||||
}
|
||||
@@ -191,15 +162,13 @@ void ffInitCommandOptions(FFCommandOptions* options) {
|
||||
);
|
||||
ffStrbufInitStatic(&options->param,
|
||||
#ifdef _WIN32
|
||||
"/d/c"
|
||||
"/c"
|
||||
#else
|
||||
"-c"
|
||||
#endif
|
||||
);
|
||||
ffStrbufInit(&options->text);
|
||||
// Capture both streams by default: most tools log to stdout or stderr without saying which, and
|
||||
// the previous single-stream default silently dropped one of them.
|
||||
options->useOutput = FF_PROCESS_OUTPUT_BOTH;
|
||||
options->useStdErr = false;
|
||||
options->parallel = true;
|
||||
options->splitLines = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/option.h"
|
||||
#include "common/processing.h" // FFProcessOutputType
|
||||
|
||||
typedef struct FFCommandOptions {
|
||||
FFModuleArgs moduleArgs;
|
||||
@@ -9,7 +8,7 @@ typedef struct FFCommandOptions {
|
||||
FFstrbuf shell;
|
||||
FFstrbuf param;
|
||||
FFstrbuf text;
|
||||
FFProcessOutputType useOutput;
|
||||
bool useStdErr;
|
||||
bool parallel;
|
||||
bool splitLines;
|
||||
} FFCommandOptions;
|
||||
|
||||
+12
-11
@@ -15,8 +15,6 @@
|
||||
static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index) {
|
||||
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
|
||||
|
||||
uint64_t bytesUsed = disk->bytesTotal - disk->bytesFree;
|
||||
|
||||
if (options->moduleArgs.key.length == 0) {
|
||||
ffStrbufSetF(&key, "%s (%s)", FF_MODULE_GET_DISPLAY_NAME(Disk), disk->mountpoint.chars);
|
||||
} else {
|
||||
@@ -52,12 +50,12 @@ static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index
|
||||
}
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
|
||||
ffSizeAppendNum(bytesUsed, &usedPretty);
|
||||
ffSizeAppendNum(disk->bytesUsed, &usedPretty);
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
|
||||
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;
|
||||
|
||||
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();
|
||||
ffSizeAppendNum(disk->bytesFree, &freePretty);
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY availPretty = ffStrbufCreate();
|
||||
ffSizeAppendNum(disk->bytesAvailable, &availPretty);
|
||||
|
||||
uint64_t now = ffTimeGetNow();
|
||||
uint64_t duration = now - disk->createTime;
|
||||
uint32_t milliseconds = (uint32_t) (duration % 1000);
|
||||
@@ -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.yearsFraction, "years-fraction"),
|
||||
FF_ARG(freePretty, "size-free"),
|
||||
FF_ARG(bytesUsed, "bytes-used"),
|
||||
FF_ARG(disk->bytesTotal, "bytes-total"),
|
||||
FF_ARG(availPretty, "size-available"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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* bytes = yyjson_mut_obj_add_obj(doc, obj, "bytes");
|
||||
yyjson_mut_obj_add_uint(doc, bytes, "available", item->bytesAvailable);
|
||||
yyjson_mut_obj_add_uint(doc, bytes, "free", item->bytesFree);
|
||||
yyjson_mut_obj_add_uint(doc, bytes, "total", item->bytesTotal);
|
||||
yyjson_mut_obj_add_uint(doc, bytes, "used", item->bytesUsed);
|
||||
|
||||
yyjson_mut_val* files = yyjson_mut_obj_add_obj(doc, obj, "files");
|
||||
if (item->filesTotal == 0 && item->filesUsed == 0) {
|
||||
@@ -485,8 +487,8 @@ FFModuleBaseInfo ffDiskModuleInfo = {
|
||||
.generateJsonResult = (void*) ffGenerateDiskJsonResult,
|
||||
.generateJsonConfig = (void*) ffGenerateDiskJsonConfig,
|
||||
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
|
||||
{ "Size used (formatted)", "size-used" },
|
||||
{ "Size total (formatted)", "size-total" },
|
||||
{ "Size used", "size-used" },
|
||||
{ "Size total", "size-total" },
|
||||
{ "Size percentage num", "size-percentage" },
|
||||
{ "Files used", "files-used" },
|
||||
{ "Files total", "files-total" },
|
||||
@@ -509,9 +511,8 @@ FFModuleBaseInfo ffDiskModuleInfo = {
|
||||
{ "Years integer after creation", "years" },
|
||||
{ "Days of year after creation", "days-of-year" },
|
||||
{ "Years fraction after creation", "years-fraction" },
|
||||
{ "Size free (formatted)", "size-free" },
|
||||
{ "Size used (in bytes)", "bytes-used" },
|
||||
{ "Size total (in bytes)", "bytes-total" },
|
||||
{ "Size free", "size-free" },
|
||||
{ "Size available", "size-available" },
|
||||
})),
|
||||
.defaultOrder = 42,
|
||||
};
|
||||
|
||||
@@ -156,7 +156,7 @@ FFModuleBaseInfo ffHostModuleInfo = {
|
||||
.pl = "Host",
|
||||
.pt = "Host",
|
||||
.ru = "Модель",
|
||||
.tr = "Cihaz",
|
||||
.tr = "Ana Bilgisayar",
|
||||
.uk = "Хост",
|
||||
.vi = "Máy chủ",
|
||||
.zh_CN = "主机",
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ FFModuleBaseInfo ffLMModuleInfo = {
|
||||
.pl = "Menedżer logowania",
|
||||
.pt = "Gerenciador de login",
|
||||
.ru = "Менеджер входа",
|
||||
.tr = "Giriş Yöneticisi",
|
||||
.tr = "Oturum Açma Yöneticisi",
|
||||
.uk = "Менеджер входу",
|
||||
.vi = "Trình quản lý đăng nhập",
|
||||
.zh_CN = "登录管理器",
|
||||
|
||||
@@ -283,7 +283,7 @@ FFModuleBaseInfo ffTopModuleInfo = {
|
||||
.pl = "Najaktywniejsze procesy",
|
||||
.pt = "Processos principais",
|
||||
.ru = "Топ процессов",
|
||||
.tr = "En Yoğun Süreçler",
|
||||
.tr = "En yoğun işlemler",
|
||||
.uk = "Найактивніші процеси",
|
||||
.vi = "Tiến trình hàng đầu",
|
||||
.zh_CN = "进程排行",
|
||||
|
||||
@@ -1060,7 +1060,6 @@ static const char* optionGetLanguageString(uint32_t offset) {
|
||||
case offsetof(FFModuleDisplayName, cs): return "cs";
|
||||
case offsetof(FFModuleDisplayName, de): return "de";
|
||||
case offsetof(FFModuleDisplayName, es): return "es";
|
||||
case offsetof(FFModuleDisplayName, fi): return "fi";
|
||||
case offsetof(FFModuleDisplayName, fr): return "fr";
|
||||
case offsetof(FFModuleDisplayName, gl): return "gl";
|
||||
case offsetof(FFModuleDisplayName, he): return "he";
|
||||
|
||||
-201
@@ -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
|
||||
@@ -180,17 +180,6 @@ int main(void) {
|
||||
VERIFY_BAR(14, config, "[ ■--------- ]");
|
||||
VERIFY_BAR(15, config, "[ ■■-------- ]");
|
||||
|
||||
// A value above 100 saturates the bar. That is reachable from real data: the load average is
|
||||
// normalized against the number of cores, so it exceeds 100% as soon as the load does.
|
||||
VERIFY_BAR(101, config, "[ ■■■■■■■■■■ ]");
|
||||
VERIFY_BAR(150, config, "[ ■■■■■■■■■■ ]");
|
||||
|
||||
// Narrowing to `uint8_t` before clamping would wrap around here and silently render 4 blocks
|
||||
VERIFY_BAR(2600, config, "[ ■■■■■■■■■■ ]");
|
||||
|
||||
// A negative value fills nothing
|
||||
VERIFY_BAR(-60, config, "[ ---------- ]");
|
||||
|
||||
options->barWidth = 4;
|
||||
VERIFY_BAR(0, config, "[ ---- ]");
|
||||
VERIFY_BAR(25, config, "[ ■--- ]");
|
||||
|
||||
Reference in New Issue
Block a user