feat: fix
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ anisette/*
|
|||||||
js/node_modules/*
|
js/node_modules/*
|
||||||
*.so
|
*.so
|
||||||
*.wasm
|
*.wasm
|
||||||
|
frontend/.vscode/extensions.json
|
||||||
103
AGENTS.md
103
AGENTS.md
@@ -1,91 +1,62 @@
|
|||||||
# AGENTS.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Every Agents when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This crate provides Apple Anisette authentication in browser via WebAssembly. It uses a custom Unicorn Engine fork (https://github.com/lbr77/unicorn/tree/tci-emscripten) to emulate ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) for generating Anisette headers.
|
Rust/WASM project that emulates ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) via a custom Unicorn Engine fork to generate Apple Anisette headers in the browser or Node.js.
|
||||||
|
|
||||||
**Note**: The Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included in this repository. Extract them from an APK or obtain separately.
|
The Android library blobs are not included — extract from an APK or obtain separately.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
### Prerequisites
|
Must source Emscripten before building WASM:
|
||||||
|
|
||||||
- Rust nightly (edition 2024)
|
|
||||||
- Emscripten SDK (for WASM builds)
|
|
||||||
|
|
||||||
### Setup Unicorn Engine
|
|
||||||
|
|
||||||
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lbr77/unicorn.git
|
source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
|
||||||
cd unicorn && git checkout tci-emscripten
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build Unicorn for Emscripten:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash script/rebuild-unicorn.sh
|
bun run build # WASM (debug) + JS bundle
|
||||||
|
bun run release # WASM (release) + JS bundle
|
||||||
|
bun run build:js # JS bundle only (no WASM rebuild)
|
||||||
|
bun run build:glue # WASM only
|
||||||
|
bun run build:unicorn # Rebuild Unicorn (rarely needed)
|
||||||
```
|
```
|
||||||
|
|
||||||
The rebuild script handles:
|
JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
|
||||||
- Running `emcmake cmake` with appropriate flags
|
|
||||||
- Building only `arm` and `aarch64` architectures
|
|
||||||
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
|
|
||||||
|
|
||||||
### Build WASM Glue
|
The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
|
||||||
|
|
||||||
```bash
|
|
||||||
bash script/build-glue.sh # Debug build
|
|
||||||
bash script/build-glue.sh --release # Release build
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
- `test/dist/anisette_rs.js` / `.wasm` (web)
|
|
||||||
- `test/dist/anisette_rs.node.js` / `.wasm` (Node.js)
|
|
||||||
- Copies to `../../frontend/public/anisette/`
|
|
||||||
|
|
||||||
### Run Native Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [apple_root_pem]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Node.js Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node example/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Core Modules
|
### Rust → WASM layer (`src/`)
|
||||||
|
|
||||||
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests
|
- `exports.rs` — all `#[no_mangle]` C FFI exports. Every new public function must also be added to `EXPORTED_FUNCTIONS` in `script/build-glue.sh` (both `WEB_EXPORTED_FUNCTIONS` and `NODE_EXPORTED_FUNCTIONS` as appropriate).
|
||||||
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls
|
- `adi.rs` — wraps the emulated ADI library calls
|
||||||
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions
|
- `emu.rs` — Unicorn ARM64 emulator core
|
||||||
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence
|
- `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
|
||||||
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
|
|
||||||
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
|
|
||||||
|
|
||||||
### Emulator Memory Layout
|
### JS/TS layer (`js/src/`)
|
||||||
|
|
||||||
- Libraries mapped to import address space with stub hooks
|
- `anisette.ts` — main `Anisette` class. **Each `getData()` call fully reinits the WASM state** (new `WasmBridge`, re-writes VFS files, re-calls `initFromBlobs`) to work around a Unicorn emulator bug that causes illegal writes on repeated use.
|
||||||
- Stack, heap, and return addresses pre-allocated
|
- `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
|
||||||
- Import hooks dispatch to runtime stubs
|
- `wasm-loader.ts` — thin wrapper around `ModuleFactory`; caller must pass `locateFile` via `moduleOverrides` to resolve the `.wasm` path
|
||||||
|
- `provisioning.ts` — Apple provisioning HTTP flow (fetches SPIM, sends CPIM)
|
||||||
|
- `device.ts` — loads or generates `device.json`
|
||||||
|
|
||||||
### Public API (exports.rs)
|
### Key design decisions
|
||||||
|
|
||||||
- `anisette_init_from_blobs` — Initialize from library blobs
|
- `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
|
||||||
- `anisette_is_machine_provisioned` — Check provisioning state
|
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
|
||||||
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow
|
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
|
||||||
- `anisette_request_otp` — Generate OTP + machine ID headers
|
- Browser FS/path gotcha:
|
||||||
|
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
|
||||||
|
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
|
||||||
|
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
|
||||||
|
|
||||||
### Data Flow
|
### Example usage
|
||||||
|
|
||||||
1. Load Android `libstoreservicescore.so` + `libCoreADI.so`
|
```bash
|
||||||
2. Initialize device identity (`device.json`)
|
NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
|
||||||
3. Provision with Apple (if needed)
|
<libstoreservicescore.so> <libCoreADI.so> [library_path]
|
||||||
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers
|
```
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ The `js/package.json` build script also outputs to `../dist/anisette.js` directl
|
|||||||
- `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
|
- `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
|
||||||
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
|
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
|
||||||
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
|
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
|
||||||
|
- Browser FS/path gotcha:
|
||||||
|
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
|
||||||
|
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
|
||||||
|
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
|
||||||
|
|
||||||
### Example usage
|
### Example usage
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ async function runDemo() {
|
|||||||
status.value = 'Initializing Anisette...'
|
status.value = 'Initializing Anisette...'
|
||||||
const anisette = await Anisette.fromSo(ssBytes, caBytes, wasmModule, {
|
const anisette = await Anisette.fromSo(ssBytes, caBytes, wasmModule, {
|
||||||
httpClient,
|
httpClient,
|
||||||
init: { libraryPath: '/anisette' }
|
init: { libraryPath: './anisette/' }
|
||||||
})
|
})
|
||||||
console.log(anisette.getDevice())
|
|
||||||
if (!anisette.isProvisioned) {
|
if (!anisette.isProvisioned) {
|
||||||
status.value = 'Provisioning...'
|
status.value = 'Provisioning...'
|
||||||
await anisette.provision()
|
await anisette.provision()
|
||||||
|
} else {
|
||||||
|
status.vaule = 'Already provisioned, skipping provisioning step'
|
||||||
}
|
}
|
||||||
|
|
||||||
status.value = 'Getting headers...'
|
status.value = 'Getting headers...'
|
||||||
@@ -52,7 +53,7 @@ async function runDemo() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Anisette JS Demo</h1>
|
<h1>Anisette Vue Demo</h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button @click="runDemo">Run</button>
|
<button @click="runDemo">Run</button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
toAppleClientTime,
|
toAppleClientTime,
|
||||||
detectLocale,
|
detectLocale,
|
||||||
encodeUtf8,
|
encodeUtf8,
|
||||||
|
decodeUtf8,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
|
||||||
const DEFAULT_DSID = BigInt(-2);
|
const DEFAULT_DSID = BigInt(-2);
|
||||||
@@ -91,31 +92,41 @@ export class Anisette {
|
|||||||
): Promise<Anisette> {
|
): Promise<Anisette> {
|
||||||
const bridge = new WasmBridge(wasmModule);
|
const bridge = new WasmBridge(wasmModule);
|
||||||
const initOpts = options.init ?? {};
|
const initOpts = options.init ?? {};
|
||||||
const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH;
|
const libraryPath = normalizeAdiPath(initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH);
|
||||||
const provisioningPath = initOpts.provisioningPath ?? libraryPath;
|
const provisioningPath = normalizeAdiPath(initOpts.provisioningPath ?? libraryPath);
|
||||||
const dsid = options.dsid ?? DEFAULT_DSID;
|
const dsid = options.dsid ?? DEFAULT_DSID;
|
||||||
|
|
||||||
// Load or generate device config
|
// Mount + load persisted IDBFS first so file state is stable before init.
|
||||||
const savedDeviceJson = initOpts.deviceJsonBytes
|
mountIdbfsPaths(bridge, libraryPath, provisioningPath);
|
||||||
? (() => { try { return JSON.parse(new TextDecoder().decode(initOpts.deviceJsonBytes)) as import("./types.js").DeviceJson; } catch { return null; } })()
|
try {
|
||||||
: null;
|
await bridge.syncIdbfsFromStorage();
|
||||||
const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig);
|
} catch {
|
||||||
|
// Ignore errors - might be first run with no existing data
|
||||||
|
}
|
||||||
|
|
||||||
// Restore adi.pb into VFS if provided
|
// Load device config from explicit bytes first, then from persisted VFS.
|
||||||
|
const savedDeviceJson =
|
||||||
|
parseDeviceJsonBytes(initOpts.deviceJsonBytes) ??
|
||||||
|
readDeviceJsonFromVfs(bridge, joinPath(libraryPath, "device.json"));
|
||||||
|
const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig);
|
||||||
|
const identifier = initOpts.identifier ?? device.adiIdentifier;
|
||||||
|
|
||||||
|
// Restore explicit adi.pb into VFS if provided.
|
||||||
if (initOpts.adiPb) {
|
if (initOpts.adiPb) {
|
||||||
bridge.writeVirtualFile(joinPath(provisioningPath, "adi.pb"), initOpts.adiPb);
|
bridge.writeVirtualFile(joinPath(provisioningPath, "adi.pb"), initOpts.adiPb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write device.json into WASM VFS
|
// Keep VFS device.json consistent with the active in-memory device.
|
||||||
const deviceJsonBytes = initOpts.deviceJsonBytes ?? encodeUtf8(JSON.stringify(device.toJson(), null, 2));
|
const deviceJsonBytes = initOpts.deviceJsonBytes ?? encodeUtf8(JSON.stringify(device.toJson(), null, 2));
|
||||||
bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes);
|
bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes);
|
||||||
|
|
||||||
// Initialize WASM ADI
|
// Initialize WASM ADI
|
||||||
bridge.initFromBlobs(
|
bridge.initFromBlobs(
|
||||||
libs.storeservicescore,
|
libs.storeservicescore,
|
||||||
libs.coreadi,
|
libs.coreadi,
|
||||||
libraryPath,
|
libraryPath,
|
||||||
provisioningPath,
|
provisioningPath,
|
||||||
initOpts.identifier ?? device.adiIdentifier
|
identifier
|
||||||
);
|
);
|
||||||
|
|
||||||
const provisioning = new ProvisioningSession(
|
const provisioning = new ProvisioningSession(
|
||||||
@@ -124,8 +135,6 @@ export class Anisette {
|
|||||||
options.httpClient
|
options.httpClient
|
||||||
);
|
);
|
||||||
|
|
||||||
const identifier = initOpts.identifier ?? device.adiIdentifier;
|
|
||||||
|
|
||||||
return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient);
|
return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +148,12 @@ export class Anisette {
|
|||||||
/** Run the provisioning flow against Apple servers. */
|
/** Run the provisioning flow against Apple servers. */
|
||||||
async provision(): Promise<void> {
|
async provision(): Promise<void> {
|
||||||
await this.provisioning.provision(this.dsid);
|
await this.provisioning.provision(this.dsid);
|
||||||
|
// Sync provisioning state to IndexedDB (browser only)
|
||||||
|
try {
|
||||||
|
await this.bridge.syncIdbfsToStorage();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors in Node.js or if IDBFS unavailable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read adi.pb from the WASM VFS for persistence. */
|
/** Read adi.pb from the WASM VFS for persistence. */
|
||||||
@@ -149,12 +164,27 @@ export class Anisette {
|
|||||||
/** Generate Anisette headers. Throws if not provisioned. */
|
/** Generate Anisette headers. Throws if not provisioned. */
|
||||||
async getData(): Promise<AnisetteHeaders> {
|
async getData(): Promise<AnisetteHeaders> {
|
||||||
// Reinit WASM state before each call to avoid emulator corruption on repeated use
|
// Reinit WASM state before each call to avoid emulator corruption on repeated use
|
||||||
const adiPb = this.bridge.readVirtualFile(joinPath(this.provisioningPath, "adi.pb"));
|
const adiPb = readOptionalFile(
|
||||||
|
this.bridge,
|
||||||
|
joinPath(this.provisioningPath, "adi.pb")
|
||||||
|
);
|
||||||
const deviceJsonBytes = encodeUtf8(JSON.stringify(this.device.toJson(), null, 2));
|
const deviceJsonBytes = encodeUtf8(JSON.stringify(this.device.toJson(), null, 2));
|
||||||
|
|
||||||
this.bridge = new WasmBridge(this.wasmModule);
|
this.bridge = new WasmBridge(this.wasmModule);
|
||||||
|
mountIdbfsPaths(this.bridge, this.libraryPath, this.provisioningPath);
|
||||||
|
try {
|
||||||
|
await this.bridge.syncIdbfsFromStorage();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors - might be first run or Node.js
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adiPb) {
|
||||||
this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb);
|
this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb);
|
||||||
|
}
|
||||||
this.bridge.writeVirtualFile(joinPath(this.libraryPath, "device.json"), deviceJsonBytes);
|
this.bridge.writeVirtualFile(joinPath(this.libraryPath, "device.json"), deviceJsonBytes);
|
||||||
|
|
||||||
this.bridge.initFromBlobs(this.libs.storeservicescore, this.libs.coreadi, this.libraryPath, this.provisioningPath, this.identifier);
|
this.bridge.initFromBlobs(this.libs.storeservicescore, this.libs.coreadi, this.libraryPath, this.provisioningPath, this.identifier);
|
||||||
|
|
||||||
this.provisioning = new ProvisioningSession(this.bridge, this.device, this.httpClient);
|
this.provisioning = new ProvisioningSession(this.bridge, this.device, this.httpClient);
|
||||||
|
|
||||||
const { otp, machineId } = this.bridge.requestOtp(this.dsid);
|
const { otp, machineId } = this.bridge.requestOtp(this.dsid);
|
||||||
@@ -195,3 +225,66 @@ function joinPath(base: string, file: string): string {
|
|||||||
const b = base.endsWith("/") ? base : `${base}/`;
|
const b = base.endsWith("/") ? base : `${base}/`;
|
||||||
return `${b}${file}`;
|
return `${b}${file}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAdiPath(path: string): string {
|
||||||
|
const trimmed = path.trim().replace(/\\/g, "/");
|
||||||
|
if (!trimmed || trimmed === "." || trimmed === "./" || trimmed === "/") {
|
||||||
|
return "./";
|
||||||
|
}
|
||||||
|
|
||||||
|
const noTrail = trimmed.replace(/\/+$/, "");
|
||||||
|
if (!noTrail || noTrail === ".") {
|
||||||
|
return "./";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noTrail.startsWith("./") || noTrail.startsWith("../")) {
|
||||||
|
return `${noTrail}/`;
|
||||||
|
}
|
||||||
|
if (noTrail.startsWith("/")) {
|
||||||
|
return `.${noTrail}/`;
|
||||||
|
}
|
||||||
|
return `./${noTrail}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountIdbfsPaths(
|
||||||
|
bridge: WasmBridge,
|
||||||
|
libraryPath: string,
|
||||||
|
provisioningPath: string
|
||||||
|
): void {
|
||||||
|
const paths = new Set([libraryPath, provisioningPath]);
|
||||||
|
for (const path of paths) {
|
||||||
|
bridge.initIdbfs(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalFile(bridge: WasmBridge, path: string): Uint8Array | null {
|
||||||
|
try {
|
||||||
|
return bridge.readVirtualFile(path);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDeviceJsonBytes(
|
||||||
|
bytes: Uint8Array | undefined
|
||||||
|
): import("./types.js").DeviceJson | null {
|
||||||
|
if (!bytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(decodeUtf8(bytes)) as import("./types.js").DeviceJson;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDeviceJsonFromVfs(
|
||||||
|
bridge: WasmBridge,
|
||||||
|
path: string
|
||||||
|
): import("./types.js").DeviceJson | null {
|
||||||
|
const bytes = readOptionalFile(bridge, path);
|
||||||
|
if (!bytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseDeviceJsonBytes(bytes);
|
||||||
|
}
|
||||||
|
|||||||
5782
js/src/anisette_rs.js
Normal file
5782
js/src/anisette_rs.js
Normal file
File diff suppressed because one or more lines are too long
5452
js/src/anisette_rs.node.js
Normal file
5452
js/src/anisette_rs.node.js
Normal file
File diff suppressed because one or more lines are too long
@@ -215,4 +215,89 @@ export class WasmBridge {
|
|||||||
machineId: this.readBytes(midPtr, midLen),
|
machineId: this.readBytes(midPtr, midLen),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.m.FS || !this.m.FS.filesystems?.IDBFS) {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
async syncIdbfsFromStorage(): Promise<void> {
|
||||||
|
if (!this.m.FS) {
|
||||||
|
return; // FS not available
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
async syncIdbfsToStorage(): Promise<void> {
|
||||||
|
if (!this.m.FS) {
|
||||||
|
return; // FS not available
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user