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}