fix: read back sent copies after the sec 5.6 upstream fix, pinned at 2d65d66

This commit is contained in:
randogoth 2026-09-28 23:49:39 +03:00
parent 52947e153d
commit d6f4c434fa
36 changed files with 4223 additions and 3118 deletions

211
lib/native/client.dart Normal file
View file

@ -0,0 +1,211 @@
// The async surface over the raw ABI: every operation runs on a short-lived
// isolate via Isolate.run, because the Rust side blocks — a fetch can hold a
// thread for seconds, which must never be the UI thread. Handles are plain
// addresses (ints) and the master is bytes, so both cross isolates freely,
// and fumi-core's `Send + Sync` store is safe to call from any of them.
//
// Error codes cross in two ranges: 0-10 are wire statuses, 64 up are local
// failures (see ffi.dart) — recovery paths branch on the range. After a
// smolPanic the store's lock is poisoned and every later call panics; the
// UI treats that as "close and reopen the store", not an unsolvable error.
import "dart:isolate";
import "dart:typed_data";
import "package:smol_mail/native/ffi.dart";
class FumiNative {
FumiNative(this.dbPath);
/// The SQLite file the store owns; one store per process.
final String dbPath;
int? _store;
int? _account;
int? _flag;
/// The 32-byte master, held by the app (Hive in kirakira), never by the
/// library.
Uint8List? _master;
int? get store => _store;
/// The account handle, when a master is set; readers need it for describe.
int? get account => _account;
/// Binds the account locally, as register and restore do at their end.
void setAccount(String address) =>
SmolFfi.open().setAccount(_store!, address);
Future<T> _io<T>(T Function(SmolFfi ffi) op) => Isolate.run(() {
final ffi = SmolFfi.open();
return op(ffi);
});
Future<void> open() async {
if (_store != null) return;
_store = await _io((ffi) => ffi.openStore(dbPath));
_flag = await _io((ffi) => ffi.newFlag());
}
/// Drops the identity from memory the honest way Dart allows: the
/// master's bytes are overwritten before the reference goes, and the
/// account handle is freed. What persists is the caller's business
/// (the store wipes its own copy).
void clearMaster() {
_master?.fillRange(0, _master!.length, 0);
_master = null;
final old = _account;
if (old != null) {
SmolFfi.open().freeAccount(old);
}
_account = null;
}
/// Sets the master and rebuilds the account handle synchronously: the
/// rebuild is a few key derivations (microseconds native), not a network
/// operation, so it never needs an isolate — and widget tests can settle
/// it inside their fake-async zones.
void setMaster(Uint8List master, {int? rotations}) {
_master = master;
_rebuildAccount(rotations: rotations);
}
/// Rebuilds the account handle from the master and the store's rotation
/// index — required after register, restore and rotate, which all change
/// the index the account stands at.
void _rebuildAccount({int? rotations}) {
final ffi = SmolFfi.open();
final index = rotations ?? ffi.rotations(_store!);
final old = _account;
if (old != null) {
ffi.freeAccount(old);
}
_account = ffi.newAccount(_master!, index);
}
int get _s => _store!;
int get _a => _account!;
Future<void> close() async {
final store = _store;
final account = _account;
final flag = _flag;
_store = null;
_account = null;
_flag = null;
if (account != null) await _io((ffi) => ffi.freeAccount(account));
if (flag != null) await _io((ffi) => ffi.freeFlag(flag));
if (store != null) await _io((ffi) => ffi.closeStore(store));
}
// --- identity and pins ------------------------------------------------------
Future<String> accountPk() => _io((ffi) => ffi.accountPk(_a));
Future<String?> accountAddress() => _io((ffi) => ffi.accountAddress(_s));
Future<int> rotations() => _io((ffi) => ffi.rotations(_s));
Future<void> pinServer(String host, String keyB32) =>
_io((ffi) => ffi.pinServer(_s, host, keyB32));
Future<String?> serverPin(String host) =>
_io((ffi) => ffi.serverPin(_s, host));
Future<void> unpinServer(String host) =>
_io((ffi) => ffi.unpinServer(_s, host));
Future<List<dynamic>> pins() => _io((ffi) => ffi.pins(_s));
Future<bool> syncOk() => _io((ffi) => ffi.syncOk(_s));
Future<void> saveContact(String address, String keyB32,
{required bool verified}) =>
_io((ffi) => ffi.saveContact(_s, address, keyB32, verified: verified));
// --- account lifecycle ------------------------------------------------------
Future<void> register(String address,
{String? invite, String? dial, int timeout = 30}) =>
_io((ffi) {
ffi.register(_s, _a, address,
invite: invite, dial: dial, timeout: timeout);
}).then((_) => _rebuildAccount());
Future<void> restore(String address, {String? dial, int timeout = 30}) =>
_io((ffi) =>
ffi.restore(_s, _master!, address, dial: dial, timeout: timeout))
.then((_) => _rebuildAccount());
Future<Map<String, dynamic>> rotate({String? dial, int timeout = 30}) =>
_io((ffi) => ffi.rotate(_s, _a, dial: dial, timeout: timeout))
.then((out) {
_rebuildAccount();
return out;
});
// --- mail -------------------------------------------------------------------
/// Raises the cancellation flag; the running fetch stops between
/// envelopes and returns a partial summary.
void cancelFetch() => SmolFfi.open().setFlag(_flag ?? 0, true);
Future<Map<String, dynamic>> fetch(
{bool keep = false,
bool reset = false,
String? dial,
int timeout = 30}) =>
_io((ffi) {
ffi.setFlag(_flag!, false);
return ffi.fetch(_s, _a,
keep: keep, reset: reset, dial: dial, timeout: timeout,
cancel: _flag);
});
Future<Map<String, dynamic>> send(String to, String body,
{String? subject, String? replyTo, String? dial, int timeout = 30}) =>
_io((ffi) => ffi.send(_s, _a, to, body,
subject: subject, replyTo: replyTo, dial: dial));
Future<void> delete(List<String> idsHex, {String? dial, int timeout = 30}) =>
_io((ffi) => ffi.delete(_s, _a, idsHex, dial: dial, timeout: timeout));
/// The reader's delete: local removal plus seen-marking, no server round
/// trip. Use [delete] for the server-side DELETE.
Future<void> deleteLocal(String folder, List<String> idsHex) =>
_io((ffi) => ffi.deleteLocal(_s, folder, idsHex));
Future<List<dynamic>> mail(String folder) =>
_io((ffi) => ffi.mail(_s, folder));
Future<Map<String, dynamic>> describe(String idHex) =>
_io((ffi) => ffi.describe(_s, _a, idHex));
// --- contacts ---------------------------------------------------------------
Future<List<dynamic>> contacts() => _io((ffi) => ffi.contacts(_s));
Future<Map<String, dynamic>> contact(String address) =>
_io((ffi) => ffi.contact(_s, address));
Future<Map<String, dynamic>> resolve(String address,
{String? dial, int timeout = 30}) =>
_io((ffi) => ffi.resolve(_s, address, dial: dial, timeout: timeout));
Future<void> importContact(String uri) =>
_io((ffi) => ffi.importContact(_s, uri));
Future<int> accept(String address, {String? dial, int timeout = 30}) =>
_io((ffi) => ffi.accept(_s, _a, address, dial: dial, timeout: timeout));
Future<void> block(String address, {String? dial, int timeout = 30}) =>
_io((ffi) => ffi.block(_s, _a, address, dial: dial, timeout: timeout));
// --- backups ----------------------------------------------------------------
Future<String> exportBackup() => _io((ffi) => ffi.exportBackup(_s, _master!));
Future<String> importBackup(String text) =>
_io((ffi) => ffi.importBackup(_s, _master!, text));
}

518
lib/native/ffi.dart Normal file
View file

@ -0,0 +1,518 @@
// Raw FFI bindings for the C ABI in native/src/ffi.rs: one lookup per entry
// point, no logic. Handles are opaque pointers at this layer; the typed
// methods accept plain addresses (ints) so they cross isolates freely.
// Every string this ABI returns is freed with smolFreeString — Dart's GC
// runs no Rust destructor.
import "dart:convert";
import "dart:ffi";
import "dart:typed_data";
import "package:ffi/ffi.dart";
import "package:smol_mail/native/library.dart";
// Stable error codes, mirrored from native/src/ffi.rs. The space is split
// like the wire's: 0-10 are protocol status codes, verbatim, and local
// failures live from 64 up, so a recovery path can be picked by range —
// RATE_LIMITED (8) retries on the live session it arrived on, while
// HANDSHAKE_REFUSED (71) means there is no session to retry on.
const smolOk = 0;
const smolMalformed = 1;
const smolBadVersion = 2;
const smolUnknownUser = 3;
const smolAuthRequired = 4;
const smolAuthFailed = 5;
const smolQuotaExceeded = 6;
const smolTooLarge = 7;
const smolRateLimited = 8;
const smolNotPermitted = 9;
const smolInternalError = 10;
/// Unassigned status byte; the raw byte is in the payload's "status".
const smolUnknownStatus = 11;
const smolNotPinned = 64;
const smolPinMismatch = 65;
const smolKeyChanged = 66;
const smolNotRegistered = 67;
const smolChainLimit = 68;
const smolSchemaVersion = 69;
const smolUnreachable = 70;
const smolHandshakeRefused = 71;
const smolStorage = 72;
const smolNoise = 73;
const smolIo = 74;
const smolOther = 75;
const smolPanic = 76;
/// What a failed native call reports: the decision code plus the structured
/// payload the ABI carries (host, known, offered — keys as base32).
class NativeSmolException implements Exception {
final int code;
final String message;
final Map<String, dynamic> details;
const NativeSmolException(this.code, this.message, this.details);
@override
String toString() => message;
}
/// The raw ABI. Opened per isolate — handles are plain addresses, so the
/// same store can be called from any isolate, which is what the async
/// wrapper relies on.
class SmolFfi {
SmolFfi._(this._lib);
final DynamicLibrary _lib;
static SmolFfi? _cached;
factory SmolFfi.open() => _cached ??= SmolFfi._(openSmolLibrary());
Pointer<Void> _h(int address) => Pointer<Void>.fromAddress(address);
late final Pointer<Utf8> Function() _lastError = _lib
.lookupFunction<Pointer<Utf8> Function(), Pointer<Utf8> Function()>(
"smol_last_error");
late final void Function(Pointer<Utf8>) _freeString = _lib.lookupFunction<
Void Function(Pointer<Utf8>),
void Function(Pointer<Utf8>)>("smol_free_string");
late final Pointer<Void> Function(Pointer<Utf8>) _storeOpen = _lib
.lookupFunction<Pointer<Void> Function(Pointer<Utf8>),
Pointer<Void> Function(Pointer<Utf8>)>("smol_store_open");
late final Pointer<Void> Function() _storeMemory = _lib.lookupFunction<
Pointer<Void> Function(),
Pointer<Void> Function()>("smol_store_memory");
late final void Function(Pointer<Void>) _storeFree = _lib.lookupFunction<
Void Function(Pointer<Void>),
void Function(Pointer<Void>)>("smol_store_free");
late final Pointer<Void> Function(Pointer<Uint8>, int) _accountNew = _lib
.lookupFunction<
Pointer<Void> Function(Pointer<Uint8>, Uint32),
Pointer<Void> Function(
Pointer<Uint8>, int)>("smol_account_new");
late final void Function(Pointer<Void>) _accountFree = _lib.lookupFunction<
Void Function(Pointer<Void>),
void Function(Pointer<Void>)>("smol_account_free");
late final Pointer<Utf8> Function(Pointer<Void>) _accountPk = _lib
.lookupFunction<Pointer<Utf8> Function(Pointer<Void>),
Pointer<Utf8> Function(Pointer<Void>)>("smol_account_pk");
late final Pointer<Void> Function() _flagNew = _lib.lookupFunction<
Pointer<Void> Function(),
Pointer<Void> Function()>("smol_flag_new");
late final void Function(Pointer<Void>, int) _flagSet = _lib.lookupFunction<
Void Function(Pointer<Void>, Int32),
void Function(Pointer<Void>, int)>("smol_flag_set");
late final void Function(Pointer<Void>) _flagFree = _lib.lookupFunction<
Void Function(Pointer<Void>),
void Function(Pointer<Void>)>("smol_flag_free");
late final int Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>)
_pinServer = _lib.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>),
int Function(
Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>)>("smol_pin_server");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>)
_serverPin = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>)>(
"smol_server_pin");
late final Pointer<Utf8> Function(Pointer<Void>) _storeAccount = _lib
.lookupFunction<Pointer<Utf8> Function(Pointer<Void>),
Pointer<Utf8> Function(Pointer<Void>)>("smol_store_account");
late final int Function(Pointer<Void>) _rotations = _lib.lookupFunction<
Int32 Function(Pointer<Void>),
int Function(Pointer<Void>)>("smol_rotations");
late final int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, int) _register = _lib.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Uint64),
int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, int)
>("smol_register");
late final int Function(Pointer<Void>, Pointer<Uint8>, Pointer<Utf8>,
Pointer<Utf8>, int) _restore = _lib.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Uint8>, Pointer<Utf8>,
Pointer<Utf8>, Uint64),
int Function(Pointer<Void>, Pointer<Uint8>, Pointer<Utf8>,
Pointer<Utf8>, int)>("smol_restore");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>,
Pointer<Utf8>, int) _rotate = _lib.lookupFunction<
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Uint64),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
int)>("smol_rotate");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, int, int,
Pointer<Utf8>, int, Pointer<Void>) _fetch = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, Int32, Int32,
Pointer<Utf8>, Uint64, Pointer<Void>),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, int, int,
Pointer<Utf8>, int, Pointer<Void>)>("smol_fetch");
late final Pointer<Utf8> Function(
Pointer<Void>,
Pointer<Void>,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
int,
int,
Pointer<Utf8>,
int) _send = _lib.lookupFunction<
Pointer<Utf8> Function(
Pointer<Void>,
Pointer<Void>,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
Int32,
Int32,
Pointer<Utf8>,
Uint64),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, int, int,
Pointer<Utf8>, int)>("smol_send");
late final int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int) _delete = _lib.lookupFunction<
Int32 Function(
Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, Uint64),
int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int)>("smol_delete");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>) _mail = _lib
.lookupFunction<Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>),
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Utf8>)>("smol_mail");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>,
Pointer<Utf8>) _describe = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>),
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Void>, Pointer<Utf8>)>("smol_describe");
late final Pointer<Utf8> Function(Pointer<Void>) _contacts = _lib
.lookupFunction<Pointer<Utf8> Function(Pointer<Void>),
Pointer<Utf8> Function(Pointer<Void>)>("smol_contacts");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>) _contact =
_lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>)>(
"smol_contact");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>,
Pointer<Utf8>, Pointer<Utf8>, int) _accept = _lib.lookupFunction<
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, Uint64),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int)>("smol_accept");
late final int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int) _block = _lib.lookupFunction<
Int32 Function(
Pointer<Void>, Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, Uint64),
int Function(Pointer<Void>, Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int)>("smol_block");
late final Pointer<Utf8> Function(
Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, int) _resolve = _lib
.lookupFunction<
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, Uint64),
Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>,
Pointer<Utf8>, int)>("smol_resolve");
late final int Function(Pointer<Void>, Pointer<Utf8>) _accountSet = _lib
.lookupFunction<Int32 Function(Pointer<Void>, Pointer<Utf8>),
int Function(Pointer<Void>, Pointer<Utf8>)>("smol_account_set");
late final int Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, int)
_contactSave = _lib.lookupFunction<
Int32 Function(
Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>, Int32),
int Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
int)>("smol_contact_save");
late final int Function(Pointer<Void>, Pointer<Utf8>) _unpinServer = _lib
.lookupFunction<Int32 Function(Pointer<Void>, Pointer<Utf8>),
int Function(Pointer<Void>, Pointer<Utf8>)>("smol_unpin_server");
late final Pointer<Utf8> Function(Pointer<Void>) _pins = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>),
Pointer<Utf8> Function(Pointer<Void>)>("smol_pins");
late final int Function(Pointer<Void>) _syncOk = _lib.lookupFunction<
Int32 Function(Pointer<Void>),
int Function(Pointer<Void>)>("smol_sync_ok");
late final int Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>)
_deleteLocal = _lib.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>),
int Function(
Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>)>("smol_delete_local");
late final Pointer<Utf8> Function(Pointer<Utf8>) _parseAddress = _lib
.lookupFunction<Pointer<Utf8> Function(Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Utf8>)>("smol_parse_address");
late final int Function(Pointer<Void>, Pointer<Utf8>) _importContact = _lib
.lookupFunction<Int32 Function(Pointer<Void>, Pointer<Utf8>),
int Function(Pointer<Void>, Pointer<Utf8>)>("smol_import_contact");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Uint8>)
_exportBackup = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Void>, Pointer<Uint8>),
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Uint8>)>("smol_export_backup");
late final Pointer<Utf8> Function(Pointer<Void>, Pointer<Uint8>,
Pointer<Utf8>) _importBackup = _lib.lookupFunction<
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Uint8>, Pointer<Utf8>),
Pointer<Utf8> Function(
Pointer<Void>, Pointer<Uint8>, Pointer<Utf8>)>(
"smol_import_backup");
// --- helpers over the raw surface ------------------------------------------
/// Throws when a status call returned non-zero, decoding the error slot.
/// The slot is thread-local: this only works because it runs inside the
/// same Isolate.run closure that made the failing call — after the
/// closure returns, the slot on that thread is gone.
Never _fail() {
final slot = _lastError();
if (slot == nullptr) {
throw const NativeSmolException(smolOther, "the native call failed", {});
}
final text = take(slot);
final Map<String, dynamic> details;
try {
details = jsonDecode(text) as Map<String, dynamic>;
} on FormatException {
throw NativeSmolException(smolOther, text, {});
}
throw NativeSmolException(
details["code"] as int? ?? smolOther,
details["message"] as String? ?? "the native call failed",
details);
}
/// Reads one returned buffer into a String and frees the buffer — the
/// ownership rule every caller owes a returned pointer.
String take(Pointer<Utf8> ptr) {
if (ptr == nullptr) _fail();
final text = ptr.toDartString();
_freeString(ptr);
return text;
}
Pointer<Utf8> _text(String text) => text.toNativeUtf8();
Pointer<Uint8> _bytes(Uint8List bytes) {
final buf = malloc<Uint8>(32);
buf.asTypedList(32).setAll(0, bytes);
return buf;
}
// --- typed surface: handles cross as ints ----------------------------------
int openStore(String path) {
final ptr = _storeOpen(_text(path));
if (ptr == nullptr) _fail();
return ptr.address;
}
int openMemoryStore() {
final ptr = _storeMemory();
if (ptr == nullptr) _fail();
return ptr.address;
}
void closeStore(int store) => _storeFree(_h(store));
int newAccount(Uint8List master, int index) {
final buf = _bytes(master);
final ptr = _accountNew(buf, index);
malloc.free(buf);
if (ptr == nullptr) _fail();
return ptr.address;
}
void freeAccount(int account) => _accountFree(_h(account));
String accountPk(int account) => take(_accountPk(_h(account)));
int newFlag() => _flagNew().address;
void setFlag(int flag, bool value) => _flagSet(_h(flag), value ? 1 : 0);
void freeFlag(int flag) => _flagFree(_h(flag));
void pinServer(int store, String host, String keyB32) {
if (_pinServer(_h(store), _text(host), _text(keyB32)) != smolOk) _fail();
}
void unpinServer(int store, String host) {
if (_unpinServer(_h(store), _text(host)) != smolOk) _fail();
}
/// Every pin: [{host, key}] with keys as base32.
List<dynamic> pins(int store) =>
jsonDecode(take(_pins(_h(store)))) as List<dynamic>;
/// Whether the local accept-token set may replace the server's (sec 4).
bool syncOk(int store) => _syncOk(_h(store)) == 1;
void saveContact(int store, String address, String keyB32,
{required bool verified}) =>
_contactSave(
_h(store), _text(address), _text(keyB32), verified ? 1 : 0);
/// Null when nothing is pinned.
String? serverPin(int store, String host) {
final text = take(_serverPin(_h(store), _text(host)));
return text.isEmpty ? null : text;
}
/// Null when not registered.
String? accountAddress(int store) {
final text = take(_storeAccount(_h(store)));
return text.isEmpty ? null : text;
}
int rotations(int store) => _rotations(_h(store));
/// Binds the account locally, as register and restore do at their end.
void setAccount(int store, String address) {
if (_accountSet(_h(store), _text(address)) != smolOk) _fail();
}
void register(int store, int account, String address,
{String? invite, String? dial, int timeout = 30}) {
if (_register(
_h(store),
_h(account),
_text(address),
invite == null ? nullptr : _text(invite),
dial == null ? nullptr : _text(dial),
timeout) !=
smolOk) {
_fail();
}
}
void restore(int store, Uint8List master, String address,
{String? dial, int timeout = 30}) {
final buf = _bytes(master);
final code = _restore(_h(store), buf, _text(address),
dial == null ? nullptr : _text(dial), timeout);
malloc.free(buf);
if (code != smolOk) _fail();
}
Map<String, dynamic> rotate(int store, int account,
{String? dial, int timeout = 30}) =>
jsonDecode(take(_rotate(_h(store), _h(account),
dial == null ? nullptr : _text(dial), timeout)))
as Map<String, dynamic>;
Map<String, dynamic> fetch(int store, int account,
{bool keep = false,
bool reset = false,
String? dial,
int timeout = 30,
int? cancel}) {
final text = take(_fetch(_h(store), _h(account), keep ? 1 : 0, reset ? 1 : 0,
dial == null ? nullptr : _text(dial), timeout,
cancel == null ? nullptr : _h(cancel)));
return jsonDecode(text) as Map<String, dynamic>;
}
Map<String, dynamic> send(int store, int account, String to, String body,
{String? subject,
String? replyTo,
bool anonymous = false,
bool noPad = false,
String? dial,
int timeout = 30}) {
final text = take(_send(
_h(store),
_h(account),
_text(to),
subject == null ? nullptr : _text(subject),
_text(body),
replyTo == null ? nullptr : _text(replyTo),
anonymous ? 1 : 0,
noPad ? 1 : 0,
dial == null ? nullptr : _text(dial),
timeout));
return jsonDecode(text) as Map<String, dynamic>;
}
/// Ids are 64 hex characters everywhere — the same shape `mail` and
/// `describe` return, so no caller hex-decodes solely to delete.
void delete(int store, int account, List<String> ids,
{String? dial, int timeout = 30}) {
if (_delete(_h(store), _h(account), _text(jsonEncode(ids)),
dial == null ? nullptr : _text(dial), timeout) !=
smolOk) {
_fail();
}
}
List<dynamic> mail(int store, String folder) =>
jsonDecode(take(_mail(_h(store), _text(folder)))) as List<dynamic>;
Map<String, dynamic> describe(int store, int account, String idHex) =>
jsonDecode(take(_describe(_h(store), _h(account), _text(idHex))))
as Map<String, dynamic>;
List<dynamic> contacts(int store) =>
jsonDecode(take(_contacts(_h(store)))) as List<dynamic>;
Map<String, dynamic> contact(int store, String address) =>
jsonDecode(take(_contact(_h(store), _text(address))))
as Map<String, dynamic>;
int accept(int store, int account, String address,
{String? dial, int timeout = 30}) =>
(jsonDecode(take(_accept(_h(store), _h(account), _text(address),
dial == null ? nullptr : _text(dial), timeout)))
as Map<String, dynamic>)["held"] as int;
void block(int store, int account, String address,
{String? dial, int timeout = 30}) {
if (_block(_h(store), _h(account), _text(address),
dial == null ? nullptr : _text(dial), timeout) !=
smolOk) {
_fail();
}
}
Map<String, dynamic> resolve(int store, String address,
{String? dial, int timeout = 30}) {
final text = take(_resolve(_h(store), _text(address),
dial == null ? nullptr : _text(dial), timeout));
return jsonDecode(text) as Map<String, dynamic>;
}
/// The reader's delete: removes locally and marks seen, so a kept
/// server copy is not re-stored by the next fetch.
void deleteLocal(int store, String folder, List<String> ids) {
if (_deleteLocal(_h(store), _text(folder), _text(jsonEncode(ids))) !=
smolOk) {
_fail();
}
}
/// One address for the UI: {user, host, port, short, scheme}, or a
/// NativeSmolException when it does not parse.
Map<String, dynamic> parseAddress(String text) {
return jsonDecode(take(_parseAddress(_text(text))))
as Map<String, dynamic>;
}
void importContact(int store, String uri) {
if (_importContact(_h(store), _text(uri)) != smolOk) _fail();
}
String exportBackup(int store, Uint8List master) {
final buf = _bytes(master);
final text = take(_exportBackup(_h(store), buf));
malloc.free(buf);
return text;
}
String importBackup(int store, Uint8List master, String text) {
final buf = _bytes(master);
final summary = take(_importBackup(_h(store), buf, _text(text)));
malloc.free(buf);
return summary;
}
}

13
lib/native/library.dart Normal file
View file

@ -0,0 +1,13 @@
// Opens the native library (lib/smol -> fumi-core) the app links against.
// Android bundles the cdylib under its plain soname; Linux loads the built
// artifact from the checkout in dev/test and the bundle dir in release.
import "dart:ffi";
import "dart:io";
DynamicLibrary openSmolLibrary() {
if (Platform.isAndroid) {
return DynamicLibrary.open("libsmol_mail_native.so");
}
return DynamicLibrary.open("native/target/release/libsmol_mail_native.so");
}