317 lines
8.3 KiB
TypeScript
317 lines
8.3 KiB
TypeScript
// Low-level bridge to the Emscripten-generated WASM module.
|
|
// Handles all pointer/length marshalling so higher layers never touch raw memory.
|
|
|
|
export interface StartProvisioningResult {
|
|
cpim: Uint8Array;
|
|
session: number;
|
|
}
|
|
|
|
export interface RequestOtpResult {
|
|
otp: Uint8Array;
|
|
machineId: Uint8Array;
|
|
}
|
|
|
|
export class WasmBridge {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private m: any;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
constructor(wasmModule: any) {
|
|
this.m = wasmModule;
|
|
}
|
|
|
|
// ---- memory helpers ----
|
|
|
|
private allocBytes(bytes: Uint8Array): number {
|
|
const ptr = this.m._malloc(bytes.length) as number;
|
|
this.m.HEAPU8.set(bytes, ptr);
|
|
return ptr;
|
|
}
|
|
|
|
private allocCString(value: string | null | undefined): number {
|
|
if (!value) return 0;
|
|
const size = (this.m.lengthBytesUTF8(value) as number) + 1;
|
|
const ptr = this.m._malloc(size) as number;
|
|
this.m.stringToUTF8(value, ptr, size);
|
|
return ptr;
|
|
}
|
|
|
|
private readBytes(ptr: number, len: number): Uint8Array {
|
|
if (!ptr || !len) return new Uint8Array(0);
|
|
return (this.m.HEAPU8 as Uint8Array).slice(ptr, ptr + len);
|
|
}
|
|
|
|
private free(ptr: number): void {
|
|
if (ptr) this.m._free(ptr);
|
|
}
|
|
|
|
// ---- error handling ----
|
|
|
|
getLastError(): string {
|
|
const ptr = this.m._anisette_last_error_ptr() as number;
|
|
const len = this.m._anisette_last_error_len() as number;
|
|
if (!ptr || !len) return "";
|
|
const bytes = (this.m.HEAPU8 as Uint8Array).subarray(ptr, ptr + len);
|
|
return new TextDecoder("utf-8").decode(bytes);
|
|
}
|
|
|
|
private check(result: number, context: string): void {
|
|
if (result !== 0) {
|
|
const msg = this.getLastError();
|
|
throw new Error(`${context}: ${msg || "unknown error"}`);
|
|
}
|
|
}
|
|
|
|
// ---- public API ----
|
|
|
|
/**
|
|
* Initialize ADI from in-memory library blobs.
|
|
*/
|
|
initFromBlobs(
|
|
storeservices: Uint8Array,
|
|
coreadi: Uint8Array,
|
|
libraryPath: string,
|
|
provisioningPath?: string,
|
|
identifier?: string
|
|
): void {
|
|
const ssPtr = this.allocBytes(storeservices);
|
|
const caPtr = this.allocBytes(coreadi);
|
|
const libPtr = this.allocCString(libraryPath);
|
|
const provPtr = this.allocCString(provisioningPath ?? null);
|
|
const idPtr = this.allocCString(identifier ?? null);
|
|
|
|
try {
|
|
const result = this.m._anisette_init_from_blobs(
|
|
ssPtr,
|
|
storeservices.length,
|
|
caPtr,
|
|
coreadi.length,
|
|
libPtr,
|
|
provPtr,
|
|
idPtr
|
|
) as number;
|
|
this.check(result, "anisette_init_from_blobs");
|
|
} finally {
|
|
this.free(ssPtr);
|
|
this.free(caPtr);
|
|
this.free(libPtr);
|
|
this.free(provPtr);
|
|
this.free(idPtr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a file from the WASM virtual filesystem.
|
|
*/
|
|
readVirtualFile(filePath: string): Uint8Array {
|
|
const pathPtr = this.allocCString(filePath);
|
|
try {
|
|
const result = this.m._anisette_fs_read_file(pathPtr) as number;
|
|
this.check(result, `anisette_fs_read_file(${filePath})`);
|
|
} finally {
|
|
this.free(pathPtr);
|
|
}
|
|
const ptr = this.m._anisette_fs_read_ptr() as number;
|
|
const len = this.m._anisette_fs_read_len() as number;
|
|
return this.readBytes(ptr, len);
|
|
}
|
|
|
|
/**
|
|
* Write a file into the WASM virtual filesystem.
|
|
*/
|
|
writeVirtualFile(filePath: string, data: Uint8Array): void {
|
|
const pathPtr = this.allocCString(filePath);
|
|
const dataPtr = this.allocBytes(data);
|
|
try {
|
|
const result = this.m._anisette_fs_write_file(
|
|
pathPtr,
|
|
dataPtr,
|
|
data.length
|
|
) as number;
|
|
this.check(result, `anisette_fs_write_file(${filePath})`);
|
|
} finally {
|
|
this.free(pathPtr);
|
|
this.free(dataPtr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns 1 if provisioned, 0 if not, throws on error.
|
|
*/
|
|
isMachineProvisioned(dsid: bigint): boolean {
|
|
const result = this.m._anisette_is_machine_provisioned(dsid) as number;
|
|
if (result < 0) {
|
|
throw new Error(
|
|
`anisette_is_machine_provisioned: ${this.getLastError()}`
|
|
);
|
|
}
|
|
return result === 1;
|
|
}
|
|
|
|
/**
|
|
* Start provisioning — returns CPIM bytes and session handle.
|
|
*/
|
|
startProvisioning(
|
|
dsid: bigint,
|
|
spim: Uint8Array
|
|
): StartProvisioningResult {
|
|
const spimPtr = this.allocBytes(spim);
|
|
try {
|
|
const result = this.m._anisette_start_provisioning(
|
|
dsid,
|
|
spimPtr,
|
|
spim.length
|
|
) as number;
|
|
this.check(result, "anisette_start_provisioning");
|
|
} finally {
|
|
this.free(spimPtr);
|
|
}
|
|
|
|
const cpimPtr = this.m._anisette_get_cpim_ptr() as number;
|
|
const cpimLen = this.m._anisette_get_cpim_len() as number;
|
|
const session = this.m._anisette_get_session() as number;
|
|
|
|
return {
|
|
cpim: this.readBytes(cpimPtr, cpimLen),
|
|
session,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Finish provisioning with PTM and TK from Apple servers.
|
|
*/
|
|
endProvisioning(session: number, ptm: Uint8Array, tk: Uint8Array): void {
|
|
const ptmPtr = this.allocBytes(ptm);
|
|
const tkPtr = this.allocBytes(tk);
|
|
try {
|
|
const result = this.m._anisette_end_provisioning(
|
|
session,
|
|
ptmPtr,
|
|
ptm.length,
|
|
tkPtr,
|
|
tk.length
|
|
) as number;
|
|
this.check(result, "anisette_end_provisioning");
|
|
} finally {
|
|
this.free(ptmPtr);
|
|
this.free(tkPtr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request OTP — returns OTP bytes and machine ID bytes.
|
|
*/
|
|
requestOtp(dsid: bigint): RequestOtpResult {
|
|
const result = this.m._anisette_request_otp(dsid) as number;
|
|
this.check(result, "anisette_request_otp");
|
|
|
|
const otpPtr = this.m._anisette_get_otp_ptr() as number;
|
|
const otpLen = this.m._anisette_get_otp_len() as number;
|
|
const midPtr = this.m._anisette_get_mid_ptr() as number;
|
|
const midLen = this.m._anisette_get_mid_len() as number;
|
|
|
|
return {
|
|
otp: this.readBytes(otpPtr, otpLen),
|
|
machineId: this.readBytes(midPtr, midLen),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if IDBFS is available (browser environment only).
|
|
*/
|
|
isIdbfsAvailable(): boolean {
|
|
try {
|
|
return !!(this.m.FS && this.m.FS.filesystems?.IDBFS);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize IDBFS for browser persistence.
|
|
* Only works in browser environments with IDBFS available.
|
|
*/
|
|
initIdbfs(path: string): void {
|
|
// Check if FS and IDBFS are available (browser only)
|
|
if (!this.isIdbfsAvailable()) {
|
|
return; // Node.js or environment without IDBFS
|
|
}
|
|
|
|
const normalizedPath = this.normalizeMountPath(path);
|
|
|
|
// Create directory structure
|
|
if (normalizedPath !== "/") {
|
|
try {
|
|
this.m.FS.mkdirTree(normalizedPath);
|
|
} catch {
|
|
// Directory already exists, ignore
|
|
}
|
|
}
|
|
|
|
// Mount IDBFS
|
|
try {
|
|
this.m.FS.mount(this.m.FS.filesystems.IDBFS, {}, normalizedPath);
|
|
} catch {
|
|
// Already mounted, ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync IDBFS from IndexedDB to memory (async).
|
|
* Must be called after initIdbfs to load existing data from IndexedDB.
|
|
* Only works in browser environments with IDBFS available.
|
|
*/
|
|
async syncIdbfsFromStorage(): Promise<void> {
|
|
if (!this.isIdbfsAvailable()) {
|
|
return; // IDBFS not available, skip silently
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.m.FS.syncfs(true, (err: Error | null) => {
|
|
if (err) {
|
|
console.error("[anisette] IDBFS sync from storage failed:", err);
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sync IDBFS from memory to IndexedDB (async).
|
|
* Must be called after modifying files to persist them.
|
|
* Only works in browser environments with IDBFS available.
|
|
*/
|
|
async syncIdbfsToStorage(): Promise<void> {
|
|
if (!this.isIdbfsAvailable()) {
|
|
return; // IDBFS not available, skip silently
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.m.FS.syncfs(false, (err: Error | null) => {
|
|
if (err) {
|
|
console.error("[anisette] IDBFS sync to storage failed:", err);
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private normalizeMountPath(path: string): string {
|
|
const trimmed = path.trim();
|
|
const noSlash = trimmed.replace(/\/+$/, "");
|
|
const noDot = noSlash.startsWith("./") ? noSlash.slice(2) : noSlash;
|
|
|
|
if (!noDot || noDot === ".") {
|
|
return "/";
|
|
} else if (noDot.startsWith("/")) {
|
|
return noDot;
|
|
} else {
|
|
return "/" + noDot;
|
|
}
|
|
}
|
|
}
|