master ohmu/fastfetch-openbsd / src / common / android / applabel.c
ohmu  ·  2026-10-15
  1#include "common/android/applabel.h"
  2#include "common/android/binder.h"
  3#include "common/android/dex.h"
  4#include "common/android/zip.h"
  5#include "common/debug.h"
  6
  7#include <errno.h>
  8#include <fcntl.h>
  9#include <stdint.h>
 10#include <string.h>
 11#include <sys/mman.h>
 12#include <sys/stat.h>
 13#include <unistd.h>
 14
 15// The package manager, and the one method that describes an app to a caller that is not the shell.
 16// IPackageManager has always been part of framework.jar -- it is not one of the frameworks that
 17// moved into an APEX -- so there is one path and no fallback.
 18#define FF_ANDROID_LABEL_SERVICE "package"
 19#define FF_ANDROID_LABEL_DESCRIPTOR "android.content.pm.IPackageManager"
 20#define FF_ANDROID_LABEL_JAR "/system/framework/framework.jar"
 21#define FF_ANDROID_LABEL_STUB "Landroid/content/pm/IPackageManager$Stub;"
 22#define FF_ANDROID_LABEL_TRANSACTION "TRANSACTION_getApplicationInfo"
 23
 24#define FF_ANDROID_LABEL_TABLE "resources.arsc"
 25
 26// The longest package name AIDL accepts is 255 characters, and the request is one string16 of it, the
 27// interface token, a 64-bit flags argument and a 32-bit user id.
 28// clang-format off
 29#define FF_ANDROID_LABEL_REQUEST_SIZE \
 30    (12 + ((4 + 2 * (sizeof(FF_ANDROID_LABEL_DESCRIPTOR) - 1) + 3) & ~3u) \
 31        + ((4 + 2 * 255 + 3) & ~3u) + 8u + 4u)
 32// clang-format on
 33
 34// An ApplicationInfo parcel runs 2.0 to 3.2 KB on the test device, and the surplus is what a reply
 35// with more split names and directories than any of those would spend.
 36#define FF_ANDROID_LABEL_REPLY_SIZE (16 * 1024)
 37
 38// Long enough for the `/data/app/~~<22 base64 chars>==/<package>-<22 chars>==/base.apk` the platform
 39// installs to, with room to spare.
 40#define FF_ANDROID_LABEL_PATH_SIZE 512
 41
 42// ---------------------------------------------------------------------------------------------
 43// The resource table
 44// ---------------------------------------------------------------------------------------------
 45//
 46// `resources.arsc` is a ResTable: a header, a global string pool, and then one ResTable_package per
 47// package it defines, each carrying its own pools for type names and entry keys followed by the
 48// types themselves. Resolving `labelRes` is two indices deep -- the package's number names the
 49// package and the type, the low half names the entry -- and every step below reads the file rather
 50// than a table of what the file is expected to hold, because the layout is the platform's and the
 51// numbers are per APK.
 52
 53// ResChunk_header: type(2) headerSize(2) size(4). Every chunk in the file starts with one.
 54#define FF_ARSC_TABLE_TYPE 0x0002
 55#define FF_ARSC_STRING_POOL_TYPE 0x0001
 56#define FF_ARSC_TABLE_PACKAGE_TYPE 0x0200
 57#define FF_ARSC_TABLE_TYPE_TYPE 0x0201
 58
 59// ResStringPool: header(8) stringCount(4) styleCount(4) flags(4) stringsStart(4) stylesStart(4)
 60#define FF_ARSC_POOL_HEADER_SIZE 28
 61#define FF_ARSC_OFF_POOL_CHUNK_SIZE 4
 62#define FF_ARSC_OFF_POOL_COUNT 8
 63#define FF_ARSC_OFF_POOL_FLAGS 16
 64#define FF_ARSC_OFF_POOL_STRINGS_START 20
 65
 66// A UTF-8 pool says so with this bit, and then holds the strings as UTF-8 rather than as UTF-16.
 67#define FF_ARSC_POOL_FLAG_UTF8 0x0100
 68
 69// ResTable_package: header(8) id(4) name(256, a UTF-16 array) typeStrings(4) lastPublicType(4) ...
 70#define FF_ARSC_OFF_PACKAGE_ID 8
 71#define FF_ARSC_OFF_PACKAGE_TYPE_STRINGS (8 + 4 + 256)
 72
 73// ResTable_type: header(8) id(1) flags(1) reserved(2) entryCount(4) entriesStart(4) config(...)
 74#define FF_ARSC_OFF_TYPE_ID 8
 75#define FF_ARSC_OFF_TYPE_FLAGS 9
 76#define FF_ARSC_OFF_TYPE_ENTRY_COUNT 12
 77#define FF_ARSC_OFF_TYPE_ENTRIES_START 16
 78#define FF_ARSC_OFF_TYPE_CONFIG 20
 79
 80// The two shapes a type can take that this does not read: a sparse type stores its offsets as
 81// index-offset pairs rather than as a flat table, and a 16-bit one stores them at half width. Neither
 82// is in the APKs measured, and both would resolve an entry to the wrong offset rather than to none,
 83// so they are refused rather than read as if they were the plain shape.
 84#define FF_ARSC_TYPE_FLAG_SPARSE 0x01
 85#define FF_ARSC_TYPE_FLAG_OFFSET16 0x02
 86
 87// ResTable_entry: size(2) flags(2), where size is the header's own length and the value follows it.
 88#define FF_ARSC_ENTRY_FLAG_COMPLEX 0x0001
 89
 90// Res_value: size(2) res0(1) dataType(1) data(4). TYPE_STRING makes data an index into the global
 91// string pool; every other type is either a number or a reference to another resource.
 92#define FF_ARSC_OFF_VALUE_DATA_TYPE 3
 93#define FF_ARSC_OFF_VALUE_DATA 4
 94#define FF_ARSC_TYPE_STRING 0x03
 95
 96// A type chunk's offset table has one word per entry, and this is what an entry with no value in that
 97// configuration looks like. It is not zero: offset zero is a real entry, at the start of the block.
 98#define FF_ARSC_NO_ENTRY 0xffffffffu
 99
100// The type a label resolves to. `string` is what `@string/...` compiles to, and it is what tells a
101// label from the icons and banners that sit beside it in the same reply -- those point at `mipmap`
102// or `drawable` entries whose value is a path inside the APK, so resolving them would hand back
103// `res/9w.png` instead of a name.
104#define FF_ANDROID_LABEL_STRING_TYPE "string"
105
106static uint16_t readU16(const uint8_t* p) {
107    return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
108}
109
110static uint32_t readU32(const uint8_t* p) {
111    return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
112}
113
114// One ResStringPool, of which a table has three.
115typedef struct FFAndroidLabelPool {
116    const uint8_t* table;
117    size_t tableSize;
118    size_t offset;
119    uint32_t chunkSize;
120    uint32_t count;
121    uint32_t headerSize;
122    uint32_t stringsStart;
123    bool utf8;
124} FFAndroidLabelPool;
125
126[[gnu::nonnull(2), nodiscard]] static bool poolInit(FFAndroidLabelPool* pool, const uint8_t* table, size_t tableSize, size_t offset) {
127    if (offset > tableSize || tableSize - offset < FF_ARSC_POOL_HEADER_SIZE) {
128        return false;
129    }
130    if (readU16(table + offset) != FF_ARSC_STRING_POOL_TYPE) {
131        return false;
132    }
133    pool->table = table;
134    pool->tableSize = tableSize;
135    pool->offset = offset;
136    pool->chunkSize = readU32(table + offset + FF_ARSC_OFF_POOL_CHUNK_SIZE);
137    pool->count = readU32(table + offset + FF_ARSC_OFF_POOL_COUNT);
138    pool->headerSize = readU16(table + offset + 2);
139    pool->stringsStart = readU32(table + offset + FF_ARSC_OFF_POOL_STRINGS_START);
140    pool->utf8 = (readU32(table + offset + FF_ARSC_OFF_POOL_FLAGS) & FF_ARSC_POOL_FLAG_UTF8) != 0;
141    return pool->headerSize >= FF_ARSC_POOL_HEADER_SIZE && pool->chunkSize > pool->headerSize;
142}
143
144// The length in front of a string. A UTF-8 pool writes one or two bytes, a UTF-16 pool one or two
145// 16-bit words, and the top bit of the first says which of the two it is.
146[[gnu::nonnull(1, 3), nodiscard]] static bool readLength8(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
147    if (*position >= pool->tableSize) {
148        return false;
149    }
150    uint32_t value = pool->table[(*position)++];
151    if (value & 0x80) {
152        if (*position >= pool->tableSize) {
153            return false;
154        }
155        value = ((value & 0x7F) << 8) | pool->table[(*position)++];
156    }
157    *length = value;
158    return true;
159}
160
161[[gnu::nonnull(1, 3), nodiscard]] static bool readLength16(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
162    if (*position + 2 > pool->tableSize) {
163        return false;
164    }
165    uint32_t value = readU16(pool->table + *position);
166    *position += 2;
167    if (value & 0x8000) {
168        if (*position + 2 > pool->tableSize) {
169            return false;
170        }
171        value = ((value & 0x7FFF) << 16) | readU16(pool->table + *position);
172        *position += 2;
173    }
174    *length = value;
175    return true;
176}
177
178[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) {
179    char encoded[4] = {};
180    uint32_t length = 0;
181    if (code < 0x80) {
182        encoded[length++] = (char) code;
183    } else if (code < 0x800) {
184        encoded[length++] = (char) (0xC0 | (code >> 6));
185        encoded[length++] = (char) (0x80 | (code & 0x3F));
186    } else if (code < 0x10000) {
187        encoded[length++] = (char) (0xE0 | (code >> 12));
188        encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
189        encoded[length++] = (char) (0x80 | (code & 0x3F));
190    } else {
191        encoded[length++] = (char) (0xF0 | (code >> 18));
192        encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F));
193        encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
194        encoded[length++] = (char) (0x80 | (code & 0x3F));
195    }
196    ffStrbufAppendNS(result, length, encoded);
197}
198
199// One string out of a pool.
200//
201// The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they
202// differ for everything outside ASCII. Decoding `characters` bytes is the trap: a label of a few
203// multi-byte characters comes back as its first character followed by replacement bytes. Measured on
204// the test device, where every resource table read is a UTF-8 pool, this was the difference between
205// the right label and mojibake.
206[[gnu::nonnull(1, 3), nodiscard]] static bool poolGet(const FFAndroidLabelPool* pool, uint32_t index, FFstrbuf* result) {
207    if (index >= pool->count) {
208        return false;
209    }
210    const size_t offsets = pool->offset + pool->headerSize;
211    if (offsets + ((size_t) index + 1) * 4 > pool->tableSize) {
212        return false;
213    }
214    size_t position = pool->offset + pool->stringsStart + readU32(pool->table + offsets + (size_t) index * 4);
215    if (position >= pool->tableSize) {
216        return false;
217    }
218
219    if (pool->utf8) {
220        uint32_t characters = 0;
221        uint32_t bytes = 0;
222        if (!readLength8(pool, &position, &characters) || !readLength8(pool, &position, &bytes)) {
223            return false;
224        }
225        // An empty string is reported as a miss rather than written: the caller sets its fallback
226        // before asking, and ffStrbufSetNS() with a length of zero *clears* the buffer, which would
227        // throw that fallback away instead of leaving it alone.
228        if (bytes == 0 || bytes > pool->tableSize - position) {
229            return false;
230        }
231        ffStrbufSetNS(result, bytes, (const char*) (pool->table + position));
232        return true;
233    }
234
235    uint32_t units = 0;
236    if (!readLength16(pool, &position, &units)) {
237        return false;
238    }
239    if (units == 0 || (size_t) units * 2 > pool->tableSize - position) {
240        return false;
241    }
242    for (uint32_t i = 0; i < units; i++) {
243        uint32_t code = readU16(pool->table + position + (size_t) i * 2);
244        // A code point outside the basic plane arrives as a surrogate pair, and the two halves are
245        // not characters of their own: half of a pair encoded on its own is not valid UTF-8.
246        if (code >= 0xD800 && code <= 0xDBFF && i + 1 < units) {
247            const uint32_t low = readU16(pool->table + position + ((size_t) i + 1) * 2);
248            if (low >= 0xDC00 && low <= 0xDFFF) {
249                code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
250                i++;
251            }
252        }
253        appendCodePoint(result, code);
254    }
255    return true;
256}
257
258// The one ResTable_package this table defines, with the two pools a lookup starts from.
259typedef struct FFAndroidLabelTable {
260    const uint8_t* data;
261    size_t size;
262    FFAndroidLabelPool values;
263    FFAndroidLabelPool types;
264    size_t packageOffset;
265    size_t packageHeaderSize;
266    size_t packageSize;
267    uint32_t packageId;
268} FFAndroidLabelTable;
269
270[[gnu::nonnull(1), nodiscard]] static bool tableInit(FFAndroidLabelTable* table, const uint8_t* data, size_t size) {
271    // ResTable_header: header(8) packageCount(4). The header's own size is where the global pool
272    // starts, which is 12 in every table measured -- but it is the field rather than the number.
273    if (size < 12 || readU16(data) != FF_ARSC_TABLE_TYPE) {
274        return false;
275    }
276    table->data = data;
277    table->size = size;
278    const uint32_t headerSize = readU16(data + 2);
279    if (!poolInit(&table->values, data, size, headerSize)) {
280        return false;
281    }
282
283    size_t position = (size_t) headerSize + table->values.chunkSize;
284    while (position + 8 <= size) {
285        const uint16_t type = readU16(data + position);
286        const uint16_t chunkHeader = readU16(data + position + 2);
287        const uint32_t chunkSize = readU32(data + position + 4);
288        if (chunkSize < 8 || chunkSize > size - position) {
289            break;
290        }
291        // A table can define several packages -- an app that links a shared library's resources has
292        // more than one -- and the id in the resource reference is what picks the one to read.
293        if (type == FF_ARSC_TABLE_PACKAGE_TYPE && chunkSize >= chunkHeader && chunkHeader >= FF_ARSC_OFF_PACKAGE_TYPE_STRINGS + 4) {
294            table->packageOffset = position;
295            table->packageHeaderSize = chunkHeader;
296            table->packageSize = chunkSize;
297            table->packageId = readU32(data + position + FF_ARSC_OFF_PACKAGE_ID);
298            if (!poolInit(&table->types, data, size, position + readU32(data + position + FF_ARSC_OFF_PACKAGE_TYPE_STRINGS))) {
299                return false;
300            }
301            return true;
302        }
303        position += chunkSize;
304    }
305    return false;
306}
307
308// A type chunk whose ResTable_config is all zeroes is the one that applies when nothing about the
309// device or the locale matches a more specific chunk. An app with a translation has several chunks
310// for one type, and picking the first one that happens along would answer with whatever language it
311// was written in.
312[[gnu::nonnull(1), nodiscard]] static bool isDefaultConfig(const uint8_t* config, size_t length) {
313    if (length == 0) {
314        return false;
315    }
316    for (size_t i = 0; i < length; i++) {
317        if (config[i] != 0) {
318            return false;
319        }
320    }
321    return true;
322}
323
324[[gnu::nonnull(1, 5), nodiscard]] static bool resolveIn(const FFAndroidLabelTable* table, uint32_t typeId, uint32_t entryId, bool defaultOnly, FFstrbuf* result) {
325    const size_t end = table->packageOffset + table->packageSize;
326    for (size_t position = table->packageOffset + table->packageHeaderSize; position + 8 <= end;) {
327        const uint16_t type = readU16(table->data + position);
328        const uint16_t header = readU16(table->data + position + 2);
329        const uint32_t size = readU32(table->data + position + 4);
330        if (size < 8 || size > end - position) {
331            return false;
332        }
333        if (type == FF_ARSC_TABLE_TYPE_TYPE && header >= FF_ARSC_OFF_TYPE_CONFIG) {
334            const uint8_t id = table->data[position + FF_ARSC_OFF_TYPE_ID];
335            const uint8_t flags = table->data[position + FF_ARSC_OFF_TYPE_FLAGS];
336            if (id == typeId && (flags & (FF_ARSC_TYPE_FLAG_SPARSE | FF_ARSC_TYPE_FLAG_OFFSET16)) == 0 && size >= header) {
337                const uint32_t entryCount = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRY_COUNT);
338                const uint32_t entriesStart = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRIES_START);
339                const size_t offsets = position + header;
340                if (entryId < entryCount && offsets + ((size_t) entryId + 1) * 4 <= end) {
341                    if (defaultOnly && !isDefaultConfig(table->data + position + FF_ARSC_OFF_TYPE_CONFIG, (size_t) header - FF_ARSC_OFF_TYPE_CONFIG)) {
342                        position += size;
343                        continue;
344                    }
345                    const uint32_t offset = readU32(table->data + offsets + (size_t) entryId * 4);
346                    // `entriesStart` is relative to the chunk, not to the package, and both it and
347                    // the entry's own offset come out of the file.
348                    if (offset != FF_ARSC_NO_ENTRY && entriesStart <= size && offset <= size - entriesStart) {
349                        const size_t entry = position + entriesStart + offset;
350                        const size_t value = entry + readU16(table->data + entry);
351                        if (value >= entry && value + 8 <= end && (readU16(table->data + entry + 2) & FF_ARSC_ENTRY_FLAG_COMPLEX) == 0
352                            && table->data[value + FF_ARSC_OFF_VALUE_DATA_TYPE] == FF_ARSC_TYPE_STRING) {
353                            return poolGet(&table->values, readU32(table->data + value + FF_ARSC_OFF_VALUE_DATA), result);
354                        }
355                    }
356                }
357            }
358        }
359        position += size;
360    }
361    return false;
362}
363
364// Resolves one resource id to its string, and only when the type it names is a `string`.
365//
366// The type name is the whole reason this reads the package's type pool rather than trusting the id:
367// `labelRes` and the icon, logo and banner ids that sit beside it in the same reply are all resource
368// ids of the same package, and the difference between them is the type they point at. A `mipmap` id
369// resolves perfectly well -- to `res/9w.png`.
370[[gnu::nonnull(1, 3), nodiscard]] static bool tableResolve(const FFAndroidLabelTable* table, uint32_t resourceId, FFstrbuf* result) {
371    const uint32_t typeId = (resourceId >> 16) & 0xFF;
372    if (((resourceId >> 24) & 0xFF) != table->packageId || typeId == 0) {
373        return false;
374    }
375
376    FFstrbuf typeName;
377    ffStrbufInit(&typeName);
378    const bool isString = poolGet(&table->types, typeId - 1, &typeName) && ffStrbufEqualS(&typeName, FF_ANDROID_LABEL_STRING_TYPE);
379    ffStrbufDestroy(&typeName);
380    if (!isString) {
381        return false;
382    }
383
384    // The configuration that applies is the unqualified one; a chunk that only applies to a locale
385    // or a density is the fallback rather than the answer, which is what the second pass is for.
386    return resolveIn(table, typeId, resourceId & 0xFFFF, true, result)
387        || resolveIn(table, typeId, resourceId & 0xFFFF, false, result);
388}
389
390// ---------------------------------------------------------------------------------------------
391// The reply
392// ---------------------------------------------------------------------------------------------
393
394// The reply's own copy of the APK path. `sourceDir` is a string in the ApplicationInfo parcel and it
395// ends in `.apk`; nothing else in the reply does, which is what makes it findable without walking
396// the parcel's field order -- that would mean recovering `ApplicationInfo.writeToParcel` out of the
397// device's dex and then framing sixty fields of unknown width, to reach one string that is already
398// recognizable.
399[[gnu::nonnull(1, 3), nodiscard]] static bool findApkPath(const uint8_t* data, size_t size, char* result, size_t capacity) {
400    for (size_t i = 8; i < size; i++) {
401        if (data[i] != '/') {
402            continue;
403        }
404        size_t end = i;
405        while (end < size && data[end] >= 0x20 && data[end] < 0x7F) {
406            end++;
407        }
408        const size_t length = end - i;
409        if (length >= 5 && length < capacity && memcmp(data + end - 4, ".apk", 4) == 0) {
410            memcpy(result, data + i, length);
411            result[length] = '\0';
412            return true;
413        }
414        i = end;
415    }
416    return false;
417}
418
419// ---------------------------------------------------------------------------------------------
420
421typedef struct FFAndroidLabelMapping {
422    const uint8_t* data;
423    size_t size;
424} FFAndroidLabelMapping;
425
426[[gnu::nonnull(1)]] static void labelUnmap(FFAndroidLabelMapping* mapping) {
427    if (mapping->data != nullptr) {
428        munmap((void*) mapping->data, mapping->size);
429    }
430}
431
432[[gnu::nonnull(2), nodiscard]] static const char* labelMap(const char* path, FFAndroidLabelMapping* mapping) {
433    const int fd = open(path, O_RDONLY | O_CLOEXEC);
434    if (fd < 0) {
435        FF_DEBUG("open(%s) failed: %s", path, strerror(errno));
436        return "open(apk) failed";
437    }
438
439    struct stat status = {};
440    const bool failed = fstat(fd, &status) != 0;
441    if (failed || status.st_size <= 0) {
442        FF_DEBUG("fstat(%s) reported %s", path, failed ? strerror(errno) : "an empty file");
443        close(fd);
444        return "fstat(apk) failed";
445    }
446
447    void* mapped = mmap(nullptr, (size_t) status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
448    close(fd);
449    if (mapped == MAP_FAILED) {
450        FF_DEBUG("mmap(%s, %lld bytes) failed: %s", path, (long long) status.st_size, strerror(errno));
451        return "mmap(apk) failed";
452    }
453    mapping->data = mapped;
454    mapping->size = (size_t) status.st_size;
455    return nullptr;
456}
457
458// One reply buffer for every call this file makes, and it makes one per query.
459static uint8_t s_reply[FF_ANDROID_LABEL_REPLY_SIZE];
460
461const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result) {
462    // The transaction code comes out of the jar rather than from a table here: it is a property of
463    // the `.aidl` the device's ROM was built from, and a vendor that inserts a method ahead of this
464    // one shifts every code behind it. See common/android/dex.h.
465    int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
466    const FFDexStaticIntRequest lookup = {
467        .classDescriptor = FF_ANDROID_LABEL_STUB,
468        .fieldName = FF_ANDROID_LABEL_TRANSACTION,
469        .result = &transaction,
470    };
471    const char* error = ffDexStaticInts(FF_ANDROID_LABEL_JAR, &lookup, 1);
472    if (error != nullptr) {
473        return error;
474    }
475    if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
476        return "framework.jar does not declare IPackageManager.getApplicationInfo";
477    }
478
479    [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
480    error = ffBinderOpen(&binder);
481    if (error != nullptr) {
482        return error;
483    }
484
485    [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
486    error = ffBinderLookupService(&binder, FF_ANDROID_LABEL_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
487    if (error != nullptr) {
488        FF_DEBUG("Looking up \"%s\" failed: %s", FF_ANDROID_LABEL_SERVICE, error);
489        return error;
490    }
491
492    uint8_t request[FF_ANDROID_LABEL_REQUEST_SIZE];
493    FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
494    ffBinderParcelPutInterfaceToken(&parcel, FF_ANDROID_LABEL_DESCRIPTOR);
495    ffBinderParcelPutString16(&parcel, packageName);
496    // `getApplicationInfo(String, long flags, int userId)`. The flags argument is 64-bit from Android
497    // 13 on and 32-bit before it, and the wider one is what works on both: a release that reads an
498    // int takes the low word and leaves the zero high word unread, because an AIDL stub stops
499    // reading once its own arguments are in. The narrow one is not symmetric -- a release that reads
500    // a long would take half of its own flags argument for the user id.
501    ffBinderParcelPutU64(&parcel, 0);
502    ffBinderParcelPutI32(&parcel, 0);
503
504    FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
505    error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
506    if (error != nullptr) {
507        return error;
508    }
509    if (ffBinderReplyIsStatus(&reply)) {
510        return "The package manager rejected the request";
511    }
512    // The AIDL status first, then the presence marker of a nullable Parcelable: a package the caller
513    // is not allowed to see answers an empty ApplicationInfo rather than an error.
514    if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
515        return "The package manager refused to describe the app";
516    }
517    if (ffBinderReadI32(reply.data, reply.size, 4) == 0) {
518        FF_DEBUG("The package manager described \"%s\" as an empty ApplicationInfo", packageName);
519        return nullptr;
520    }
521
522    char path[FF_ANDROID_LABEL_PATH_SIZE];
523    if (!findApkPath(reply.data, reply.size, path, sizeof(path))) {
524        return "The reply carries no APK path";
525    }
526    FF_DEBUG("\"%s\" is installed at \"%s\"", packageName, path);
527
528    [[gnu::cleanup(labelUnmap)]] FFAndroidLabelMapping mapping = {};
529    error = labelMap(path, &mapping);
530    if (error != nullptr) {
531        return error;
532    }
533
534    FFAndroidZipEntry table = {};
535    error = ffAndroidZipFindEntry(mapping.data, mapping.size, FF_ANDROID_LABEL_TABLE, &table);
536    if (error != nullptr) {
537        return error;
538    }
539    if (table.method != FF_ZIP_METHOD_STORED) {
540        FF_DEBUG("\"%s\" stores resources.arsc with compression method %u", packageName, (unsigned) table.method);
541        return "The resource table is stored compressed";
542    }
543
544    FFAndroidLabelTable resources = {};
545    if (!tableInit(&resources, table.payload, table.payloadSize)) {
546        return "The resource table could not be read";
547    }
548
549    // Every word of the reply shaped like one of this package's resource ids, in the order they
550    // appear, and the first one that resolves to a `string` is the label. See the header for what
551    // that rule was audited against and why it beats recovering the parcel's field order.
552    for (size_t offset = 8; offset + 4 <= reply.size; offset += 4) {
553        if (tableResolve(&resources, readU32(reply.data + offset), result)) {
554            FF_DEBUG("\"%s\" shows itself as \"%s\"", packageName, result->chars);
555            return nullptr;
556        }
557    }
558    FF_DEBUG("No resource id in the reply of \"%s\" resolves to a string", packageName);
559    return nullptr;
560}