mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-27 23:30:47 +01:00
Media (Android): adds app uid support
This commit is contained in:
@@ -14,6 +14,8 @@ 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
|
||||
* Performance improvements & internal cleanups
|
||||
* Added Finnish language support. (General)
|
||||
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
|
||||
|
||||
@@ -678,9 +678,11 @@ if(LINUX)
|
||||
)
|
||||
elseif(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/android/applabel.c
|
||||
src/common/android/binder.c
|
||||
src/common/android/dex.c
|
||||
src/common/android/package.c
|
||||
src/common/android/zip.c
|
||||
src/common/impl/dbus.c
|
||||
src/common/impl/io_unix.c
|
||||
src/common/impl/netif_linux.c
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
#include "common/android/applabel.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/android/zip.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// The name an app presents to the user, which is not its package name: an app installed as
|
||||
// `com.termux` presents itself as `Termux:Launcher` on the launcher, in its notifications and on
|
||||
// the settings screen.
|
||||
//
|
||||
// Every route that answers with an *already resolved* label is closed to an app UID, and each one
|
||||
// was measured on the test device rather than assumed:
|
||||
//
|
||||
// * `dumpsys package` needs android.permission.DUMP.
|
||||
// * `cmd package dump` is filtered down to 929 bytes for an app UID, against 1.2 MB for the shell.
|
||||
// * The task descriptions the activity service hands out only ever describe the caller's own task.
|
||||
// * `getApplicationInfo` answers with `labelRes` -- a resource id -- and leaves `nonLocalizedLabel`
|
||||
// null, so there is no string in the reply to read.
|
||||
//
|
||||
// What is open is the APK itself. `getApplicationInfo` carries its path (`sourceDir`), APKs are
|
||||
// world readable, and the `resources.arsc` inside one is *stored* rather than deflated -- the
|
||||
// platform expects to mmap it -- so resolving an id out of it needs no decompressor. That is the
|
||||
// route this takes: one transaction, one mapping, no fork and no child process.
|
||||
//
|
||||
// The label is the resource id in the reply whose type is `string`. That is a rule about the reply
|
||||
// rather than about the parcel's field order, and it was audited against the manifest's own
|
||||
// `android:label` over every package installed on the test device: nine in ten have exactly one
|
||||
// `string` candidate and it is the right one every time, a handful more have two and the first is
|
||||
// still the right one, and the rest have none because their label is a literal string rather than a
|
||||
// reference (`android:label="Example"`), so the reply carries no id at all. No package resolved to
|
||||
// a wrong string; the counts behind that are in doc/app-label.md.
|
||||
//
|
||||
// Writes the label into `result` when it can be resolved, and leaves `result` alone otherwise --
|
||||
// which includes the cases above where there is nothing to resolve. The caller sets its fallback
|
||||
// first, so that an unresolved label costs nothing but the query.
|
||||
//
|
||||
// Returns nullptr on success, a static message when the query itself could not be made. An error is
|
||||
// not a fallback: it says the label was not looked for, not that the app has none.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result);
|
||||
+13
-125
@@ -1,4 +1,5 @@
|
||||
#include "common/android/dex.h"
|
||||
#include "common/android/zip.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/mallocHelper.h"
|
||||
@@ -54,43 +55,6 @@
|
||||
#define FF_DEX_OP_FIELD_FIRST 0x52
|
||||
#define FF_DEX_OP_FIELD_LAST 0x5F
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7. Only the two length fields are read from it:
|
||||
// the payload starts right behind them, and the local extra field is allowed to differ from the
|
||||
// central one, so the offset cannot be computed from the directory alone.
|
||||
#define FF_ZIP_LOCAL_HEADER_SIZE 30
|
||||
#define FF_ZIP_OFF_NAME_LENGTH 26
|
||||
#define FF_ZIP_OFF_EXTRA_LENGTH 28
|
||||
|
||||
// The central directory. This is what makes an entry reachable without walking everything in front
|
||||
// of it, and that matters here: framework.jar is 52 MB and its classes2.dex starts 9.6 MB in, so
|
||||
// looking for a local header by scanning the file costs a pass over everything before it -- once per
|
||||
// lookup. Measured on the test device, where the sound module resolves seven codes, that was the
|
||||
// difference between 30 ms and 2 seconds.
|
||||
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
|
||||
#define FF_ZIP_CENTRAL_SIZE 46
|
||||
#define FF_ZIP_OFF_CENTRAL_METHOD 10
|
||||
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
|
||||
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
|
||||
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
|
||||
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
|
||||
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
|
||||
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
|
||||
|
||||
// The end of central directory record, which points at the directory. It is the last thing in the
|
||||
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
|
||||
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
|
||||
#define FF_ZIP_EOCD_SIZE 22
|
||||
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
|
||||
|
||||
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
|
||||
// are orders of magnitude below that, and carrying the extra field reader for them is not worth it.
|
||||
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
|
||||
|
||||
#define FF_ZIP_METHOD_STORED 0
|
||||
#define FF_ZIP_METHOD_DEFLATED 8
|
||||
|
||||
#define FF_DEX_ENTRY "classes.dex"
|
||||
// How many `classesN.dex` entries a jar is walked for. A jar that spreads its classes over more
|
||||
// than this still resolves everything in the entries that were walked; only a class that lives in
|
||||
@@ -879,96 +843,20 @@ static const char* dexMapJar(const char* jarPath, FFDexMapping* mapping) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Locates an entry through the jar's central directory.
|
||||
//
|
||||
// The sizes and the compression method are read from the directory rather than from the entry's own
|
||||
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
|
||||
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
|
||||
// choose that. Only the local header's two length fields are used, because they are what says where
|
||||
// the payload begins -- the local extra field does not have to match the central one.
|
||||
// Locates `classesN.dex` in the jar's central directory. The walk that finds an entry is the same
|
||||
// one the app label reader needs for an APK's resource table, so it lives in common/android/zip.h
|
||||
// and this is the dex-specific half of the question: which name to ask for.
|
||||
static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const char* entry, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) {
|
||||
const size_t entryLength = strlen(entry);
|
||||
if (jarSize < FF_ZIP_EOCD_SIZE) {
|
||||
return "The jar is too small to be a zip";
|
||||
FFAndroidZipEntry found = {};
|
||||
const char* error = ffAndroidZipFindEntry(jar, jarSize, entry, &found);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// The record is the last thing in the file, so it is searched for backwards -- over at most the
|
||||
// 64 KiB a trailing comment can take, not over the whole archive.
|
||||
const size_t newest = jarSize - FF_ZIP_EOCD_SIZE;
|
||||
const size_t oldest = jarSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
|
||||
? jarSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
|
||||
: 0;
|
||||
size_t eocd = SIZE_MAX;
|
||||
for (size_t i = newest;; --i) {
|
||||
if (memcmp(jar + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
|
||||
eocd = i;
|
||||
break;
|
||||
}
|
||||
if (i == oldest) {
|
||||
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
|
||||
// before it stops would wrap and read the whole file backwards, which is the very scan
|
||||
// this is here to avoid.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eocd == SIZE_MAX) {
|
||||
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte jar",
|
||||
newest - oldest + FF_ZIP_EOCD_SIZE, jarSize);
|
||||
return "The jar has no zip directory";
|
||||
}
|
||||
|
||||
const uint32_t directorySize = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
|
||||
const uint32_t directoryOffset = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
|
||||
if (directoryOffset > jarSize || directorySize > jarSize - directoryOffset) {
|
||||
return "The zip directory is out of range";
|
||||
}
|
||||
|
||||
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
|
||||
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
|
||||
if (memcmp(jar + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
|
||||
return "A zip directory entry is malformed";
|
||||
}
|
||||
const uint16_t nameLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
|
||||
const uint16_t extraLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
|
||||
const uint16_t commentLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
|
||||
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
|
||||
if (entrySize > directoryEnd - offset) {
|
||||
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
|
||||
entrySize, (size_t) (directoryEnd - offset));
|
||||
return "A zip directory entry runs past the directory";
|
||||
}
|
||||
|
||||
if (nameLength == entryLength && memcmp(jar + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
|
||||
const uint32_t compressedSize = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
|
||||
const uint32_t uncompressed = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
|
||||
const uint32_t localOffset = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
|
||||
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressed == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
|
||||
FF_DEBUG("The dex entry is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
|
||||
compressedSize, uncompressed, localOffset);
|
||||
return "A dex entry is stored as zip64, which is not supported";
|
||||
}
|
||||
if (localOffset > jarSize || FF_ZIP_LOCAL_HEADER_SIZE > jarSize - localOffset) {
|
||||
return "A zip local header is out of range";
|
||||
}
|
||||
|
||||
const uint16_t localNameLength = dexU16(jar + localOffset + FF_ZIP_OFF_NAME_LENGTH);
|
||||
const uint16_t localExtraLength = dexU16(jar + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
|
||||
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
|
||||
if (headerSize > jarSize - localOffset || compressedSize > jarSize - localOffset - headerSize) {
|
||||
return "The zip entry bounds are out of range";
|
||||
}
|
||||
|
||||
*method = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_METHOD);
|
||||
*uncompressedSize = uncompressed;
|
||||
*data = jar + localOffset + headerSize;
|
||||
*dataSize = compressedSize;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
}
|
||||
FF_DEBUG("No entry in the %zu byte jar is named %s", jarSize, entry);
|
||||
return "The jar has no usable dex entry";
|
||||
*data = found.payload;
|
||||
*dataSize = found.payloadSize;
|
||||
*uncompressedSize = found.uncompressedSize;
|
||||
*method = found.method;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Fallback for a jar whose entry is STORED but whose header has no sizes: the dex magic is then a
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#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";
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Locating an entry inside a zip, which is what both the jar reader and the app label reader need:
|
||||
// one looks for `classesN.dex`, the other for the resource table of an APK.
|
||||
//
|
||||
// The entry is found through the central directory rather than by scanning for its local header,
|
||||
// and that is not a micro-optimisation. A local header is preceded by everything that came before
|
||||
// it, so scanning costs a pass over all of it, once per lookup: `framework.jar` is 52 MB and its
|
||||
// `classes2.dex` starts 9.6 MB in. Measured on the test device, where the sound module resolves
|
||||
// seven codes, looking the entries up by scanning was the difference between 30 ms and 2 seconds.
|
||||
//
|
||||
// The sizes and the compression method are read from the directory rather than from the entry's own
|
||||
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
|
||||
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
|
||||
// choose that. Only the local header's two length fields are used, because they are what says where
|
||||
// the payload begins -- the local extra field does not have to match the central one.
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7.
|
||||
#define FF_ZIP_LOCAL_HEADER_SIZE 30
|
||||
#define FF_ZIP_OFF_NAME_LENGTH 26
|
||||
#define FF_ZIP_OFF_EXTRA_LENGTH 28
|
||||
|
||||
// The central directory.
|
||||
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
|
||||
#define FF_ZIP_CENTRAL_SIZE 46
|
||||
#define FF_ZIP_OFF_CENTRAL_METHOD 10
|
||||
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
|
||||
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
|
||||
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
|
||||
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
|
||||
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
|
||||
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
|
||||
|
||||
// The end of central directory record, which points at the directory. It is the last thing in the
|
||||
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
|
||||
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
|
||||
#define FF_ZIP_EOCD_SIZE 22
|
||||
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
|
||||
|
||||
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
|
||||
// and the APKs measured so far are orders of magnitude below that, and carrying the extra field
|
||||
// reader for them is not worth it.
|
||||
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
|
||||
|
||||
#define FF_ZIP_METHOD_STORED 0
|
||||
#define FF_ZIP_METHOD_DEFLATED 8
|
||||
|
||||
// One entry, as the archive describes it. `payload` points into the mapping the caller passed in and
|
||||
// is not a copy: for a STORED entry it is the file's own bytes, and for a DEFLATED one it is the raw
|
||||
// deflate stream, which the caller inflates with `uncompressedSize` as the bound.
|
||||
typedef struct FFAndroidZipEntry {
|
||||
const uint8_t* payload;
|
||||
size_t payloadSize;
|
||||
uint32_t uncompressedSize;
|
||||
uint16_t method;
|
||||
} FFAndroidZipEntry;
|
||||
|
||||
// Resolves `entry` through the archive's central directory, writing where its payload is into
|
||||
// `result`. Returns nullptr on success, a static message otherwise -- an archive that is not a zip,
|
||||
// a directory that is out of range, an entry that is not there and an entry stored as zip64 are all
|
||||
// reported rather than answered with a pointer into nothing.
|
||||
[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result);
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "media.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/android/applabel.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/android/package.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/mallocHelper.h"
|
||||
@@ -31,8 +33,9 @@
|
||||
// service and reaches onTransact, and is then answered with a SecurityException carrying the server's
|
||||
// own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell
|
||||
// UID gets the session list from the same transaction code. Nothing in this file tests the uid: the
|
||||
// service is asked and its answer is believed, so a device whose policy is looser, or one where a
|
||||
// notification listener is registered, works without a change here.
|
||||
// service is asked and its answer is believed, so a device whose policy is looser works without a
|
||||
// change here. What the answer does depend on is the ComponentName in the request, which is not a
|
||||
// filter and not decoration either -- see the request build in androidDetectMediaSession().
|
||||
//
|
||||
// Only one of the two routes is ever asked, and which one it is depends on the display server. The
|
||||
// binder route is Android's, and the session it reads is the one the phone is playing only while
|
||||
@@ -94,8 +97,21 @@
|
||||
// strings; a reply that did not fit would be reported rather than misread.
|
||||
#define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024)
|
||||
|
||||
// An interface token plus, for getSessions, two int32s.
|
||||
#define FF_MEDIA_ANDROID_REQUEST_SIZE 128
|
||||
// ffAndroidGetOwnPackage() writes at most this many bytes, terminator included.
|
||||
#define FF_MEDIA_ANDROID_PACKAGE_SIZE 128
|
||||
|
||||
// The request is the interface token, the ComponentName and the user id. A ComponentName is a
|
||||
// presence marker in front of the package name and the class name, so the two string16s are what
|
||||
// took this past the 128 it used to be. That size had room for a seven-character package name --
|
||||
// `com.termux` with an empty class is already 136 bytes -- so the size is derived from the longest
|
||||
// name that can be written rather than rounded up by hand, because a request that does not fit is
|
||||
// dropped and reported as truncated: that fails the whole detection instead of degrading to a call
|
||||
// without a name. The empty class name costs a 4-byte length, a NUL terminator and the padding, so 8.
|
||||
// clang-format off
|
||||
#define FF_MEDIA_ANDROID_REQUEST_SIZE \
|
||||
(12 + ((4 + 2 * sizeof(FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR) + 3) & ~3u) \
|
||||
+ 4 + ((4 + 2 * FF_MEDIA_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8u + 4u)
|
||||
// clang-format on
|
||||
|
||||
// Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a
|
||||
// larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2%
|
||||
@@ -794,12 +810,18 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t tr
|
||||
return;
|
||||
}
|
||||
|
||||
// `player` wants the name the app shows, and there is no way to get one from here: `cmd package
|
||||
// dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and
|
||||
// resolving it would mean reading the app's resource table. The package name is what the
|
||||
// notification and the settings screen fall back to as well, so both fields get it -- the same
|
||||
// fallback media_linux.c uses when MPRIS answers without an Identity.
|
||||
// `player` wants the name the app shows rather than the name it is installed under, and the
|
||||
// package name is only the fallback. The label comes out of the APK's own resource table, which
|
||||
// the package manager points at with `sourceDir` and `labelRes`: the table is *stored* rather
|
||||
// than deflated -- the platform expects to mmap it -- so resolving an id out of it needs no
|
||||
// decompressor. What keeps the package name are the apps whose label is a literal string in
|
||||
// their manifest instead of a resource reference, since their reply carries no id at all; that
|
||||
// is also what the notification and the settings screen fall back to. See doc/app-label.md.
|
||||
ffStrbufSet(&result->player, &result->playerId);
|
||||
const char* labelError = ffAndroidGetAppLabel(result->playerId.chars, &result->player);
|
||||
if (labelError != nullptr) {
|
||||
FF_DEBUG("The label of \"%s\" could not be resolved: %s", result->playerId.chars, labelError);
|
||||
}
|
||||
}
|
||||
|
||||
static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) {
|
||||
@@ -859,7 +881,8 @@ 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.
|
||||
// pass, instead of hard-coding today's policy into the client. What the request has to carry for the
|
||||
// check to be consulted at all is the ComponentName: see the request build below.
|
||||
static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) {
|
||||
// Resolved before the binder is opened, so that a jar which does not declare the methods says so
|
||||
// rather than the module failing later with a message about the transport. The reader's own
|
||||
@@ -911,7 +934,38 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
|
||||
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR);
|
||||
ffBinderParcelPutI32(&parcel, 0); // ComponentName: null asks for every session
|
||||
|
||||
// The ComponentName is not a filter and not decoration: it decides which branch of the
|
||||
// service's permission check runs, and with a null one the branch this module needs is never
|
||||
// reached. MediaSessionService.enforceMediaPermissions() admits the shell and root, a holder of
|
||||
// android.permission.MEDIA_CONTENT_CONTROL, and a UID that owns an enabled notification
|
||||
// listener -- but the last one is reached through isEnabledNotificationListener(compName, uid),
|
||||
// which answers false for a null compName by construction. A null therefore asks for every
|
||||
// session only on behalf of a UID that was going to be admitted anyway.
|
||||
//
|
||||
// Measured on the test device, one binary run by three UIDs: the shell gets the session list
|
||||
// for every shape below; the app UID that owns an enabled listener gets -1 EX_SECURITY for a
|
||||
// null ComponentName and the session list for a non-null one; an app UID that owns none gets
|
||||
// -1 EX_SECURITY for both. Two more facts
|
||||
// come out of the same run: the class inside the ComponentName is never validated -- a name no
|
||||
// package declares and an empty string are answered exactly like a real one -- and the package
|
||||
// has to be one the calling uid owns, because the service's own enforcePackageName() rejects
|
||||
// anything else with EX_ILLEGAL_ARGUMENT ("packageName does not belong to the calling uid")
|
||||
// before the permission check is reached.
|
||||
//
|
||||
// So the caller declares its own package and the class is left empty rather than invented:
|
||||
// this is a standalone binary with no component of its own to name, and naming one the device
|
||||
// does not have would be a claim that cannot be backed. A build whose package cannot be
|
||||
// derived sends the null request, which is what this did before.
|
||||
char package[FF_MEDIA_ANDROID_PACKAGE_SIZE];
|
||||
if (ffAndroidGetOwnPackage(package, sizeof(package))) {
|
||||
ffBinderParcelPutI32(&parcel, 1);
|
||||
ffBinderParcelPutString16(&parcel, package);
|
||||
ffBinderParcelPutString16(&parcel, "");
|
||||
} else {
|
||||
FF_DEBUG("Cannot determine the package of this process, so the request declares no component");
|
||||
ffBinderParcelPutI32(&parcel, 0);
|
||||
}
|
||||
ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has
|
||||
|
||||
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
|
||||
@@ -927,11 +981,13 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
|
||||
if (exception != 0) {
|
||||
// -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The
|
||||
// number goes to the debug log rather than into the message: what tells the two cases apart
|
||||
// for the user is which UID is asking.
|
||||
// for the user is which UID is asking. On an app UID the missing privilege is named,
|
||||
// because it is one the user can grant -- the request below already declares the caller,
|
||||
// so whatever is left is the listener the UID does not own.
|
||||
FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : "");
|
||||
return ffAndroidIsRootOrShell(instance.state.platform.uid)
|
||||
? "The media session service refused the request"
|
||||
: "Reading the media session needs the shell UID or root: an app UID is refused by the media_session service";
|
||||
: "Reading the media session from an app UID needs an enabled notification listener, which this UID does not have";
|
||||
}
|
||||
|
||||
// The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order
|
||||
|
||||
Reference in New Issue
Block a user