master ohmu/fastfetch-openbsd / src / common / android / dex.h
ohmu  ·  2026-10-15
  1#pragma once
  2
  3// AIDL gives every method of an interface a transaction code at build time:
  4//
  5//     static final int TRANSACTION_<method> = IBinder.FIRST_CALL_TRANSACTION + <declaration index>;
  6//
  7// so the number is a property of the `.aidl` of the release that built the device's jar, not of any
  8// public header. Counting the methods in an AOSP checkout only describes that checkout, and it goes
  9// wrong the moment a release or a vendor fork inserts a method ahead of the one being looked for:
 10// `IWifiManager.getConnectionInfo` is 29 on Android 11 and 41 on Android 16. There is no negotiation
 11// and no discovery -- a wrong code reaches a different method, or none.
 12//
 13// Being a compile-time constant, the value lands in the `static_values` array of the `X$Stub` class
 14// in the jar's dex, paired position by position with that class's static field list. Reading the two
 15// together gives the mapping of the build that is actually on the device, on any release, with
 16// nothing to keep in sync.
 17//
 18// `libdexfile.so` cannot do this for us. It is dlopen-able from an app UID through the ART apex, but
 19// it is not in the NDK and its only C ABI -- the `ADexFile_*` family -- covers methods, not fields;
 20// the field and class-data APIs are C++ with no ABI guarantee.
 21//
 22// `ffReadFileBuffer` would work but pulls the whole jar through the heap for one integer. The jar is
 23// mapped instead, and only the pages actually read are faulted in.
 24
 25#include "fastfetch.h"
 26
 27// What a request is written when the jar does not answer it, on every path out of ffDexStaticInts --
 28// a jar that cannot be opened and a field the class does not declare both leave it behind, so a
 29// caller reads a result rather than what its own stack happened to hold there. No transaction code
 30// is negative: AIDL numbers them from `IBinder.FIRST_CALL_TRANSACTION`, which is 1.
 31//
 32// This is what keeps the reader a lookup and nothing more. Whether a code is one the caller cannot do
 33// without or one it only uses when the build declares it, whether what came back is enough to carry
 34// on, and whether the module reports a value or an error are the caller's decisions, made by reading
 35// the results. The reader finds numbers and says which ones it could not.
 36#define FF_DEX_STATIC_INT_UNRESOLVED (-1)
 37
 38// One `<classDescriptor>.<fieldName>` lookup, and where its value is written. `classDescriptor` is
 39// the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
 40typedef struct FFDexStaticIntRequest {
 41    const char* classDescriptor;
 42    const char* fieldName;
 43    int32_t* result;
 44} FFDexStaticIntRequest;
 45
 46// Resolves the int value of every request out of the dex entries of `jarPath`, walking `classes.dex`,
 47// `classes2.dex`, ... in order until one of them defines the class.
 48//
 49// Asking for several fields at once is not a convenience. A lookup is dominated by the type table,
 50// which lists every type the dex references -- 8428 descriptors in `framework.jar`'s `classes.dex`,
 51// each one read from a different part of a 51 MB mapping -- and a single pass answers every request
 52// whose class is named in it. Measured over three devices' own `framework.jar`, four fields of two
 53// classes cost one pass rather than four.
 54//
 55// Every request is written a code or FF_DEX_STATIC_INT_UNRESOLVED. A request is never allowed to
 56// take the others down with it: the classes a caller asks for are looked for independently, and one
 57// the jar does not declare leaves its own result at the sentinel while the rest are answered.
 58//
 59// Not finding a code is not a failure. A jar that is readable answers the call, whether it declares
 60// every field asked for, some of them, or none -- the results say which, and what that means for the
 61// module is the caller's to decide. The return value is therefore reserved for the one thing the
 62// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
 63// or does not hold a dex that can be read. nullptr means the lookup ran.
 64[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
 65
 66// ---------------------------------------------------------------------------------------------
 67// Reading a Parcelable's wire layout
 68// ---------------------------------------------------------------------------------------------
 69//
 70// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
 71// that order is the only thing that says where each one is. It is not in the class's declaration: a
 72// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
 73// declaration order a reader would want is not recorded anywhere but the method body.
 74//
 75// Recovering it therefore means walking the bytecode. The compiler emits
 76//
 77//     iget v0, p0, LDisplayInfo;->logicalWidth:I
 78//     invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
 79//
 80// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
 81// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
 82// easy to get wrong in a way that still produces a plausible-looking list:
 83//
 84//  * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
 85//    The table below is ART's `Instruction::SizeInCodeUnits`.
 86//  * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
 87//    low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
 88//    reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
 89//    first of them rather than trying to step over it.
 90//
 91// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
 92// again inside the loop that writes the elements. A class cannot declare two instance fields under
 93// one name, so a repeat is always that reload, and the first occurrence is where the field is
 94// written. The reader de-duplicates by name for that reason.
 95//
 96// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
 97// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
 98// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
 99// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
100// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
101// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
102// does not. Measured over three releases' own `framework.jar`, that is four names per class.
103//
104// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
105// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
106// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
107// ones a parser written against one release reads out of the middle of another. Reading the sequence
108// from the build that is running makes every one of them optional by construction.
109
110// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
111// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
112// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
113// than truncated: half a name would silently match nothing, which is indistinguishable from the field
114// not being there.
115#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
116#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
117
118// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
119// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
120typedef struct FFDexParcelField {
121    char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
122    char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
123} FFDexParcelField;
124
125// One class, and the two things that can be asked about it: the write sequence of one of its methods,
126// and which instance fields it declares. Either half may be left out, and both are answered out of
127// the same `class_data_item` -- which is why they are one request rather than two calls.
128//
129// Asking for them together is not a convenience. Both halves are read from a walk that is dominated
130// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a
131// different part of a 51 MB mapping -- and the fields a class declares are listed in the very
132// `class_data_item` the sequence is read out of. A second call would map the jar again, open every
133// entry again and make that pass again to reach a list the first call already had in hand. Measured
134// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup.
135typedef struct FFDexParcelRequest {
136    const char* classDescriptor;
137
138    // The write sequence. Left out by a request that only wants the instance fields, in which case
139    // `fields`, `capacity` and `count` are not read.
140    const char* methodName;
141    FFDexParcelField* fields;
142    uint32_t capacity;
143    uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
144
145    // The instance fields. This is the question a Parcelable whose layout is not field-driven needs.
146    // `DisplayCutout` is one: its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors
147    // the compiler synthesised, so there is no `iget` to walk and no class name on the wire either.
148    // What does say how long its body is, is which fields the class carries -- Android 11 declares
149    // three, Android 13 adds `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each
150    // one adds a known run of writes behind the three both have. A declaration is a field set rather
151    // than an order, which is exactly what this answers.
152    const char* const* instanceFieldNames;
153    bool* instanceFieldResults;
154    uint32_t instanceFieldCount;
155} FFDexParcelRequest;
156
157// Resolves every request out of the dex entries of `jarPath`, walking `classes.dex`, `classes2.dex`,
158// ... in order until one of them defines the class -- the same walk, and the same one pass over the
159// type table, that ffDexStaticInts makes.
160//
161// Every request is written its answers before anything can fail: a count of 0 for the sequence, and
162// `false` for every instance field. A class the jar does not declare, a method it does not declare,
163// and a sequence longer than the caller's `capacity` all leave the count at 0, and none of them holds
164// back the others: they are independent questions about the same pass. A count of 0 is not a failure
165// but an answer -- this build does not write that class, so a caller reading it has nothing to walk.
166// The return value is reserved for the jar itself, as in ffDexStaticInts.
167[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);