frida-objc-bridge
Version:
Objective-C runtime interop from Frida
1,354 lines (1,199 loc) • 94.9 kB
JavaScript
/* jshint esnext: true, evil: true */
import {getApi, defaultInvocationOptions} from './lib/api.js';
import * as fastpaths from './lib/fastpaths.js';
function Runtime() {
const pointerSize = Process.pointerSize;
let api = null;
let apiError = null;
const realizedClasses = new Set();
const classRegistry = new ClassRegistry();
const protocolRegistry = new ProtocolRegistry();
const replacedMethods = new Map();
const scheduledWork = new Map();
let nextId = 1;
let workCallback = null;
let NSAutoreleasePool = null;
const bindings = new Map();
let readObjectIsa = null;
const msgSendBySignatureId = new Map();
const msgSendSuperBySignatureId = new Map();
let cachedNSString = null;
let cachedNSStringCtor = null;
let cachedNSNumber = null;
let cachedNSNumberCtor = null;
let singularTypeById = null;
let modifiers = null;
try {
tryInitialize();
} catch (e) {
}
function tryInitialize() {
if (api !== null)
return true;
if (apiError !== null)
throw apiError;
try {
api = getApi();
} catch (e) {
apiError = e;
throw e;
}
return api !== null;
}
function dispose() {
for (const [rawMethodHandle, impls] of replacedMethods.entries()) {
const methodHandle = ptr(rawMethodHandle);
const [oldImp, newImp] = impls;
if (api.method_getImplementation(methodHandle).equals(newImp))
api.method_setImplementation(methodHandle, oldImp);
}
replacedMethods.clear();
}
Script.bindWeak(this, dispose);
Object.defineProperty(this, 'available', {
enumerable: true,
get() {
return tryInitialize();
}
});
Object.defineProperty(this, 'api', {
enumerable: true,
get() {
return getApi();
}
});
Object.defineProperty(this, 'classes', {
enumerable: true,
value: classRegistry
});
Object.defineProperty(this, 'protocols', {
enumerable: true,
value: protocolRegistry
});
Object.defineProperty(this, 'Object', {
enumerable: true,
value: ObjCObject
});
Object.defineProperty(this, 'Protocol', {
enumerable: true,
value: ObjCProtocol
});
Object.defineProperty(this, 'Block', {
enumerable: true,
value: Block
});
Object.defineProperty(this, 'mainQueue', {
enumerable: true,
get() {
return api?._dispatch_main_q ?? null;
}
});
Object.defineProperty(this, 'registerProxy', {
enumerable: true,
value: registerProxy
});
Object.defineProperty(this, 'registerClass', {
enumerable: true,
value: registerClass
});
Object.defineProperty(this, 'registerProtocol', {
enumerable: true,
value: registerProtocol
});
Object.defineProperty(this, 'bind', {
enumerable: true,
value: bind
});
Object.defineProperty(this, 'unbind', {
enumerable: true,
value: unbind
});
Object.defineProperty(this, 'getBoundData', {
enumerable: true,
value: getBoundData
});
Object.defineProperty(this, 'enumerateLoadedClasses', {
enumerable: true,
value: enumerateLoadedClasses
});
Object.defineProperty(this, 'enumerateLoadedClassesSync', {
enumerable: true,
value: enumerateLoadedClassesSync
});
Object.defineProperty(this, 'choose', {
enumerable: true,
value: choose
});
Object.defineProperty(this, 'chooseSync', {
enumerable: true,
value(specifier) {
const instances = [];
choose(specifier, {
onMatch(i) {
instances.push(i);
},
onComplete() {
}
});
return instances;
}
});
this.schedule = function (queue, work) {
const id = ptr(nextId++);
scheduledWork.set(id.toString(), work);
if (workCallback === null) {
workCallback = new NativeCallback(performScheduledWorkItem, 'void', ['pointer']);
}
Script.pin();
api.dispatch_async_f(queue, id, workCallback);
};
function performScheduledWorkItem(rawId) {
const id = rawId.toString();
const work = scheduledWork.get(id);
scheduledWork.delete(id);
if (NSAutoreleasePool === null)
NSAutoreleasePool = classRegistry.NSAutoreleasePool;
const pool = NSAutoreleasePool.alloc().init();
let pendingException = null;
try {
work();
} catch (e) {
pendingException = e;
}
pool.release();
setImmediate(performScheduledWorkCleanup, pendingException);
}
function performScheduledWorkCleanup(pendingException) {
Script.unpin();
if (pendingException !== null) {
throw pendingException;
}
}
this.implement = function (method, fn) {
return new NativeCallback(fn, method.returnType, method.argumentTypes);
};
this.selector = selector;
this.selectorAsString = selectorAsString;
function selector(name) {
return api.sel_registerName(Memory.allocUtf8String(name));
}
function selectorAsString(sel) {
return api.sel_getName(sel).readUtf8String();
}
const registryBuiltins = new Set([
"prototype",
"constructor",
"hasOwnProperty",
"toJSON",
"toString",
"valueOf"
]);
function ClassRegistry() {
const cachedClasses = {};
let numCachedClasses = 0;
const registry = new Proxy(this, {
has(target, property) {
return hasProperty(property);
},
get(target, property, receiver) {
switch (property) {
case "prototype":
return target.prototype;
case "constructor":
return target.constructor;
case "hasOwnProperty":
return hasProperty;
case "toJSON":
return toJSON;
case "toString":
return toString;
case "valueOf":
return valueOf;
default:
const klass = findClass(property);
return (klass !== null) ? klass : undefined;
}
},
set(target, property, value, receiver) {
return false;
},
ownKeys(target) {
if (api === null)
return [];
let numClasses = api.objc_getClassList(NULL, 0);
if (numClasses !== numCachedClasses) {
// It's impossible to unregister classes in ObjC, so if the number of
// classes hasn't changed, we can assume that the list is up to date.
const classHandles = Memory.alloc(numClasses * pointerSize);
numClasses = api.objc_getClassList(classHandles, numClasses);
for (let i = 0; i !== numClasses; i++) {
const handle = classHandles.add(i * pointerSize).readPointer();
const name = api.class_getName(handle).readUtf8String();
cachedClasses[name] = handle;
}
numCachedClasses = numClasses;
}
return Object.keys(cachedClasses);
},
getOwnPropertyDescriptor(target, property) {
return {
writable: false,
configurable: true,
enumerable: true
};
},
});
function hasProperty(name) {
if (registryBuiltins.has(name))
return true;
return findClass(name) !== null;
}
function getClass(name) {
const cls = findClass(name);
if (cls === null)
throw new Error("Unable to find class '" + name + "'");
return cls;
}
function findClass(name) {
let handle = cachedClasses[name];
if (handle === undefined) {
handle = api.objc_lookUpClass(Memory.allocUtf8String(name));
if (handle.isNull())
return null;
cachedClasses[name] = handle;
numCachedClasses++;
}
return new ObjCObject(handle, undefined, true);
}
function toJSON() {
return Object.keys(registry).reduce(function (r, name) {
r[name] = getClass(name).toJSON();
return r;
}, {});
}
function toString() {
return "ClassRegistry";
}
function valueOf() {
return "ClassRegistry";
}
return registry;
}
function ProtocolRegistry() {
let cachedProtocols = {};
let numCachedProtocols = 0;
const registry = new Proxy(this, {
has(target, property) {
return hasProperty(property);
},
get(target, property, receiver) {
switch (property) {
case "prototype":
return target.prototype;
case "constructor":
return target.constructor;
case "hasOwnProperty":
return hasProperty;
case "toJSON":
return toJSON;
case "toString":
return toString;
case "valueOf":
return valueOf;
default:
const proto = findProtocol(property);
return (proto !== null) ? proto : undefined;
}
},
set(target, property, value, receiver) {
return false;
},
ownKeys(target) {
if (api === null)
return [];
const numProtocolsBuf = Memory.alloc(pointerSize);
const protocolHandles = api.objc_copyProtocolList(numProtocolsBuf);
try {
const numProtocols = numProtocolsBuf.readUInt();
if (numProtocols !== numCachedProtocols) {
cachedProtocols = {};
for (let i = 0; i !== numProtocols; i++) {
const handle = protocolHandles.add(i * pointerSize).readPointer();
const name = api.protocol_getName(handle).readUtf8String();
cachedProtocols[name] = handle;
}
numCachedProtocols = numProtocols;
}
} finally {
api.free(protocolHandles);
}
return Object.keys(cachedProtocols);
},
getOwnPropertyDescriptor(target, property) {
return {
writable: false,
configurable: true,
enumerable: true
};
},
});
function hasProperty(name) {
if (registryBuiltins.has(name))
return true;
return findProtocol(name) !== null;
}
function findProtocol(name) {
let handle = cachedProtocols[name];
if (handle === undefined) {
handle = api.objc_getProtocol(Memory.allocUtf8String(name));
if (handle.isNull())
return null;
cachedProtocols[name] = handle;
numCachedProtocols++;
}
return new ObjCProtocol(handle);
}
function toJSON() {
return Object.keys(registry).reduce(function (r, name) {
r[name] = { handle: cachedProtocols[name] };
return r;
}, {});
}
function toString() {
return "ProtocolRegistry";
}
function valueOf() {
return "ProtocolRegistry";
}
return registry;
}
const objCObjectBuiltins = new Set([
"prototype",
"constructor",
"handle",
"hasOwnProperty",
"toJSON",
"toString",
"valueOf",
"equals",
"$kind",
"$super",
"$superClass",
"$class",
"$className",
"$moduleName",
"$protocols",
"$methods",
"$ownMethods",
"$ivars"
]);
function ObjCObject(handle, protocol, cachedIsClass, superSpecifier) {
let cachedClassHandle = null;
let cachedKind = null;
let cachedSuper = null;
let cachedSuperClass = null;
let cachedClass = null;
let cachedClassName = null;
let cachedModuleName = null;
let cachedProtocols = null;
let cachedMethodNames = null;
let cachedProtocolMethods = null;
let respondsToSelector = null;
const cachedMethods = {};
let cachedNativeMethodNames = null;
let cachedOwnMethodNames = null;
let cachedIvars = null;
handle = getHandle(handle);
if (cachedIsClass === undefined) {
// We need to ensure the class is realized, otherwise calling APIs like object_isClass() will crash.
// The first message delivery will realize the class, but users intercepting calls to objc_msgSend()
// and inspecting the first argument will run into this situation.
const klass = api.object_getClass(handle);
const key = klass.toString();
if (!realizedClasses.has(key)) {
api.objc_lookUpClass(api.class_getName(klass));
realizedClasses.add(key);
}
}
const self = new Proxy(this, {
has(target, property) {
return hasProperty(property);
},
get(target, property, receiver) {
switch (property) {
case "handle":
return handle;
case "prototype":
return target.prototype;
case "constructor":
return target.constructor;
case "hasOwnProperty":
return hasProperty;
case "toJSON":
return toJSON;
case "toString":
case "valueOf":
const descriptionImpl = receiver.description;
if (descriptionImpl !== undefined) {
const description = descriptionImpl.call(receiver);
if (description !== null)
return description.UTF8String.bind(description);
}
return function () {
return receiver.$className;
};
case "equals":
return equals;
case "$kind":
if (cachedKind === null) {
if (isClass())
cachedKind = api.class_isMetaClass(handle) ? 'meta-class' : 'class';
else
cachedKind = 'instance';
}
return cachedKind;
case "$super":
if (cachedSuper === null) {
const superHandle = api.class_getSuperclass(classHandle());
if (!superHandle.isNull()) {
const specifier = Memory.alloc(2 * pointerSize);
specifier.writePointer(handle);
specifier.add(pointerSize).writePointer(superHandle);
cachedSuper = [new ObjCObject(handle, undefined, cachedIsClass, specifier)];
} else {
cachedSuper = [null];
}
}
return cachedSuper[0];
case "$superClass":
if (cachedSuperClass === null) {
const superClassHandle = api.class_getSuperclass(classHandle());
if (!superClassHandle.isNull()) {
cachedSuperClass = [new ObjCObject(superClassHandle)];
} else {
cachedSuperClass = [null];
}
}
return cachedSuperClass[0];
case "$class":
if (cachedClass === null)
cachedClass = new ObjCObject(api.object_getClass(handle), undefined, true);
return cachedClass;
case "$className":
if (cachedClassName === null) {
if (superSpecifier)
cachedClassName = api.class_getName(superSpecifier.add(pointerSize).readPointer()).readUtf8String();
else if (isClass())
cachedClassName = api.class_getName(handle).readUtf8String();
else
cachedClassName = api.object_getClassName(handle).readUtf8String();
}
return cachedClassName;
case "$moduleName":
if (cachedModuleName === null) {
cachedModuleName = api.class_getImageName(classHandle()).readUtf8String();
}
return cachedModuleName;
case "$protocols":
if (cachedProtocols === null) {
cachedProtocols = {};
const numProtocolsBuf = Memory.alloc(pointerSize);
const protocolHandles = api.class_copyProtocolList(classHandle(), numProtocolsBuf);
if (!protocolHandles.isNull()) {
try {
const numProtocols = numProtocolsBuf.readUInt();
for (let i = 0; i !== numProtocols; i++) {
const protocolHandle = protocolHandles.add(i * pointerSize).readPointer();
const p = new ObjCProtocol(protocolHandle);
cachedProtocols[p.name] = p;
}
} finally {
api.free(protocolHandles);
}
}
}
return cachedProtocols;
case "$methods":
if (cachedNativeMethodNames === null) {
const klass = superSpecifier ? superSpecifier.add(pointerSize).readPointer() : classHandle();
const meta = api.object_getClass(klass);
const names = new Set();
let cur = meta;
do {
for (let methodName of collectMethodNames(cur, "+ "))
names.add(methodName);
cur = api.class_getSuperclass(cur);
} while (!cur.isNull());
cur = klass;
do {
for (let methodName of collectMethodNames(cur, "- "))
names.add(methodName);
cur = api.class_getSuperclass(cur);
} while (!cur.isNull());
cachedNativeMethodNames = Array.from(names);
}
return cachedNativeMethodNames;
case "$ownMethods":
if (cachedOwnMethodNames === null) {
const klass = superSpecifier ? superSpecifier.add(pointerSize).readPointer() : classHandle();
const meta = api.object_getClass(klass);
const classMethods = collectMethodNames(meta, "+ ");
const instanceMethods = collectMethodNames(klass, "- ");
cachedOwnMethodNames = classMethods.concat(instanceMethods);
}
return cachedOwnMethodNames;
case "$ivars":
if (cachedIvars === null) {
if (isClass())
cachedIvars = {};
else
cachedIvars = new ObjCIvars(self, classHandle());
}
return cachedIvars;
default:
if (typeof property === "symbol") {
return target[property];
}
if (protocol) {
const details = findProtocolMethod(property);
if (details === null || !details.implemented)
return undefined;
}
const wrapper = findMethodWrapper(property);
if (wrapper === null)
return undefined;
return wrapper;
}
},
set(target, property, value, receiver) {
return false;
},
ownKeys(target) {
if (cachedMethodNames === null) {
if (!protocol) {
const jsNames = {};
const nativeNames = {};
let cur = api.object_getClass(handle);
do {
const numMethodsBuf = Memory.alloc(pointerSize);
const methodHandles = api.class_copyMethodList(cur, numMethodsBuf);
const fullNamePrefix = isClass() ? "+ " : "- ";
try {
const numMethods = numMethodsBuf.readUInt();
for (let i = 0; i !== numMethods; i++) {
const methodHandle = methodHandles.add(i * pointerSize).readPointer();
const sel = api.method_getName(methodHandle);
const nativeName = api.sel_getName(sel).readUtf8String();
if (nativeNames[nativeName] !== undefined)
continue;
nativeNames[nativeName] = nativeName;
const jsName = jsMethodName(nativeName);
let serial = 2;
let name = jsName;
while (jsNames[name] !== undefined) {
serial++;
name = jsName + serial;
}
jsNames[name] = true;
const fullName = fullNamePrefix + nativeName;
if (cachedMethods[fullName] === undefined) {
const details = {
sel: sel,
handle: methodHandle,
wrapper: null
};
cachedMethods[fullName] = details;
cachedMethods[name] = details;
}
}
} finally {
api.free(methodHandles);
}
cur = api.class_getSuperclass(cur);
} while (!cur.isNull());
cachedMethodNames = Object.keys(jsNames);
} else {
const methodNames = [];
const protocolMethods = allProtocolMethods();
Object.keys(protocolMethods).forEach(function (methodName) {
if (methodName[0] !== '+' && methodName[0] !== '-') {
const details = protocolMethods[methodName];
if (details.implemented) {
methodNames.push(methodName);
}
}
});
cachedMethodNames = methodNames;
}
}
return ['handle'].concat(cachedMethodNames);
},
getOwnPropertyDescriptor(target, property) {
return {
writable: false,
configurable: true,
enumerable: true
};
},
});
if (protocol) {
respondsToSelector = !isClass() ? findMethodWrapper("- respondsToSelector:") : null;
}
return self;
function hasProperty(name) {
if (objCObjectBuiltins.has(name))
return true;
if (protocol) {
const details = findProtocolMethod(name);
return !!(details !== null && details.implemented);
}
return findMethod(name) !== null;
}
function classHandle() {
if (cachedClassHandle === null)
cachedClassHandle = isClass() ? handle : api.object_getClass(handle);
return cachedClassHandle;
}
function isClass() {
if (cachedIsClass === undefined) {
if (api.object_isClass)
cachedIsClass = !!api.object_isClass(handle);
else
cachedIsClass = !!api.class_isMetaClass(api.object_getClass(handle));
}
return cachedIsClass;
}
function findMethod(rawName) {
let method = cachedMethods[rawName];
if (method !== undefined)
return method;
const tokens = parseMethodName(rawName);
const fullName = tokens[2];
method = cachedMethods[fullName];
if (method !== undefined) {
cachedMethods[rawName] = method;
return method;
}
const kind = tokens[0];
const name = tokens[1];
const sel = selector(name);
const defaultKind = isClass() ? '+' : '-';
if (protocol) {
const details = findProtocolMethod(fullName);
if (details !== null) {
method = {
sel: sel,
types: details.types,
wrapper: null,
kind
};
}
}
if (method === undefined) {
const methodHandle = (kind === '+') ?
api.class_getClassMethod(classHandle(), sel) :
api.class_getInstanceMethod(classHandle(), sel);
if (!methodHandle.isNull()) {
method = {
sel: sel,
handle: methodHandle,
wrapper: null,
kind
};
} else {
if (isClass() || kind !== '-' || name === "forwardingTargetForSelector:" || name === "methodSignatureForSelector:") {
return null;
}
let target = self;
if ("- forwardingTargetForSelector:" in self) {
const forwardingTarget = self.forwardingTargetForSelector_(sel);
if (forwardingTarget !== null && forwardingTarget.$kind === 'instance') {
target = forwardingTarget;
} else {
return null;
}
} else {
return null;
}
const methodHandle = api.class_getInstanceMethod(api.object_getClass(target.handle), sel);
if (methodHandle.isNull()) {
return null;
}
let types = api.method_getTypeEncoding(methodHandle).readUtf8String();
if (types === null || types === "") {
types = stealTypesFromProtocols(target, fullName);
if (types === null)
types = stealTypesFromProtocols(self, fullName);
if (types === null)
return null;
}
method = {
sel,
types,
wrapper: null,
kind
};
}
}
cachedMethods[fullName] = method;
cachedMethods[rawName] = method;
if (kind === defaultKind)
cachedMethods[jsMethodName(name)] = method;
return method;
}
function stealTypesFromProtocols(klass, fullName) {
const candidates = Object.keys(klass.$protocols)
.map(protocolName => flatProtocolMethods({}, klass.$protocols[protocolName]))
.reduce((allMethods, methods) => {
Object.assign(allMethods, methods);
return allMethods;
}, {});
const method = candidates[fullName];
if (method === undefined) {
return null;
}
return method.types;
}
function flatProtocolMethods(result, protocol) {
if (protocol.methods !== undefined) {
Object.assign(result, protocol.methods);
}
if (protocol.protocol !== undefined) {
flatProtocolMethods(result, protocol.protocol);
}
return result;
}
function findProtocolMethod(rawName) {
const protocolMethods = allProtocolMethods();
const details = protocolMethods[rawName];
return (details !== undefined) ? details : null;
}
function allProtocolMethods() {
if (cachedProtocolMethods === null) {
const methods = {};
const protocols = collectProtocols(protocol);
const defaultKind = isClass() ? '+' : '-';
Object.keys(protocols).forEach(function (name) {
const p = protocols[name];
const m = p.methods;
Object.keys(m).forEach(function (fullMethodName) {
const method = m[fullMethodName];
const methodName = fullMethodName.substr(2);
const kind = fullMethodName[0];
let didCheckImplemented = false;
let implemented = false;
const details = {
types: method.types
};
Object.defineProperty(details, 'implemented', {
get() {
if (!didCheckImplemented) {
if (method.required) {
implemented = true;
} else {
implemented = (respondsToSelector !== null && respondsToSelector.call(self, selector(methodName)));
}
didCheckImplemented = true;
}
return implemented;
}
});
methods[fullMethodName] = details;
if (kind === defaultKind)
methods[jsMethodName(methodName)] = details;
});
});
cachedProtocolMethods = methods;
}
return cachedProtocolMethods;
}
function findMethodWrapper(name) {
const method = findMethod(name);
if (method === null)
return null;
let wrapper = method.wrapper;
if (wrapper === null) {
wrapper = makeMethodInvocationWrapper(method, self, superSpecifier, defaultInvocationOptions);
method.wrapper = wrapper;
}
return wrapper;
}
function parseMethodName(rawName) {
const match = /([+\-])\s(\S+)/.exec(rawName);
let name, kind;
if (match === null) {
kind = isClass() ? '+' : '-';
name = objcMethodName(rawName);
} else {
kind = match[1];
name = match[2];
}
const fullName = [kind, name].join(' ');
return [kind, name, fullName];
}
function toJSON() {
return {
handle: handle.toString()
};
}
function equals(ptr) {
return handle.equals(getHandle(ptr));
}
}
function getReplacementMethodImplementation(methodHandle) {
const existingEntry = replacedMethods.get(methodHandle.toString());
if (existingEntry === undefined)
return null;
const [, newImp] = existingEntry;
return newImp;
}
function replaceMethodImplementation(methodHandle, imp) {
const key = methodHandle.toString();
let oldImp;
const existingEntry = replacedMethods.get(key);
if (existingEntry !== undefined)
[oldImp] = existingEntry;
else
oldImp = api.method_getImplementation(methodHandle);
if (!imp.equals(oldImp))
replacedMethods.set(key, [oldImp, imp]);
else
replacedMethods.delete(key);
api.method_setImplementation(methodHandle, imp);
}
function collectMethodNames(klass, prefix) {
const names = [];
const numMethodsBuf = Memory.alloc(pointerSize);
const methodHandles = api.class_copyMethodList(klass, numMethodsBuf);
try {
const numMethods = numMethodsBuf.readUInt();
for (let i = 0; i !== numMethods; i++) {
const methodHandle = methodHandles.add(i * pointerSize).readPointer();
const sel = api.method_getName(methodHandle);
const nativeName = api.sel_getName(sel).readUtf8String();
names.push(prefix + nativeName);
}
} finally {
api.free(methodHandles);
}
return names;
}
function ObjCProtocol(handle) {
let cachedName = null;
let cachedProtocols = null;
let cachedProperties = null;
let cachedMethods = null;
Object.defineProperty(this, 'handle', {
value: handle,
enumerable: true
});
Object.defineProperty(this, 'name', {
get() {
if (cachedName === null)
cachedName = api.protocol_getName(handle).readUtf8String();
return cachedName;
},
enumerable: true
});
Object.defineProperty(this, 'protocols', {
get() {
if (cachedProtocols === null) {
cachedProtocols = {};
const numProtocolsBuf = Memory.alloc(pointerSize);
const protocolHandles = api.protocol_copyProtocolList(handle, numProtocolsBuf);
if (!protocolHandles.isNull()) {
try {
const numProtocols = numProtocolsBuf.readUInt();
for (let i = 0; i !== numProtocols; i++) {
const protocolHandle = protocolHandles.add(i * pointerSize).readPointer();
const protocol = new ObjCProtocol(protocolHandle);
cachedProtocols[protocol.name] = protocol;
}
} finally {
api.free(protocolHandles);
}
}
}
return cachedProtocols;
},
enumerable: true
});
Object.defineProperty(this, 'properties', {
get() {
if (cachedProperties === null) {
cachedProperties = {};
const numBuf = Memory.alloc(pointerSize);
const propertyHandles = api.protocol_copyPropertyList(handle, numBuf);
if (!propertyHandles.isNull()) {
try {
const numProperties = numBuf.readUInt();
for (let i = 0; i !== numProperties; i++) {
const propertyHandle = propertyHandles.add(i * pointerSize).readPointer();
const propName = api.property_getName(propertyHandle).readUtf8String();
const attributes = {};
const attributeEntries = api.property_copyAttributeList(propertyHandle, numBuf);
if (!attributeEntries.isNull()) {
try {
const numAttributeValues = numBuf.readUInt();
for (let j = 0; j !== numAttributeValues; j++) {
const attributeEntry = attributeEntries.add(j * (2 * pointerSize));
const name = attributeEntry.readPointer().readUtf8String();
const value = attributeEntry.add(pointerSize).readPointer().readUtf8String();
attributes[name] = value;
}
} finally {
api.free(attributeEntries);
}
}
cachedProperties[propName] = attributes;
}
} finally {
api.free(propertyHandles);
}
}
}
return cachedProperties;
},
enumerable: true
});
Object.defineProperty(this, 'methods', {
get() {
if (cachedMethods === null) {
cachedMethods = {};
const numBuf = Memory.alloc(pointerSize);
collectMethods(cachedMethods, numBuf, { required: true, instance: false });
collectMethods(cachedMethods, numBuf, { required: false, instance: false });
collectMethods(cachedMethods, numBuf, { required: true, instance: true });
collectMethods(cachedMethods, numBuf, { required: false, instance: true });
}
return cachedMethods;
},
enumerable: true
});
function collectMethods(methods, numBuf, spec) {
const methodDescValues = api.protocol_copyMethodDescriptionList(handle, spec.required ? 1 : 0, spec.instance ? 1 : 0, numBuf);
if (methodDescValues.isNull())
return;
try {
const numMethodDescValues = numBuf.readUInt();
for (let i = 0; i !== numMethodDescValues; i++) {
const methodDesc = methodDescValues.add(i * (2 * pointerSize));
const name = (spec.instance ? '- ' : '+ ') + selectorAsString(methodDesc.readPointer());
const types = methodDesc.add(pointerSize).readPointer().readUtf8String();
methods[name] = {
required: spec.required,
types: types
};
}
} finally {
api.free(methodDescValues);
}
}
}
const objCIvarsBuiltins = new Set([
"prototype",
"constructor",
"hasOwnProperty",
"toJSON",
"toString",
"valueOf"
]);
function ObjCIvars(instance, classHandle) {
const ivars = {};
let cachedIvarNames = null;
let classHandles = [];
let currentClassHandle = classHandle;
do {
classHandles.unshift(currentClassHandle);
currentClassHandle = api.class_getSuperclass(currentClassHandle);
} while (!currentClassHandle.isNull());
const numIvarsBuf = Memory.alloc(pointerSize);
classHandles.forEach(c => {
const ivarHandles = api.class_copyIvarList(c, numIvarsBuf);
try {
const numIvars = numIvarsBuf.readUInt();
for (let i = 0; i !== numIvars; i++) {
const handle = ivarHandles.add(i * pointerSize).readPointer();
const name = api.ivar_getName(handle).readUtf8String();
ivars[name] = [handle, null];
}
} finally {
api.free(ivarHandles);
}
});
const self = new Proxy(this, {
has(target, property) {
return hasProperty(property);
},
get(target, property, receiver) {
switch (property) {
case "prototype":
return target.prototype;
case "constructor":
return target.constructor;
case "hasOwnProperty":
return hasProperty;
case "toJSON":
return toJSON;
case "toString":
return toString;
case "valueOf":
return valueOf;
default:
const ivar = findIvar(property);
if (ivar === null)
return undefined;
return ivar.get();
}
},
set(target, property, value, receiver) {
const ivar = findIvar(property);
if (ivar === null)
throw new Error("Unknown ivar");
ivar.set(value);
return true;
},
ownKeys(target) {
if (cachedIvarNames === null)
cachedIvarNames = Object.keys(ivars);
return cachedIvarNames;
},
getOwnPropertyDescriptor(target, property) {
return {
writable: true,
configurable: true,
enumerable: true
};
},
});
return self;
function findIvar(name) {
const entry = ivars[name];
if (entry === undefined)
return null;
let impl = entry[1];
if (impl === null) {
const ivar = entry[0];
const offset = api.ivar_getOffset(ivar).toInt32();
const address = instance.handle.add(offset);
const type = parseType(api.ivar_getTypeEncoding(ivar).readUtf8String());
const fromNative = type.fromNative || identityTransform;
const toNative = type.toNative || identityTransform;
let read, write;
if (name === 'isa') {
read = readObjectIsa;
write = function () {
throw new Error('Unable to set the isa instance variable');
};
} else {
read = type.read;
write = type.write;
}
impl = {
get() {
return fromNative.call(instance, read(address));
},
set(value) {
write(address, toNative.call(instance, value));
}
};
entry[1] = impl;
}
return impl;
}
function hasProperty(name) {
if (objCIvarsBuiltins.has(name))
return true;
return ivars.hasOwnProperty(name);
}
function toJSON() {
return Object.keys(self).reduce(function (result, name) {
result[name] = self[name];
return result;
}, {});
}
function toString() {
return "ObjCIvars";
}
function valueOf() {
return "ObjCIvars";
}
}
let blockDescriptorAllocSize, blockDescriptorDeclaredSize, blockDescriptorOffsets;
let blockSize, blockOffsets;
if (pointerSize === 4) {
blockDescriptorAllocSize = 16; /* sizeof (BlockDescriptor) == 12 */
blockDescriptorDeclaredSize = 20;
blockDescriptorOffsets = {
reserved: 0,
size: 4,
rest: 8
};
blockSize = 20;
blockOffsets = {
isa: 0,
flags: 4,
reserved: 8,
invoke: 12,
descriptor: 16
};
} else {
blockDescriptorAllocSize = 32; /* sizeof (BlockDescriptor) == 24 */
blockDescriptorDeclaredSize = 32;
blockDescriptorOffsets = {
reserved: 0,
size: 8,
rest: 16
};
blockSize = 32;
blockOffsets = {
isa: 0,
flags: 8,
reserved: 12,
invoke: 16,
descriptor: 24
};
}
const BLOCK_HAS_COPY_DISPOSE = (1 << 25);
const BLOCK_HAS_CTOR = (1 << 26);
const BLOCK_IS_GLOBAL = (1 << 28);
const BLOCK_HAS_STRET = (1 << 29);
const BLOCK_HAS_SIGNATURE = (1 << 30);
function Block(target, options = defaultInvocationOptions) {
this._options = options;
if (target instanceof NativePointer) {
const descriptor = target.add(blockOffsets.descriptor).readPointer();
this.handle = target;
const flags = target.add(blockOffsets.flags).readU32();
if ((flags & BLOCK_HAS_SIGNATURE) !== 0) {
const signatureOffset = ((flags & BLOCK_HAS_COPY_DISPOSE) !== 0) ? 2 : 0;
this.types = descriptor.add(blockDescriptorOffsets.rest + (signatureOffset * pointerSize)).readPointer().readCString();
this._signature = parseSignature(this.types);
} else {
this._signature = null;
}
} else {
this.declare(target);
const descriptor = Memory.alloc(blockDescriptorAllocSize + blockSize);
const block = descriptor.add(blockDescriptorAllocSize);
const typesStr = Memory.allocUtf8String(this.types);
descriptor.add(blockDescriptorOffsets.reserved).writeULong(0);
descriptor.add(blockDescriptorOffsets.size).writeULong(blockDescriptorDeclaredSize);
descriptor.add(blockDescriptorOffsets.rest).writePointer(typesStr);
block.add(blockOffsets.isa).writePointer(classRegistry.__NSGlobalBlock__);
block.add(blockOffsets.flags).writeU32(BLOCK_HAS_SIGNATURE | BLOCK_IS_GLOBAL);
block.add(blockOffsets.reserved).writeU32(0);
block.add(blockOffsets.descriptor).writePointer(descriptor);
this.handle = block;
this._storage = [descriptor, typesStr];
this.implementation = target.implementation;
}
}
Object.defineProperties(Block.prototype, {
implementation: {
enumerable: true,
get() {
const address = this.handle.add(blockOffsets.invoke).readPointer().strip();
const signature = this._getSignature();
return makeBlockInvocationWrapper(this, signature, new NativeFunction(
a