Skip to content

Tier 3 — low-level / escape hatches

Pathological cases, debugging, and interop with hooks not authored against rosetta-frida. Two surfaces:

  • rosetta.map.* — direct resolver queries and runtime overrides.
  • rosetta.events.* — programmatic subscription to the session's diagnostic event bus.

Both surfaces are intentionally thin. They delegate to the underlying Resolver and EventBus; the value is in being the documented escape hatch when tier 1 and tier 2 don't fit.

rosetta.map

The rosetta.map accessor returns a fresh MapApi bound to the current ambient session each access — so switching sessions via rosetta.session(...) also switches what rosetta.map returns.

MapApi shape

interface MapApi {
    resolveClass(realName: string): ResolvedClass;
    resolveMethod(
        className: string,
        methodName: string,
        argTypes?: readonly string[],
    ): ResolvedMethod;
    resolveField(className: string, fieldName: string): ResolvedField;
    override(realName: string, entry: ClassEntry): void;
    extract(): RosettaMap;
}

rosetta.map.resolveClass

const cls = rosetta.map.resolveClass('com.example.app.IRemoteService$Stub');
// → { realName: 'com.example.app.IRemoteService$Stub',
//     obfName: 'aaaa',
//     entry: { obfuscated: 'aaaa', kind: 'class', methods: { ... }, ... } }

ResolvedClass carries the full ClassEntry from the map, so you can introspect kind, extends, dex, methods, fields, and source provenance.

Useful for adaptive logic that branches on whether a real name is in the map this release:

let stubClass: ResolvedClass | null = null;
try {
    stubClass = rosetta.map.resolveClass('com.example.app.IRemoteService$Stub');
} catch (e) {
    if (e instanceof ResolveError) {
        send({ stage: 'stub-not-in-map', version: rosetta.map.extract().version });
    } else {
        throw e;
    }
}

rosetta.map.resolveMethod

const m = rosetta.map.resolveMethod(
    'com.example.app.IRemoteService$Stub',
    'requestTicket',
);
// → { realName: 'requestTicket',
//     obfName: 'c',
//     className: 'aaaa',
//     signature: '(Landroid/os/Bundle;Lbbbb;)V',
//     aidlTxn: 2,
//     static: false,
//     allOverloads: [ ... ] }

When requestTicket has multiple overloads in the map, pass argTypes to disambiguate (real names + framework types, same rule as tier 1's args):

const m = rosetta.map.resolveMethod(
    'com.example.app.IRemoteService$Stub',
    'requestTicket',
    ['android.os.Bundle', 'com.example.app.IServiceCallback'],
);

Omitting argTypes on a multi-overload method throws AmbiguousOverloadError.

allOverloads is always populated — even for single-overload methods (one-element array) — so callers can inspect the full set.

Falling through to plain Java.use

The classic tier-3 pattern: resolve, then call into Frida directly without going through the proxy layer.

const m = rosetta.map.resolveMethod('com.example.app.IRemoteService$Stub', 'requestTicket');
Java.use(m.className)[m.obfName]
    .overload('android.os.Bundle', 'bbbb')
    .implementation = function (b, cb) {
        send({ stage: 'raw-hook' });
        return this[m.obfName].apply(this, arguments);
    };

This is exactly what rosetta.hook(...) does under the hood. Drop to it when you need control the wrapper takes away (e.g. wrapping Frida's own overload-selection logic).

rosetta.map.resolveField

const f = rosetta.map.resolveField('com.example.app.Config', 'MAX_RETRIES');
// → { realName: 'MAX_RETRIES',
//     obfName: 'b',
//     className: 'nnnn',
//     type: 'I',
//     static: true }

The type field is the JVM descriptor (I for int, J for long, Ljava/lang/String; for String, etc.).

rosetta.map.override

Install a runtime override for a class entry. Future lookups see the override instead of the map's value. Caches are invalidated automatically for the overridden name.

rosetta.map.override('com.example.app.IRemoteService$Stub', {
    obfuscated: 'xyz',
    kind: 'class',
    methods: {
        requestTicket: { obfuscated: 'a', signature: '(Landroid/os/Bundle;Lbbbb;)V' },
    },
});

Use cases:

  • Hot-patching a map gap mid-session without rebuilding the bundle.
  • Test fixtures — feeding a session a synthetic class entry to exercise the rest of the pipeline.
  • V2+ runtime discovery will populate overrides automatically; in V1 you do this by hand.

The override replaces the entire ClassEntry. If you only want to amend one method, read the existing entry first, mutate, write back:

const cls = rosetta.map.resolveClass('com.example.app.Foo');
const entry = { ...cls.entry, methods: { ...cls.entry.methods, newMethod: { obfuscated: 'd', signature: '()V' } } };
rosetta.map.override('com.example.app.Foo', entry);

rosetta.map.extract

Returns the bound RosettaMap (after registry resolution if applicable). The returned object is the same one the session loaded — don't mutate it; use override instead.

const map = rosetta.map.extract();
send({
    stage: 'session-info',
    app: map.app,
    version: map.version,
    classes: Object.keys(map.classes).length,
});

Useful for diagnostic reporting and integrating with host-side tooling that wants to know what map is active.

Errors

The four resolve methods raise the standard resolver errors:

Error When
ResolveError The real name isn't in the map.
AmbiguousOverloadError resolveMethod called on a multi-overload method without argTypes.

override and extract don't throw — they're pure reads/writes on the resolver state.

rosetta.events

The diagnostic event surface. Subscribe to every resolve, every health-check, every detect, every map-load.

interface EventsApi {
    on(listener: EventListener): () => void;
    onType<T extends DiagnosticEvent['type']>(
        type: T,
        listener: EventListener<Extract<DiagnosticEvent, { type: T }>>,
    ): () => void;
}

Like rosetta.map, rosetta.events is property-getter-shaped — it rebuilds the API against the current ambient session on each access.

rosetta.events.on

Subscribe to all events:

const off = rosetta.events.on((event) => {
    send({ rosettaEvent: event });
});

// ... later ...
off();   // unsubscribe

The returned function unsubscribes the listener. Idempotent — calling it twice is a no-op.

rosetta.events.onType

Subscribe to events of one type, with full TypeScript narrowing on the listener's argument:

rosetta.events.onType('resolve', (e) => {
    if (e.miss) {
        send({ stage: 'unresolved', name: e.name, scope: e.classScope });
    }
});

rosetta.events.onType('health-check', (e) => {
    send({ stage: 'health', passed: e.passed, rate: e.rate });
});

rosetta.events.onType('detect', (e) => {
    send({ stage: 'detect', app: e.app, version: e.version, source: e.source });
});

rosetta.events.onType('map-load', (e) => {
    send({ stage: 'map-load', classes: e.classCount });
});

Event types

Four event kinds; see Events reference for field-by-field documentation.

type Emitted when
'resolve' A real name is translated to obfuscated (or misses). Fires inside every tier-1, tier-2, tier-3 resolver call.
'health-check' Attach-time health check completed. Carries passed, rate, failedEntries, threshold.
'detect' App / version was determined at session start. source: 'auto' or 'override'.
'map-load' Map was selected (post-registry-pick if applicable). Carries app, version, classCount, schemaVersion.

Trace mode and rosetta.events coexist

Setting trace: true on the session emits the same events to console.error as readable single-line strings in addition to delivering them to programmatic subscribers. Use both:

rosetta.session({ map, trace: true });

rosetta.events.onType('resolve', (e) => {
    if (e.miss) {
        send({ alert: 'unresolved', name: e.name });
    }
});

Stderr trace is for development; send()-channel subscription is for the host controller to aggregate or fail CI on misses.

Tier-3 lower-level handles

If you need to compose your own session wiring — typically for multi-session scripts or for tests — the package re-exports the underlying primitives:

Symbol Purpose
createResolver(map, options) Build a Resolver bound to a map + EventBus.
ResolverImpl Concrete class behind createResolver.
makeClassProxy, makeMethodHandle, makeFieldAccessor, makeInstanceProxy Build proxy objects directly without going through rosetta.use.
EventBus, createSilentBus Build an event bus directly.
makeSentinel, isSentinel Sentinels for the warn failure policy — see Design — sentinels.
import { createResolver, EventBus } from 'rosetta-frida';

const events = new EventBus();
const resolver = createResolver(map, { events, failurePolicy: 'strict' });

const cls = resolver.resolveClass('com.example.app.Foo');

For two simultaneous sessions in one script, build them explicitly via createSession — see Advanced composition.