feat: работа с шифрованным текстом/фото
This commit is contained in:
@@ -26,12 +26,18 @@ class UploadDone extends UploadEvent {
|
|||||||
final String? url;
|
final String? url;
|
||||||
final String filename;
|
final String filename;
|
||||||
final int size;
|
final int size;
|
||||||
|
|
||||||
|
/// Server-assigned message id. Without it the optimistic message keeps its
|
||||||
|
/// local temp id and download URLs cannot be resolved until a restart.
|
||||||
|
final String? messageId;
|
||||||
|
|
||||||
const UploadDone({
|
const UploadDone({
|
||||||
required this.fileId,
|
required this.fileId,
|
||||||
required this.filename,
|
required this.filename,
|
||||||
required this.size,
|
required this.size,
|
||||||
this.token,
|
this.token,
|
||||||
this.url,
|
this.url,
|
||||||
|
this.messageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,14 +145,14 @@ class FileUploader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ok = await messages.sendFileMessage(
|
final messageId = await messages.sendFileMessage(
|
||||||
chatId,
|
chatId,
|
||||||
info.fileId,
|
info.fileId,
|
||||||
token: info.token,
|
token: info.token,
|
||||||
scheduledTime: scheduledTime,
|
scheduledTime: scheduledTime,
|
||||||
);
|
);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (!ok) {
|
if (messageId == null) {
|
||||||
ctrl.add(const UploadError('send_failed'));
|
ctrl.add(const UploadError('send_failed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -158,6 +164,7 @@ class FileUploader {
|
|||||||
url: info.url,
|
url: info.url,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
size: totalSize,
|
size: totalSize,
|
||||||
|
messageId: messageId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1314,7 +1314,10 @@ class MessagesModule {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> sendFileMessage(
|
/// Returns the server-assigned message id, or null on failure. The id is
|
||||||
|
/// required to later resolve a download URL — a message still carrying its
|
||||||
|
/// local temp id resolves to messageId 0 and the server rejects it.
|
||||||
|
Future<String?> sendFileMessage(
|
||||||
int chatId,
|
int chatId,
|
||||||
int fileId, {
|
int fileId, {
|
||||||
String? token,
|
String? token,
|
||||||
@@ -1343,12 +1346,12 @@ class MessagesModule {
|
|||||||
}
|
}
|
||||||
final payload = {'chatId': chatId, 'message': message, 'notify': notify};
|
final payload = {'chatId': chatId, 'message': message, 'notify': notify};
|
||||||
|
|
||||||
return _sendWithNotReadyRetry<bool>(
|
return _sendWithNotReadyRetry<String?>(
|
||||||
payload: payload,
|
payload: payload,
|
||||||
maxAttempts: maxAttempts,
|
maxAttempts: maxAttempts,
|
||||||
retryDelay: retryDelay,
|
retryDelay: retryDelay,
|
||||||
onResult: (response) => response.isOk,
|
onResult: (response) => _sentMessageMap(response)?['id']?.toString(),
|
||||||
onExhausted: false,
|
onExhausted: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:komet_crypto/komet_crypto.dart' as kc;
|
||||||
|
|
||||||
|
import '../storage/chat_encryption_store.dart';
|
||||||
|
import '../utils/logger.dart';
|
||||||
|
|
||||||
|
const int kMaxEncryptedMessageLength = 1000;
|
||||||
|
|
||||||
|
enum CryptoFailure { noKey, wrongKey, notEncrypted, malformed, unavailable }
|
||||||
|
|
||||||
|
class CryptoResult {
|
||||||
|
final String? text;
|
||||||
|
final CryptoFailure? failure;
|
||||||
|
|
||||||
|
const CryptoResult.ok(String this.text) : failure = null;
|
||||||
|
const CryptoResult.failed(CryptoFailure this.failure) : text = null;
|
||||||
|
|
||||||
|
bool get isOk => text != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatCryptoService {
|
||||||
|
ChatCryptoService._() {
|
||||||
|
ChatEncryptionStore.instance.revision.addListener(clearKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final ChatCryptoService instance = ChatCryptoService._();
|
||||||
|
|
||||||
|
final Map<String, Uint8List> _keys = {};
|
||||||
|
final Map<String, Future<Uint8List?>> _pending = {};
|
||||||
|
Future<void>? _init;
|
||||||
|
bool _unavailable = false;
|
||||||
|
|
||||||
|
String _cacheKey(int accountId, int chatId) => '$accountId/$chatId';
|
||||||
|
|
||||||
|
void clearKeys() {
|
||||||
|
_keys.clear();
|
||||||
|
_pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _ensureInitialized() async {
|
||||||
|
if (_unavailable) return false;
|
||||||
|
try {
|
||||||
|
await (_init ??= kc.RustLib.init());
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_init = null;
|
||||||
|
_unavailable = true;
|
||||||
|
logger.w('komet_crypto init failed: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> _keyFor(int accountId, int chatId) {
|
||||||
|
final cacheKey = _cacheKey(accountId, chatId);
|
||||||
|
final cached = _keys[cacheKey];
|
||||||
|
if (cached != null) return Future.value(cached);
|
||||||
|
return _pending[cacheKey] ??= _deriveKey(accountId, chatId, cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> _deriveKey(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
String cacheKey,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
if (!await _ensureInitialized()) return null;
|
||||||
|
final password = await ChatEncryptionStore.instance.readKey(
|
||||||
|
accountId,
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
if (password == null || password.isEmpty) return null;
|
||||||
|
final key = await kc.deriveKey(password: password);
|
||||||
|
_keys[cacheKey] = key;
|
||||||
|
return key;
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('derive key for chat $chatId: $e');
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
_pending.remove(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEnabled(int accountId, int chatId) =>
|
||||||
|
ChatEncryptionStore.instance.isEnabled(accountId, chatId);
|
||||||
|
|
||||||
|
Future<void> warmKey(int accountId, int chatId) => _keyFor(accountId, chatId);
|
||||||
|
|
||||||
|
Future<CryptoResult> encrypt(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
String plaintext,
|
||||||
|
) async {
|
||||||
|
final key = await _keyFor(accountId, chatId);
|
||||||
|
if (key == null) {
|
||||||
|
return CryptoResult.failed(
|
||||||
|
_unavailable ? CryptoFailure.unavailable : CryptoFailure.noKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return CryptoResult.ok(
|
||||||
|
await kc.encryptMessage(plaintext: plaintext, key: key),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('encrypt for chat $chatId: $e');
|
||||||
|
return const CryptoResult.failed(CryptoFailure.unavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CryptoResult> decrypt(int accountId, int chatId, String text) async {
|
||||||
|
final key = await _keyFor(accountId, chatId);
|
||||||
|
if (key == null) {
|
||||||
|
return CryptoResult.failed(
|
||||||
|
_unavailable ? CryptoFailure.unavailable : CryptoFailure.noKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return CryptoResult.ok(await kc.decryptMessage(text: text, key: key));
|
||||||
|
} catch (e) {
|
||||||
|
return CryptoResult.failed(_failureFromCode(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CryptoFailure?> encryptImageFile(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
String sourcePath,
|
||||||
|
String destPath,
|
||||||
|
) => _imageOp(
|
||||||
|
accountId,
|
||||||
|
chatId,
|
||||||
|
() => kc.encryptImageFile(
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
destPath: destPath,
|
||||||
|
key: _keys[_cacheKey(accountId, chatId)]!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<CryptoFailure?> decryptImageFile(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
String sourcePath,
|
||||||
|
String destPath,
|
||||||
|
) => _imageOp(
|
||||||
|
accountId,
|
||||||
|
chatId,
|
||||||
|
() => kc.decryptImageFile(
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
destPath: destPath,
|
||||||
|
key: _keys[_cacheKey(accountId, chatId)]!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<CryptoFailure?> _imageOp(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
Future<void> Function() run,
|
||||||
|
) async {
|
||||||
|
final key = await _keyFor(accountId, chatId);
|
||||||
|
if (key == null) {
|
||||||
|
return _unavailable ? CryptoFailure.unavailable : CryptoFailure.noKey;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await run();
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('image crypto for chat $chatId: $e');
|
||||||
|
return _failureFromCode(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> looksEncryptedImage(String path) async {
|
||||||
|
if (!await _ensureInitialized()) return false;
|
||||||
|
try {
|
||||||
|
return await kc.looksEncryptedImageFile(path: path);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> looksEncrypted(String text) async {
|
||||||
|
if (!await _ensureInitialized()) return false;
|
||||||
|
try {
|
||||||
|
return await kc.looksEncrypted(text: text);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CryptoFailure _failureFromCode(String message) {
|
||||||
|
if (message.contains('wrong_key')) return CryptoFailure.wrongKey;
|
||||||
|
if (message.contains('not_encrypted')) return CryptoFailure.notEncrypted;
|
||||||
|
if (message.contains('malformed')) return CryptoFailure.malformed;
|
||||||
|
return CryptoFailure.unavailable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import '../utils/logger.dart';
|
||||||
|
import '../utils/media_cache.dart';
|
||||||
|
import 'chat_crypto_service.dart';
|
||||||
|
|
||||||
|
const String kEncryptedPhotoExtension = '.png';
|
||||||
|
|
||||||
|
class EncryptedPhotoResult {
|
||||||
|
final File? file;
|
||||||
|
final CryptoFailure? failure;
|
||||||
|
|
||||||
|
const EncryptedPhotoResult.ok(File this.file) : failure = null;
|
||||||
|
const EncryptedPhotoResult.failed(CryptoFailure this.failure) : file = null;
|
||||||
|
|
||||||
|
bool get isOk => file != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-encodes an arbitrary image into lossless PNG. Encryption needs a format
|
||||||
|
/// that survives byte-for-byte; a re-encoded JPEG would not.
|
||||||
|
Future<File?> reencodeAsPng(File source, String destPath) async {
|
||||||
|
try {
|
||||||
|
final bytes = await source.readAsBytes();
|
||||||
|
final codec = await ui.instantiateImageCodec(bytes);
|
||||||
|
final frame = await codec.getNextFrame();
|
||||||
|
final data = await frame.image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
frame.image.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
if (data == null) return null;
|
||||||
|
final dest = File(destPath);
|
||||||
|
await dest.writeAsBytes(data.buffer.asUint8List(), flush: true);
|
||||||
|
return dest;
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('png re-encode failed: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Directory> _scratchDir() async {
|
||||||
|
final dir = Directory('${(await getTemporaryDirectory()).path}/komet_enc');
|
||||||
|
if (!await dir.exists()) await dir.create(recursive: true);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picked image → PNG → encrypted noise PNG, ready to upload as a file.
|
||||||
|
Future<EncryptedPhotoResult> prepareEncryptedPhoto({
|
||||||
|
required int accountId,
|
||||||
|
required int chatId,
|
||||||
|
required File source,
|
||||||
|
required String stamp,
|
||||||
|
}) async {
|
||||||
|
final dir = await _scratchDir();
|
||||||
|
final pngPath = '${dir.path}/plain_$stamp.png';
|
||||||
|
final encPath = '${dir.path}/enc_$stamp.png';
|
||||||
|
|
||||||
|
final png = await reencodeAsPng(source, pngPath);
|
||||||
|
if (png == null) {
|
||||||
|
return const EncryptedPhotoResult.failed(CryptoFailure.malformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
final failure = await ChatCryptoService.instance.encryptImageFile(
|
||||||
|
accountId,
|
||||||
|
chatId,
|
||||||
|
png.path,
|
||||||
|
encPath,
|
||||||
|
);
|
||||||
|
await _quietDelete(png);
|
||||||
|
if (failure != null) return EncryptedPhotoResult.failed(failure);
|
||||||
|
return EncryptedPhotoResult.ok(File(encPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloaded noise PNG → original photo, cached for the viewer.
|
||||||
|
Future<EncryptedPhotoResult> openEncryptedPhoto({
|
||||||
|
required int accountId,
|
||||||
|
required int chatId,
|
||||||
|
required File encrypted,
|
||||||
|
required String cacheName,
|
||||||
|
}) async {
|
||||||
|
final target = await MediaCache.fileFor('decrypted_$cacheName');
|
||||||
|
if (await target.exists() && await target.length() > 0) {
|
||||||
|
return EncryptedPhotoResult.ok(target);
|
||||||
|
}
|
||||||
|
final failure = await ChatCryptoService.instance.decryptImageFile(
|
||||||
|
accountId,
|
||||||
|
chatId,
|
||||||
|
encrypted.path,
|
||||||
|
target.path,
|
||||||
|
);
|
||||||
|
if (failure != null) {
|
||||||
|
await _quietDelete(target);
|
||||||
|
return EncryptedPhotoResult.failed(failure);
|
||||||
|
}
|
||||||
|
return EncryptedPhotoResult.ok(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _quietDelete(File file) async {
|
||||||
|
try {
|
||||||
|
if (await file.exists()) await file.delete();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../storage/chat_encryption_store.dart';
|
||||||
|
import 'chat_crypto_service.dart';
|
||||||
|
|
||||||
|
enum MessageDecryptionState { decrypted, wrongKey }
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MessageDecryption {
|
||||||
|
final String? plaintext;
|
||||||
|
final MessageDecryptionState state;
|
||||||
|
|
||||||
|
const MessageDecryption.decrypted(String this.plaintext)
|
||||||
|
: state = MessageDecryptionState.decrypted;
|
||||||
|
|
||||||
|
const MessageDecryption.wrongKey()
|
||||||
|
: plaintext = null,
|
||||||
|
state = MessageDecryptionState.wrongKey;
|
||||||
|
|
||||||
|
bool get isDecrypted => state == MessageDecryptionState.decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageDecryptionCache {
|
||||||
|
MessageDecryptionCache._() {
|
||||||
|
ChatEncryptionStore.instance.revision.addListener(clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final MessageDecryptionCache instance = MessageDecryptionCache._();
|
||||||
|
|
||||||
|
static const int _maxEntries = 1000;
|
||||||
|
|
||||||
|
final LinkedHashMap<String, ValueNotifier<MessageDecryption?>> _entries =
|
||||||
|
LinkedHashMap();
|
||||||
|
final Set<String> _inFlight = {};
|
||||||
|
|
||||||
|
ValueListenable<MessageDecryption?> listenableFor(String messageId) =>
|
||||||
|
_entryFor(messageId);
|
||||||
|
|
||||||
|
ValueNotifier<MessageDecryption?> _entryFor(String messageId) =>
|
||||||
|
_entries[messageId] ??= ValueNotifier<MessageDecryption?>(null);
|
||||||
|
|
||||||
|
void _evictStale(String keep) {
|
||||||
|
while (_entries.length > _maxEntries) {
|
||||||
|
final oldest = _entries.keys.first;
|
||||||
|
if (oldest == keep) break;
|
||||||
|
_entries.remove(oldest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void seed(String messageId, String plaintext) {
|
||||||
|
_entryFor(messageId).value = MessageDecryption.decrypted(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
void adopt(String fromMessageId, String toMessageId) {
|
||||||
|
final value = _entries[fromMessageId]?.value;
|
||||||
|
if (value != null) _entryFor(toMessageId).value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void request({
|
||||||
|
required int accountId,
|
||||||
|
required int chatId,
|
||||||
|
required String messageId,
|
||||||
|
required String cipherText,
|
||||||
|
}) {
|
||||||
|
if (cipherText.isEmpty) return;
|
||||||
|
if (!ChatCryptoService.instance.isEnabled(accountId, chatId)) return;
|
||||||
|
if (_entryFor(messageId).value != null) return;
|
||||||
|
if (!_inFlight.add(messageId)) return;
|
||||||
|
_evictStale(messageId);
|
||||||
|
unawaited(_resolve(accountId, chatId, messageId, cipherText));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _resolve(
|
||||||
|
int accountId,
|
||||||
|
int chatId,
|
||||||
|
String messageId,
|
||||||
|
String cipherText,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final crypto = ChatCryptoService.instance;
|
||||||
|
final result = await crypto.decrypt(accountId, chatId, cipherText);
|
||||||
|
if (result.isOk) {
|
||||||
|
_entryFor(messageId).value = MessageDecryption.decrypted(result.text!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (result.failure) {
|
||||||
|
case CryptoFailure.wrongKey:
|
||||||
|
_entryFor(messageId).value = const MessageDecryption.wrongKey();
|
||||||
|
case CryptoFailure.noKey:
|
||||||
|
if (await crypto.looksEncrypted(cipherText)) {
|
||||||
|
_entryFor(messageId).value = const MessageDecryption.wrongKey();
|
||||||
|
}
|
||||||
|
case CryptoFailure.notEncrypted:
|
||||||
|
case CryptoFailure.malformed:
|
||||||
|
case CryptoFailure.unavailable:
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_inFlight.remove(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_entries.clear();
|
||||||
|
_inFlight.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'per_chat_json_store.dart';
|
||||||
|
import 'token_storage.dart';
|
||||||
|
|
||||||
|
class ChatEncryptionStore extends PerChatJsonStore<bool> {
|
||||||
|
ChatEncryptionStore._()
|
||||||
|
: super(
|
||||||
|
prefsKey: 'chat_encryption',
|
||||||
|
fromJson: (raw) => raw == true ? true : null,
|
||||||
|
toJson: (value) => value,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final ChatEncryptionStore instance = ChatEncryptionStore._();
|
||||||
|
|
||||||
|
static const String _keyPrefix = 'chat_encryption_key';
|
||||||
|
|
||||||
|
bool isEnabled(int accountId, int chatId) => read(accountId, chatId) == true;
|
||||||
|
|
||||||
|
Future<void> setEnabled(int accountId, int chatId, bool enabled) =>
|
||||||
|
write(accountId, chatId, enabled ? true : null);
|
||||||
|
|
||||||
|
Future<String?> readKey(int accountId, int chatId) async {
|
||||||
|
if (accountId == 0) return null;
|
||||||
|
return TokenStorage.readSecure(_secureKey(accountId, chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeKey(int accountId, int chatId, String key) async {
|
||||||
|
if (accountId == 0) return;
|
||||||
|
await TokenStorage.writeSecure(_secureKey(accountId, chatId), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteKey(int accountId, int chatId) async {
|
||||||
|
if (accountId == 0) return;
|
||||||
|
await TokenStorage.deleteSecure(_secureKey(accountId, chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
String _secureKey(int accountId, int chatId) =>
|
||||||
|
'${_keyPrefix}_${accountId}_$chatId';
|
||||||
|
}
|
||||||
@@ -15,17 +15,29 @@ class FileDownloadResult {
|
|||||||
/// [cacheName] — стабильное имя в кэше (например, `<fileId>_имя.ext`).
|
/// [cacheName] — стабильное имя в кэше (например, `<fileId>_имя.ext`).
|
||||||
/// [resolveUrl] вызывается лениво — только если файла ещё нет в кэше,
|
/// [resolveUrl] вызывается лениво — только если файла ещё нет в кэше,
|
||||||
/// чтобы не дёргать сервер за временной ссылкой повторно.
|
/// чтобы не дёргать сервер за временной ссылкой повторно.
|
||||||
|
/// [onReady] вызывается как только файл лежит на диске — до открытия во
|
||||||
|
/// внешнем приложении, которое может не возвращать управление, пока его не
|
||||||
|
/// закроют. Без этого индикатор загрузки висел бы всё это время.
|
||||||
Future<FileDownloadResult> openCachedFile(
|
Future<FileDownloadResult> openCachedFile(
|
||||||
String cacheName,
|
String cacheName,
|
||||||
Future<String?> Function() resolveUrl, {
|
Future<String?> Function() resolveUrl, {
|
||||||
void Function(double progress)? onProgress,
|
void Function(double progress)? onProgress,
|
||||||
|
void Function()? onReady,
|
||||||
}) async {
|
}) async {
|
||||||
|
var readyFired = false;
|
||||||
|
void ready() {
|
||||||
|
if (readyFired) return;
|
||||||
|
readyFired = true;
|
||||||
|
onReady?.call();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var file = await MediaCache.existing(cacheName);
|
var file = await MediaCache.existing(cacheName);
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
final url = await resolveUrl();
|
final url = await resolveUrl();
|
||||||
if (url == null || url.isEmpty) {
|
if (url == null || url.isEmpty) {
|
||||||
|
ready();
|
||||||
return const FileDownloadResult(ok: false, error: 'нет ссылки');
|
return const FileDownloadResult(ok: false, error: 'нет ссылки');
|
||||||
}
|
}
|
||||||
file = await MediaCache.getOrDownload(
|
file = await MediaCache.getOrDownload(
|
||||||
@@ -34,10 +46,12 @@ Future<FileDownloadResult> openCachedFile(
|
|||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
);
|
);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
ready();
|
||||||
return const FileDownloadResult(ok: false, error: 'ошибка загрузки');
|
return const FileDownloadResult(ok: false, error: 'ошибка загрузки');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ready();
|
||||||
final opened = await OpenFilex.open(file.path);
|
final opened = await OpenFilex.open(file.path);
|
||||||
return FileDownloadResult(
|
return FileDownloadResult(
|
||||||
ok: opened.type == ResultType.done,
|
ok: opened.type == ResultType.done,
|
||||||
@@ -45,6 +59,7 @@ Future<FileDownloadResult> openCachedFile(
|
|||||||
error: opened.type == ResultType.done ? null : opened.message,
|
error: opened.type == ResultType.done ? null : opened.message,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
ready();
|
||||||
return FileDownloadResult(ok: false, error: e.toString());
|
return FileDownloadResult(ok: false, error: e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:komet/core/config/app_frost.dart';
|
import 'package:komet/core/config/app_frost.dart';
|
||||||
|
import 'package:komet/frontend/widgets/encryption_lock_badge.dart';
|
||||||
import 'package:komet/frontend/widgets/glossy_pill.dart';
|
import 'package:komet/frontend/widgets/glossy_pill.dart';
|
||||||
import 'package:komet/frontend/widgets/online_dot.dart';
|
import 'package:komet/frontend/widgets/online_dot.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ class ChatHeaderRow extends StatelessWidget {
|
|||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String chatType;
|
final String chatType;
|
||||||
final bool isOfficial;
|
final bool isOfficial;
|
||||||
|
final bool encrypted;
|
||||||
final int myId;
|
final int myId;
|
||||||
final ValueListenable<String> headerStatus;
|
final ValueListenable<String> headerStatus;
|
||||||
final ValueListenable<int> scheduledCount;
|
final ValueListenable<int> scheduledCount;
|
||||||
@@ -42,6 +44,7 @@ class ChatHeaderRow extends StatelessWidget {
|
|||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.chatType,
|
required this.chatType,
|
||||||
required this.isOfficial,
|
required this.isOfficial,
|
||||||
|
this.encrypted = false,
|
||||||
required this.myId,
|
required this.myId,
|
||||||
required this.headerStatus,
|
required this.headerStatus,
|
||||||
required this.scheduledCount,
|
required this.scheduledCount,
|
||||||
@@ -373,6 +376,7 @@ class ChatHeaderRow extends StatelessWidget {
|
|||||||
final otherId = chatId ^ myId;
|
final otherId = chatId ^ myId;
|
||||||
final showDot = chatType == 'DIALOG' && myId != 0 && otherId > 0;
|
final showDot = chatType == 'DIALOG' && myId != 0 && otherId > 0;
|
||||||
return Stack(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
avatar,
|
avatar,
|
||||||
if (showDot)
|
if (showDot)
|
||||||
@@ -385,6 +389,12 @@ class ChatHeaderRow extends StatelessWidget {
|
|||||||
size: dotSize,
|
size: dotSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (encrypted)
|
||||||
|
Positioned(
|
||||||
|
left: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: EncryptionLockBadge(size: dotSize + 4),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import '../../../core/storage/chat_encryption_store.dart';
|
||||||
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/glossy_pill.dart';
|
||||||
|
import '../../widgets/primary_loading_button.dart';
|
||||||
|
import '../../widgets/settings_card.dart';
|
||||||
|
import '../../widgets/small_spinner.dart';
|
||||||
|
|
||||||
|
class ChatEncryptionScreen extends StatefulWidget {
|
||||||
|
final int accountId;
|
||||||
|
final int chatId;
|
||||||
|
|
||||||
|
const ChatEncryptionScreen({
|
||||||
|
super.key,
|
||||||
|
required this.accountId,
|
||||||
|
required this.chatId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatEncryptionScreen> createState() => _ChatEncryptionScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatEncryptionScreenState extends State<ChatEncryptionScreen> {
|
||||||
|
final _keyController = TextEditingController();
|
||||||
|
final ValueNotifier<bool> _saving = ValueNotifier(false);
|
||||||
|
|
||||||
|
bool _loading = true;
|
||||||
|
bool _enabled = false;
|
||||||
|
bool _keyVisible = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_keyController.dispose();
|
||||||
|
_saving.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final store = ChatEncryptionStore.instance;
|
||||||
|
await store.load();
|
||||||
|
final key = await store.readKey(widget.accountId, widget.chatId);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_enabled = store.isEnabled(widget.accountId, widget.chatId);
|
||||||
|
_keyController.text = key ?? '';
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (widget.accountId == 0) {
|
||||||
|
showCustomNotification(context, 'Профиль ещё не загружен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final key = _keyController.text.trim();
|
||||||
|
if (_enabled && key.isEmpty) {
|
||||||
|
showCustomNotification(context, 'Введите ключ шифрования');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_saving.value = true;
|
||||||
|
final store = ChatEncryptionStore.instance;
|
||||||
|
if (key.isEmpty) {
|
||||||
|
await store.deleteKey(widget.accountId, widget.chatId);
|
||||||
|
} else {
|
||||||
|
await store.writeKey(widget.accountId, widget.chatId, key);
|
||||||
|
}
|
||||||
|
await store.setEnabled(widget.accountId, widget.chatId, _enabled);
|
||||||
|
if (!mounted) return;
|
||||||
|
_saving.value = false;
|
||||||
|
showCustomNotification(
|
||||||
|
context,
|
||||||
|
_enabled ? 'Шифрование включено' : 'Шифрование отключено',
|
||||||
|
);
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Symbols.arrow_back, color: cs.onSurface, weight: 400),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Шифрование сообщений',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? Center(child: SmallSpinner(size: 36, color: cs.primary))
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SettingsCard(
|
||||||
|
children: [
|
||||||
|
SettingsToggleTile(
|
||||||
|
icon: _enabled ? Symbols.lock : Symbols.lock_open,
|
||||||
|
label: 'Шифровать сообщения',
|
||||||
|
subtitle:
|
||||||
|
'Текст сообщений в этом чате будет зашифрован '
|
||||||
|
'ключом ниже',
|
||||||
|
value: _enabled,
|
||||||
|
onChanged: (v) => setState(() => _enabled = v),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GlossyPill(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
depth: 6,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Ключ',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _keyController,
|
||||||
|
obscureText: !_keyVisible,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Введите ключ',
|
||||||
|
filled: true,
|
||||||
|
fillColor: cs.surfaceContainerHighest,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_keyVisible
|
||||||
|
? Symbols.visibility_off
|
||||||
|
: Symbols.visibility,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
setState(() => _keyVisible = !_keyVisible),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Ключ хранится только на этом устройстве. '
|
||||||
|
'Собеседник должен ввести такой же ключ, иначе он '
|
||||||
|
'не прочитает сообщения.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.35,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: PrimaryLoadingButton(
|
||||||
|
loading: _saving,
|
||||||
|
onPressed: _save,
|
||||||
|
child: const Text('Сохранить'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ import 'create_channel_flow.dart';
|
|||||||
import 'create_group_flow.dart';
|
import 'create_group_flow.dart';
|
||||||
import '../contacts/add_contact_sheet.dart';
|
import '../contacts/add_contact_sheet.dart';
|
||||||
import '../../widgets/adaptive_shell.dart';
|
import '../../widgets/adaptive_shell.dart';
|
||||||
|
import '../../../core/crypto/message_decryption_cache.dart';
|
||||||
|
import '../../widgets/decrypted_text.dart';
|
||||||
|
import '../../widgets/encryption_lock_badge.dart';
|
||||||
import '../../widgets/online_dot.dart';
|
import '../../widgets/online_dot.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
import '../../widgets/glossy_pill.dart';
|
import '../../widgets/glossy_pill.dart';
|
||||||
@@ -52,6 +55,7 @@ import '../../../backend/modules/folders.dart';
|
|||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
import '../../../core/storage/draft_store.dart';
|
import '../../../core/storage/draft_store.dart';
|
||||||
import '../../../core/storage/archived_chats_store.dart';
|
import '../../../core/storage/archived_chats_store.dart';
|
||||||
|
import '../../../core/storage/chat_encryption_store.dart';
|
||||||
import '../../../core/storage/token_storage.dart';
|
import '../../../core/storage/token_storage.dart';
|
||||||
import '../../../core/storage/chat_activity_store.dart';
|
import '../../../core/storage/chat_activity_store.dart';
|
||||||
import '../../../main.dart'
|
import '../../../main.dart'
|
||||||
@@ -579,6 +583,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
});
|
});
|
||||||
chats.chatsChanged.addListener(_onChatsChanged);
|
chats.chatsChanged.addListener(_onChatsChanged);
|
||||||
ArchivedChatsStore.instance.revision.addListener(_onArchivedChanged);
|
ArchivedChatsStore.instance.revision.addListener(_onArchivedChanged);
|
||||||
|
ChatEncryptionStore.instance.revision.addListener(_onEncryptionChanged);
|
||||||
DraftStore.instance.revision.addListener(_onDraftsChanged);
|
DraftStore.instance.revision.addListener(_onDraftsChanged);
|
||||||
AppStories.current.addListener(_onStoriesEnabledChanged);
|
AppStories.current.addListener(_onStoriesEnabledChanged);
|
||||||
storiesModule.storiesChanged.addListener(_onStoriesDataChanged);
|
storiesModule.storiesChanged.addListener(_onStoriesDataChanged);
|
||||||
@@ -624,6 +629,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
if (mounted) _requestReload();
|
if (mounted) _requestReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onEncryptionChanged() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
void _onStoriesEnabledChanged() {
|
void _onStoriesEnabledChanged() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (!AppStories.current.value) {
|
if (!AppStories.current.value) {
|
||||||
@@ -1225,6 +1234,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_settleTimer?.cancel();
|
_settleTimer?.cancel();
|
||||||
chats.chatsChanged.removeListener(_onChatsChanged);
|
chats.chatsChanged.removeListener(_onChatsChanged);
|
||||||
ArchivedChatsStore.instance.revision.removeListener(_onArchivedChanged);
|
ArchivedChatsStore.instance.revision.removeListener(_onArchivedChanged);
|
||||||
|
ChatEncryptionStore.instance.revision.removeListener(_onEncryptionChanged);
|
||||||
DraftStore.instance.revision.removeListener(_onDraftsChanged);
|
DraftStore.instance.revision.removeListener(_onDraftsChanged);
|
||||||
AppStories.current.removeListener(_onStoriesEnabledChanged);
|
AppStories.current.removeListener(_onStoriesEnabledChanged);
|
||||||
storiesModule.storiesChanged.removeListener(_onStoriesDataChanged);
|
storiesModule.storiesChanged.removeListener(_onStoriesDataChanged);
|
||||||
@@ -1645,6 +1655,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
messageRanges: isPlaceholder
|
messageRanges: isPlaceholder
|
||||||
? const []
|
? const []
|
||||||
: chat.lastMsgFormatRanges,
|
: chat.lastMsgFormatRanges,
|
||||||
|
previewMessageId: isPlaceholder ? null : chat.lastMsgId,
|
||||||
|
previewCipherText: isPlaceholder
|
||||||
|
? null
|
||||||
|
: chat.lastMsgTextOneLine,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1654,6 +1668,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
String fullMsg = "";
|
String fullMsg = "";
|
||||||
|
String senderPrefix = "";
|
||||||
List<FormatRange> messageRanges = const [];
|
List<FormatRange> messageRanges = const [];
|
||||||
if (isPlaceholder) {
|
if (isPlaceholder) {
|
||||||
fullMsg = 'зайдите в чат для подгрузки';
|
fullMsg = 'зайдите в чат для подгрузки';
|
||||||
@@ -1661,6 +1676,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
var prefixLen = 0;
|
var prefixLen = 0;
|
||||||
if (sender?.isNotEmpty == true && chat.id != 0) {
|
if (sender?.isNotEmpty == true && chat.id != 0) {
|
||||||
final prefix = "$sender: ";
|
final prefix = "$sender: ";
|
||||||
|
senderPrefix = prefix;
|
||||||
fullMsg += prefix;
|
fullMsg += prefix;
|
||||||
prefixLen = prefix.length;
|
prefixLen = prefix.length;
|
||||||
}
|
}
|
||||||
@@ -1702,6 +1718,11 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
ownStatus: _ownStatusFor(chat, isPlaceholder),
|
ownStatus: _ownStatusFor(chat, isPlaceholder),
|
||||||
ownRead: chat.lastMsgReadByOthers,
|
ownRead: chat.lastMsgReadByOthers,
|
||||||
messageRanges: messageRanges,
|
messageRanges: messageRanges,
|
||||||
|
previewMessageId: isPlaceholder ? null : chat.lastMsgId,
|
||||||
|
previewPrefix: senderPrefix,
|
||||||
|
previewCipherText: isPlaceholder
|
||||||
|
? null
|
||||||
|
: chat.lastMsgText,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2550,19 +2571,55 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
String? ownStatus,
|
String? ownStatus,
|
||||||
bool ownRead = false,
|
bool ownRead = false,
|
||||||
List<FormatRange> messageRanges = const [],
|
List<FormatRange> messageRanges = const [],
|
||||||
|
int? previewMessageId,
|
||||||
|
String previewPrefix = '',
|
||||||
|
String? previewCipherText,
|
||||||
}) {
|
}) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
final isSelected = _selectedChats.contains(id);
|
final isSelected = _selectedChats.contains(id);
|
||||||
|
final isEncrypted = ChatEncryptionStore.instance.isEnabled(
|
||||||
|
_profile?.id ?? 0,
|
||||||
|
int.tryParse(id) ?? 0,
|
||||||
|
);
|
||||||
final Widget? statusIcon = (ownStatus != null && draft == null)
|
final Widget? statusIcon = (ownStatus != null && draft == null)
|
||||||
? _ownStatusIcon(cs, ownStatus, ownRead)
|
? _ownStatusIcon(cs, ownStatus, ownRead)
|
||||||
: null;
|
: null;
|
||||||
final Widget messageLine = _buildPreviewLine(
|
final canDecryptPreview =
|
||||||
cs,
|
isEncrypted &&
|
||||||
message,
|
draft == null &&
|
||||||
messageRanges,
|
previewMessageId != null &&
|
||||||
draft,
|
(previewCipherText?.isNotEmpty ?? false);
|
||||||
messageItalic,
|
final Widget messageLine = canDecryptPreview
|
||||||
);
|
? DecryptedContent(
|
||||||
|
accountId: _profile?.id ?? 0,
|
||||||
|
chatId: int.tryParse(id) ?? 0,
|
||||||
|
messageId: previewMessageId.toString(),
|
||||||
|
cipherText: previewCipherText!,
|
||||||
|
builder: (decryption) => switch (decryption?.state) {
|
||||||
|
null => _buildPreviewLine(
|
||||||
|
cs,
|
||||||
|
message,
|
||||||
|
messageRanges,
|
||||||
|
draft,
|
||||||
|
messageItalic,
|
||||||
|
),
|
||||||
|
MessageDecryptionState.wrongKey => _buildPreviewLine(
|
||||||
|
cs,
|
||||||
|
'$previewPrefix' 'неверный ключ',
|
||||||
|
const [],
|
||||||
|
draft,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
MessageDecryptionState.decrypted => _buildPreviewLine(
|
||||||
|
cs,
|
||||||
|
'$previewPrefix${decryption!.plaintext}',
|
||||||
|
const [],
|
||||||
|
draft,
|
||||||
|
messageItalic,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: _buildPreviewLine(cs, message, messageRanges, draft, messageItalic);
|
||||||
|
|
||||||
final Widget avatarCircle = CircleAvatar(
|
final Widget avatarCircle = CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
@@ -2645,8 +2702,15 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
avatarCircle,
|
avatarCircle,
|
||||||
|
if (isEncrypted)
|
||||||
|
const Positioned(
|
||||||
|
left: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: EncryptionLockBadge(size: 18),
|
||||||
|
),
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: -2,
|
right: -2,
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ import '../../../core/protocol/packet.dart';
|
|||||||
import '../../../core/push/push_service.dart';
|
import '../../../core/push/push_service.dart';
|
||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
import '../../../core/storage/chat_activity_store.dart';
|
import '../../../core/storage/chat_activity_store.dart';
|
||||||
|
import '../../../core/crypto/chat_crypto_service.dart';
|
||||||
|
import '../../../core/crypto/encrypted_photo.dart';
|
||||||
|
import '../../../core/crypto/message_decryption_cache.dart';
|
||||||
|
import '../../../core/storage/chat_encryption_store.dart';
|
||||||
import '../../../core/storage/chat_wallpaper_store.dart';
|
import '../../../core/storage/chat_wallpaper_store.dart';
|
||||||
import '../../../core/storage/draft_store.dart';
|
import '../../../core/storage/draft_store.dart';
|
||||||
import '../../../core/storage/archived_chats_store.dart';
|
import '../../../core/storage/archived_chats_store.dart';
|
||||||
@@ -103,6 +107,7 @@ import '../../widgets/chat_wallpaper_view.dart';
|
|||||||
import '../../widgets/glossy_pill.dart';
|
import '../../widgets/glossy_pill.dart';
|
||||||
import '../../widgets/liquid_glass.dart';
|
import '../../widgets/liquid_glass.dart';
|
||||||
import 'scheduled_messages_screen.dart';
|
import 'scheduled_messages_screen.dart';
|
||||||
|
import 'chat_encryption_screen.dart';
|
||||||
import 'chat_wallpaper_preview_screen.dart';
|
import 'chat_wallpaper_preview_screen.dart';
|
||||||
|
|
||||||
class _DateSeparatorItem {
|
class _DateSeparatorItem {
|
||||||
@@ -263,6 +268,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final GlobalKey _listKey = GlobalKey();
|
final GlobalKey _listKey = GlobalKey();
|
||||||
final ValueNotifier<bool> _hasText = ValueNotifier(false);
|
final ValueNotifier<bool> _hasText = ValueNotifier(false);
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
bool _encryptionEnabled = false;
|
||||||
final ValueNotifier<bool> _showAttachmentPanel = ValueNotifier(false);
|
final ValueNotifier<bool> _showAttachmentPanel = ValueNotifier(false);
|
||||||
late final StickerPanelController _stickers;
|
late final StickerPanelController _stickers;
|
||||||
final ValueNotifier<UploadStatus> _uploadStatus = ValueNotifier(
|
final ValueNotifier<UploadStatus> _uploadStatus = ValueNotifier(
|
||||||
@@ -750,6 +756,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_restoreDraft();
|
_restoreDraft();
|
||||||
unawaited(_loadPeerKind());
|
unawaited(_loadPeerKind());
|
||||||
unawaited(_loadWallpaper());
|
unawaited(_loadWallpaper());
|
||||||
|
unawaited(_loadEncryption());
|
||||||
unawaited(_refreshBadge());
|
unawaited(_refreshBadge());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1870,6 +1877,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_applyEffectiveWallpaper,
|
_applyEffectiveWallpaper,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (_encryptionListening) {
|
||||||
|
ChatEncryptionStore.instance.revision.removeListener(_applyEncryption);
|
||||||
|
}
|
||||||
_headerStatusNotifier.dispose();
|
_headerStatusNotifier.dispose();
|
||||||
_otherReadTime.dispose();
|
_otherReadTime.dispose();
|
||||||
_chatController.dispose();
|
_chatController.dispose();
|
||||||
@@ -2960,6 +2970,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
imageUrl: widget.imageUrl,
|
imageUrl: widget.imageUrl,
|
||||||
chatType: widget.chatType,
|
chatType: widget.chatType,
|
||||||
isOfficial: chat?.isOfficial ?? false,
|
isOfficial: chat?.isOfficial ?? false,
|
||||||
|
encrypted: _encryptionEnabled,
|
||||||
myId: _myId,
|
myId: _myId,
|
||||||
headerStatus: _headerStatusNotifier,
|
headerStatus: _headerStatusNotifier,
|
||||||
scheduledCount: _scheduledCount,
|
scheduledCount: _scheduledCount,
|
||||||
@@ -3049,6 +3060,11 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
label: 'Очистить историю',
|
label: 'Очистить историю',
|
||||||
onTap: _clearHistory,
|
onTap: _clearHistory,
|
||||||
),
|
),
|
||||||
|
ChatMenuItem(
|
||||||
|
icon: _encryptionEnabled ? Symbols.lock : Symbols.lock_open,
|
||||||
|
label: 'Шифрование сообщений',
|
||||||
|
onTap: _openEncryptionSettings,
|
||||||
|
),
|
||||||
ChatMenuItem(
|
ChatMenuItem(
|
||||||
icon: Symbols.delete,
|
icon: Symbols.delete,
|
||||||
label: 'Удалить чат',
|
label: 'Удалить чат',
|
||||||
@@ -3113,6 +3129,43 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _encryptionListening = false;
|
||||||
|
|
||||||
|
Future<void> _loadEncryption() async {
|
||||||
|
await ChatEncryptionStore.instance.load();
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!_encryptionListening) {
|
||||||
|
_encryptionListening = true;
|
||||||
|
ChatEncryptionStore.instance.revision.addListener(_applyEncryption);
|
||||||
|
}
|
||||||
|
_applyEncryption();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyEncryption() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final enabled = ChatEncryptionStore.instance.isEnabled(
|
||||||
|
_myId,
|
||||||
|
widget.chatId,
|
||||||
|
);
|
||||||
|
if (enabled != _encryptionEnabled) {
|
||||||
|
setState(() => _encryptionEnabled = enabled);
|
||||||
|
}
|
||||||
|
if (enabled && _myId != 0) {
|
||||||
|
unawaited(ChatCryptoService.instance.warmKey(_myId, widget.chatId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openEncryptionSettings() async {
|
||||||
|
if (_myId == 0) return;
|
||||||
|
await pushSwipeable(
|
||||||
|
context,
|
||||||
|
(context) =>
|
||||||
|
ChatEncryptionScreen(accountId: _myId, chatId: widget.chatId),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
_applyEncryption();
|
||||||
|
}
|
||||||
|
|
||||||
bool _wallpaperListening = false;
|
bool _wallpaperListening = false;
|
||||||
|
|
||||||
Future<void> _loadWallpaper() async {
|
Future<void> _loadWallpaper() async {
|
||||||
@@ -3482,6 +3535,36 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _encryptOutgoing(String text) async {
|
||||||
|
if (!_encryptionEnabled || _myId == 0) return text;
|
||||||
|
final result = await ChatCryptoService.instance.encrypt(
|
||||||
|
_myId,
|
||||||
|
widget.chatId,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
if (result.isOk) {
|
||||||
|
if (result.text!.length > kMaxEncryptedMessageLength) {
|
||||||
|
if (mounted) {
|
||||||
|
showCustomNotification(
|
||||||
|
context,
|
||||||
|
'Слишком длинное сообщение. Разделите на несколько',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result.text;
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
showCustomNotification(
|
||||||
|
context,
|
||||||
|
result.failure == CryptoFailure.noKey
|
||||||
|
? 'Не задан ключ шифрования'
|
||||||
|
: 'Не удалось зашифровать сообщение',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
final content = _messageController.buildContent();
|
final content = _messageController.buildContent();
|
||||||
final rawText = content.text;
|
final rawText = content.text;
|
||||||
@@ -3505,6 +3588,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final wireText = await _encryptOutgoing(text);
|
||||||
|
if (wireText == null || !mounted) return;
|
||||||
|
final encrypted = wireText != text;
|
||||||
|
|
||||||
final tempId = _nextTempId();
|
final tempId = _nextTempId();
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
final online = api.state == SessionState.online;
|
final online = api.state == SessionState.online;
|
||||||
@@ -3531,7 +3618,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_replyTo.value = null;
|
_replyTo.value = null;
|
||||||
_replySourceChatId = null;
|
_replySourceChatId = null;
|
||||||
|
|
||||||
final elements = _trimmedElements(content.elements, rawText, text);
|
final elements = encrypted
|
||||||
|
? const <Map<String, dynamic>>[]
|
||||||
|
: _trimmedElements(content.elements, rawText, text);
|
||||||
final Map<String, dynamic>? composedPayload =
|
final Map<String, dynamic>? composedPayload =
|
||||||
(replyPayload == null && elements.isEmpty)
|
(replyPayload == null && elements.isEmpty)
|
||||||
? null
|
? null
|
||||||
@@ -3542,11 +3631,12 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
accountId: _myId,
|
accountId: _myId,
|
||||||
chatId: widget.chatId,
|
chatId: widget.chatId,
|
||||||
senderId: _myId,
|
senderId: _myId,
|
||||||
text: text,
|
text: wireText,
|
||||||
time: now,
|
time: now,
|
||||||
status: online ? 'sending' : 'pending',
|
status: online ? 'sending' : 'pending',
|
||||||
payload: composedPayload,
|
payload: composedPayload,
|
||||||
);
|
);
|
||||||
|
if (encrypted) MessageDecryptionCache.instance.seed(tempId, text);
|
||||||
|
|
||||||
_hasText.value = false;
|
_hasText.value = false;
|
||||||
_lastSentId = tempId;
|
_lastSentId = tempId;
|
||||||
@@ -3565,7 +3655,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
widget.chatId,
|
widget.chatId,
|
||||||
messageId: tempId,
|
messageId: tempId,
|
||||||
time: now,
|
time: now,
|
||||||
text: text,
|
text: wireText,
|
||||||
status: composed.status ?? 'sending',
|
status: composed.status ?? 'sending',
|
||||||
elements: elements,
|
elements: elements,
|
||||||
),
|
),
|
||||||
@@ -3587,14 +3677,14 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_myId,
|
_myId,
|
||||||
widget.chatId,
|
widget.chatId,
|
||||||
widget.commentPostId!,
|
widget.commentPostId!,
|
||||||
text,
|
wireText,
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
)
|
)
|
||||||
: await messagesModule.sendMessage(
|
: await messagesModule.sendMessage(
|
||||||
_myId,
|
_myId,
|
||||||
widget.chatId,
|
widget.chatId,
|
||||||
text,
|
wireText,
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
replySourceChatId: replySourceChatId,
|
replySourceChatId: replySourceChatId,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
@@ -3607,11 +3697,14 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
accountId: _myId,
|
accountId: _myId,
|
||||||
chatId: widget.chatId,
|
chatId: widget.chatId,
|
||||||
senderId: _myId,
|
senderId: _myId,
|
||||||
text: text,
|
text: wireText,
|
||||||
time: now,
|
time: now,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
payload: composedPayload,
|
payload: composedPayload,
|
||||||
);
|
);
|
||||||
|
if (encrypted) {
|
||||||
|
MessageDecryptionCache.instance.adopt(tempId, sent.id);
|
||||||
|
}
|
||||||
_messages[index] = sent;
|
_messages[index] = sent;
|
||||||
_bumpMessages();
|
_bumpMessages();
|
||||||
if (!_commentsMode) {
|
if (!_commentsMode) {
|
||||||
@@ -3622,7 +3715,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
widget.chatId,
|
widget.chatId,
|
||||||
messageId: sent.id,
|
messageId: sent.id,
|
||||||
time: now,
|
time: now,
|
||||||
text: text,
|
text: wireText,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
elements: elements,
|
elements: elements,
|
||||||
),
|
),
|
||||||
@@ -5591,13 +5684,14 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
String tempId,
|
String tempId,
|
||||||
String status, {
|
String status, {
|
||||||
FileAttachment? attachment,
|
FileAttachment? attachment,
|
||||||
|
String? realId,
|
||||||
}) {
|
}) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final idx = _messages.indexWhere((m) => m.id == tempId);
|
final idx = _messages.indexWhere((m) => m.id == tempId);
|
||||||
if (idx == -1) return;
|
if (idx == -1) return;
|
||||||
final old = _messages[idx];
|
final old = _messages[idx];
|
||||||
_messages[idx] = CachedMessage(
|
_messages[idx] = CachedMessage(
|
||||||
id: tempId,
|
id: realId != null && realId.isNotEmpty ? realId : tempId,
|
||||||
accountId: old.accountId,
|
accountId: old.accountId,
|
||||||
chatId: old.chatId,
|
chatId: old.chatId,
|
||||||
senderId: old.senderId,
|
senderId: old.senderId,
|
||||||
@@ -5621,12 +5715,16 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
_showAttachmentPanel.value = false;
|
_showAttachmentPanel.value = false;
|
||||||
try {
|
try {
|
||||||
final ok = await messagesModule.sendFileMessage(
|
final realId = await messagesModule.sendFileMessage(
|
||||||
widget.chatId,
|
widget.chatId,
|
||||||
entry.fileId,
|
entry.fileId,
|
||||||
token: entry.token,
|
token: entry.token,
|
||||||
);
|
);
|
||||||
_updateFileMessageStatus(tempId, ok ? 'sent' : 'error');
|
_updateFileMessageStatus(
|
||||||
|
tempId,
|
||||||
|
realId != null ? 'sent' : 'error',
|
||||||
|
realId: realId,
|
||||||
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
_updateFileMessageStatus(tempId, 'error');
|
_updateFileMessageStatus(tempId, 'error');
|
||||||
}
|
}
|
||||||
@@ -5635,13 +5733,14 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
Future<bool> _sendFileById(int fileId) async {
|
Future<bool> _sendFileById(int fileId) async {
|
||||||
final tempId = _addOptimisticFileMessage(FileAttachment(fileId: fileId));
|
final tempId = _addOptimisticFileMessage(FileAttachment(fileId: fileId));
|
||||||
try {
|
try {
|
||||||
final ok = await messagesModule.sendFileMessage(widget.chatId, fileId);
|
final realId = await messagesModule.sendFileMessage(widget.chatId, fileId);
|
||||||
|
final ok = realId != null;
|
||||||
if (!mounted) return ok;
|
if (!mounted) return ok;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
FileHistoryCache.add(
|
FileHistoryCache.add(
|
||||||
FileHistoryEntry(fileId: fileId, sentAt: DateTime.now()),
|
FileHistoryEntry(fileId: fileId, sentAt: DateTime.now()),
|
||||||
);
|
);
|
||||||
_updateFileMessageStatus(tempId, 'sent');
|
_updateFileMessageStatus(tempId, 'sent', realId: realId);
|
||||||
_showAttachmentPanel.value = false;
|
_showAttachmentPanel.value = false;
|
||||||
} else {
|
} else {
|
||||||
_updateFileMessageStatus(tempId, 'error');
|
_updateFileMessageStatus(tempId, 'error');
|
||||||
@@ -5675,11 +5774,17 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
? _sendPhotos
|
? _sendPhotos
|
||||||
: (picked, caption) =>
|
: (picked, caption) =>
|
||||||
_sendScheduledPhotos(picked, caption, scheduledTime),
|
_sendScheduledPhotos(picked, caption, scheduledTime),
|
||||||
onPickFile: scheduledTime == null
|
onPickFile: _encryptionEnabled
|
||||||
? _pickAndUploadFile
|
? () => _refuseUnencrypted('Файлы')
|
||||||
: () => _pickAndUploadFile(scheduledTime: scheduledTime),
|
: (scheduledTime == null
|
||||||
onShareLocation: _shareLocation,
|
? _pickAndUploadFile
|
||||||
onCreatePoll: _createPoll,
|
: () => _pickAndUploadFile(scheduledTime: scheduledTime)),
|
||||||
|
onShareLocation: _encryptionEnabled
|
||||||
|
? () => _refuseUnencrypted('Геолокацию')
|
||||||
|
: _shareLocation,
|
||||||
|
onCreatePoll: _encryptionEnabled
|
||||||
|
? () => _refuseUnencrypted('Опросы')
|
||||||
|
: _createPoll,
|
||||||
);
|
);
|
||||||
if (!mounted || !hadKeyboard) return;
|
if (!mounted || !hadKeyboard) return;
|
||||||
_messageFocusNode.requestFocus();
|
_messageFocusNode.requestFocus();
|
||||||
@@ -5689,6 +5794,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
|
|
||||||
Future<void> _sendPhotos(List<PickedPhoto> picked, String caption) async {
|
Future<void> _sendPhotos(List<PickedPhoto> picked, String caption) async {
|
||||||
if (_myId == 0) return;
|
if (_myId == 0) return;
|
||||||
|
if (_encryptionEnabled) return _sendEncryptedPhotos(picked, caption);
|
||||||
final videos = picked.where((ph) => ph.item.isVideo).toList();
|
final videos = picked.where((ph) => ph.item.isVideo).toList();
|
||||||
final photos = picked.where((ph) => !ph.item.isVideo).toList();
|
final photos = picked.where((ph) => !ph.item.isVideo).toList();
|
||||||
if (photos.isEmpty && videos.isEmpty) return;
|
if (photos.isEmpty && videos.isEmpty) return;
|
||||||
@@ -6208,11 +6314,88 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_photoUploadProgress.remove(tempId)?.dispose();
|
_photoUploadProgress.remove(tempId)?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _refuseUnencrypted(String what) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_showAttachmentPanel.value = false;
|
||||||
|
showCustomNotification(context, '$what пока нельзя зашифровать');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendEncryptedPhotos(
|
||||||
|
List<PickedPhoto> picked,
|
||||||
|
String caption,
|
||||||
|
) async {
|
||||||
|
final photos = picked.where((ph) => !ph.item.isVideo).toList();
|
||||||
|
if (photos.length != picked.length && mounted) {
|
||||||
|
showCustomNotification(context, 'Видео пока нельзя зашифровать');
|
||||||
|
}
|
||||||
|
if (photos.isEmpty) return;
|
||||||
|
|
||||||
|
for (final photo in photos) {
|
||||||
|
final source =
|
||||||
|
photo.editedFile ??
|
||||||
|
photo.item.localFile ??
|
||||||
|
await photo.item.originFile();
|
||||||
|
if (source == null || !mounted) continue;
|
||||||
|
|
||||||
|
_showAttachmentPanel.value = false;
|
||||||
|
_uploadStatus.value = const UploadStatus(active: true);
|
||||||
|
final stamp = DateTime.now().microsecondsSinceEpoch.toString();
|
||||||
|
final prepared = await prepareEncryptedPhoto(
|
||||||
|
accountId: _myId,
|
||||||
|
chatId: widget.chatId,
|
||||||
|
source: source,
|
||||||
|
stamp: stamp,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!prepared.isOk) {
|
||||||
|
_uploadStatus.value = const UploadStatus();
|
||||||
|
showCustomNotification(
|
||||||
|
context,
|
||||||
|
prepared.failure == CryptoFailure.noKey
|
||||||
|
? 'Не задан ключ шифрования'
|
||||||
|
: 'Не удалось зашифровать фото',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final encrypted = prepared.file!;
|
||||||
|
await _uploadAsFile(
|
||||||
|
source: encrypted,
|
||||||
|
filename: 'photo_$stamp$kEncryptedPhotoExtension',
|
||||||
|
size: await encrypted.length(),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caption.isNotEmpty) {
|
||||||
|
final wire = await _encryptOutgoing(caption);
|
||||||
|
if (wire != null && mounted) {
|
||||||
|
await messagesModule.sendMessage(_myId, widget.chatId, wire);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUploadFile({int? scheduledTime}) async {
|
Future<void> _pickAndUploadFile({int? scheduledTime}) async {
|
||||||
final result = await FilePicker.platform.pickFiles();
|
final result = await FilePicker.platform.pickFiles();
|
||||||
if (result == null || result.files.isEmpty) return;
|
if (result == null || result.files.isEmpty) return;
|
||||||
final file = result.files.first;
|
final picked = result.files.first;
|
||||||
if (file.path == null) return;
|
if (picked.path == null) return;
|
||||||
|
await _uploadAsFile(
|
||||||
|
source: File(picked.path!),
|
||||||
|
filename: picked.name,
|
||||||
|
size: picked.size,
|
||||||
|
scheduledTime: scheduledTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadAsFile({
|
||||||
|
required File source,
|
||||||
|
required String filename,
|
||||||
|
required int size,
|
||||||
|
int? scheduledTime,
|
||||||
|
}) async {
|
||||||
|
final file = (name: filename, size: size);
|
||||||
|
final done = Completer<void>();
|
||||||
|
|
||||||
_showAttachmentPanel.value = false;
|
_showAttachmentPanel.value = false;
|
||||||
_uploadStatus.value = UploadStatus(active: true, total: file.size);
|
_uploadStatus.value = UploadStatus(active: true, total: file.size);
|
||||||
@@ -6237,7 +6420,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_uploadSub = fileUploader
|
_uploadSub = fileUploader
|
||||||
.upload(
|
.upload(
|
||||||
chatId: widget.chatId,
|
chatId: widget.chatId,
|
||||||
file: File(file.path!),
|
file: source,
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
totalSize: file.size,
|
totalSize: file.size,
|
||||||
scheduledTime: scheduledTime,
|
scheduledTime: scheduledTime,
|
||||||
@@ -6269,7 +6452,12 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
speedBps: notifSpeedBps,
|
speedBps: notifSpeedBps,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case UploadDone(:final fileId, :final token, :final url):
|
case UploadDone(
|
||||||
|
:final fileId,
|
||||||
|
:final token,
|
||||||
|
:final url,
|
||||||
|
:final messageId,
|
||||||
|
):
|
||||||
stopNotif();
|
stopNotif();
|
||||||
FileHistoryCache.add(
|
FileHistoryCache.add(
|
||||||
FileHistoryEntry(
|
FileHistoryEntry(
|
||||||
@@ -6292,6 +6480,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_updateFileMessageStatus(
|
_updateFileMessageStatus(
|
||||||
tempId!,
|
tempId!,
|
||||||
'sent',
|
'sent',
|
||||||
|
realId: messageId,
|
||||||
attachment: FileAttachment(
|
attachment: FileAttachment(
|
||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
fileToken: token,
|
fileToken: token,
|
||||||
@@ -6326,6 +6515,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
_uploadStatus.value = const UploadStatus();
|
_uploadStatus.value = const UploadStatus();
|
||||||
_uploadSub = null;
|
_uploadSub = null;
|
||||||
|
if (!done.isCompleted) done.complete();
|
||||||
},
|
},
|
||||||
onError: (Object e) {
|
onError: (Object e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -6334,8 +6524,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
if (tempId != null) _updateFileMessageStatus(tempId, 'error');
|
if (tempId != null) _updateFileMessageStatus(tempId, 'error');
|
||||||
_uploadStatus.value = const UploadStatus();
|
_uploadStatus.value = const UploadStatus();
|
||||||
_uploadSub = null;
|
_uploadSub = null;
|
||||||
|
if (!done.isCompleted) done.complete();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return done.future;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (_) => _SendByIdSheet(
|
builder: (_) => _SendByIdSheet(
|
||||||
onSend: (id) async {
|
onSend: (id) async {
|
||||||
final ok = await messagesModule.sendFileMessage(chatId, id);
|
final sentId = await messagesModule.sendFileMessage(chatId, id);
|
||||||
if (!ok) return false;
|
if (sentId == null) return false;
|
||||||
final newest = await CloudStorageModule.fetchLatestFile(
|
final newest = await CloudStorageModule.fetchLatestFile(
|
||||||
messagesModule,
|
messagesModule,
|
||||||
accountId,
|
accountId,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -8,8 +10,11 @@ import '../../../../core/utils/file_download.dart';
|
|||||||
import '../../../../core/utils/media_cache.dart';
|
import '../../../../core/utils/media_cache.dart';
|
||||||
import '../../../../core/utils/format.dart';
|
import '../../../../core/utils/format.dart';
|
||||||
import '../../../../core/utils/haptics.dart';
|
import '../../../../core/utils/haptics.dart';
|
||||||
|
import '../../../../core/crypto/chat_crypto_service.dart';
|
||||||
|
import '../../../../core/crypto/encrypted_photo.dart';
|
||||||
import '../../../../models/attachment.dart';
|
import '../../../../models/attachment.dart';
|
||||||
import '../../custom_notification.dart';
|
import '../../custom_notification.dart';
|
||||||
|
import '../../photo_viewer.dart';
|
||||||
import 'bubble_context.dart';
|
import 'bubble_context.dart';
|
||||||
|
|
||||||
class FileBubble extends StatelessWidget {
|
class FileBubble extends StatelessWidget {
|
||||||
@@ -167,7 +172,86 @@ class FileBubble extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return fill ? inner : IntrinsicWidth(child: inner);
|
final body = fill ? inner : IntrinsicWidth(child: inner);
|
||||||
|
if (!_isViewableImage(name)) return body;
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => _openInViewer(ctx.context, name, cacheName),
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _isViewableImage(String name) =>
|
||||||
|
name.toLowerCase().endsWith('.png');
|
||||||
|
|
||||||
|
Future<void> _openInViewer(
|
||||||
|
BuildContext context,
|
||||||
|
String name,
|
||||||
|
String cacheName,
|
||||||
|
) async {
|
||||||
|
final fileId = file.fileId;
|
||||||
|
if (fileId == null) return;
|
||||||
|
Haptics.tap();
|
||||||
|
|
||||||
|
final wasCached = (await MediaCache.existing(cacheName)) != null;
|
||||||
|
if (!wasCached) MediaDownloadProgress.set(cacheName, 0);
|
||||||
|
File? local;
|
||||||
|
try {
|
||||||
|
final url = await messagesModule.getFileUrl(
|
||||||
|
messageId: ctx.message.id,
|
||||||
|
chatId: ctx.message.chatId,
|
||||||
|
fileId: fileId,
|
||||||
|
);
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
local = await MediaCache.getOrDownload(
|
||||||
|
cacheName,
|
||||||
|
url,
|
||||||
|
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!wasCached) MediaDownloadProgress.set(cacheName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (local == null) {
|
||||||
|
showCustomNotification(context, 'Не удалось загрузить файл');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shown = await _decryptIfNeeded(local, cacheName);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (shown == null) {
|
||||||
|
showCustomNotification(context, 'Неверный ключ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => PhotoViewerScreen(
|
||||||
|
photos: [PhotoAttachment(localPath: shown.path)],
|
||||||
|
chatId: ctx.message.chatId,
|
||||||
|
message: ctx.message,
|
||||||
|
isFile: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File?> _decryptIfNeeded(File local, String cacheName) async {
|
||||||
|
final accountId = ctx.message.accountId;
|
||||||
|
final chatId = ctx.message.chatId;
|
||||||
|
if (!ChatCryptoService.instance.isEnabled(accountId, chatId)) return local;
|
||||||
|
if (!await ChatCryptoService.instance.looksEncryptedImage(local.path)) {
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
final result = await openEncryptedPhoto(
|
||||||
|
accountId: accountId,
|
||||||
|
chatId: chatId,
|
||||||
|
encrypted: local,
|
||||||
|
cacheName: cacheName,
|
||||||
|
);
|
||||||
|
return result.file;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadFile(
|
Future<void> _downloadFile(
|
||||||
@@ -194,8 +278,10 @@ class FileBubble extends StatelessWidget {
|
|||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
),
|
),
|
||||||
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
||||||
|
onReady: () {
|
||||||
|
if (!cached) MediaDownloadProgress.set(cacheName, null);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (!cached) MediaDownloadProgress.set(cacheName, null);
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showCustomNotification(
|
showCustomNotification(
|
||||||
|
|||||||
@@ -922,8 +922,8 @@ class _FileRow extends StatelessWidget {
|
|||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
),
|
),
|
||||||
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
||||||
|
onReady: () => MediaDownloadProgress.set(cacheName, null),
|
||||||
);
|
);
|
||||||
MediaDownloadProgress.set(cacheName, null);
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/crypto/message_decryption_cache.dart';
|
||||||
|
|
||||||
|
class DecryptedContent extends StatefulWidget {
|
||||||
|
final int accountId;
|
||||||
|
final int chatId;
|
||||||
|
final String messageId;
|
||||||
|
final String cipherText;
|
||||||
|
final Widget Function(MessageDecryption? decryption) builder;
|
||||||
|
|
||||||
|
const DecryptedContent({
|
||||||
|
super.key,
|
||||||
|
required this.accountId,
|
||||||
|
required this.chatId,
|
||||||
|
required this.messageId,
|
||||||
|
required this.cipherText,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DecryptedContent> createState() => _DecryptedContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DecryptedContentState extends State<DecryptedContent> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(DecryptedContent old) {
|
||||||
|
super.didUpdateWidget(old);
|
||||||
|
if (old.messageId != widget.messageId ||
|
||||||
|
old.cipherText != widget.cipherText) {
|
||||||
|
_request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _request() {
|
||||||
|
MessageDecryptionCache.instance.request(
|
||||||
|
accountId: widget.accountId,
|
||||||
|
chatId: widget.chatId,
|
||||||
|
messageId: widget.messageId,
|
||||||
|
cipherText: widget.cipherText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<MessageDecryption?>(
|
||||||
|
valueListenable: MessageDecryptionCache.instance.listenableFor(
|
||||||
|
widget.messageId,
|
||||||
|
),
|
||||||
|
builder: (context, decryption, _) => widget.builder(decryption),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
class EncryptionLockBadge extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final Color? borderColor;
|
||||||
|
|
||||||
|
const EncryptionLockBadge({super.key, this.size = 16, this.borderColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: borderColor ?? cs.surface, width: 1.5),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Icon(
|
||||||
|
Symbols.lock,
|
||||||
|
size: size * 0.62,
|
||||||
|
weight: 700,
|
||||||
|
fill: 1,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import '../../backend/modules/messages.dart';
|
|||||||
import '../screens/webapp/web_app_screen.dart';
|
import '../screens/webapp/web_app_screen.dart';
|
||||||
import '../../core/config/app_bubble_behavior.dart';
|
import '../../core/config/app_bubble_behavior.dart';
|
||||||
import '../../core/config/app_bubble_shape.dart';
|
import '../../core/config/app_bubble_shape.dart';
|
||||||
|
import '../../core/crypto/message_decryption_cache.dart';
|
||||||
|
import 'decrypted_text.dart';
|
||||||
import '../../core/utils/bubble_radius.dart';
|
import '../../core/utils/bubble_radius.dart';
|
||||||
import '../../core/utils/link_opener.dart';
|
import '../../core/utils/link_opener.dart';
|
||||||
import '../../core/utils/text_format.dart';
|
import '../../core/utils/text_format.dart';
|
||||||
@@ -1237,7 +1239,18 @@ class MessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextContent(BubbleContext ctx) {
|
Widget _buildTextContent(BubbleContext ctx) => DecryptedContent(
|
||||||
|
accountId: message.accountId,
|
||||||
|
chatId: message.chatId,
|
||||||
|
messageId: message.id,
|
||||||
|
cipherText: message.text ?? '',
|
||||||
|
builder: (decryption) => _buildTextContentBody(ctx, decryption),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildTextContentBody(
|
||||||
|
BubbleContext ctx,
|
||||||
|
MessageDecryption? decryption,
|
||||||
|
) {
|
||||||
final attachments = message.attachments;
|
final attachments = message.attachments;
|
||||||
final isForwardedContact =
|
final isForwardedContact =
|
||||||
attachments != null &&
|
attachments != null &&
|
||||||
@@ -1267,20 +1280,43 @@ class MessageBubble extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
final ranges = message.formatRanges;
|
final ranges = message.formatRanges;
|
||||||
final baseTextWidget = isForwarded
|
final decryptedText = decryption?.plaintext;
|
||||||
? _buildForwardedInlineText(ctx, forwarded)
|
final Widget baseTextWidget;
|
||||||
: (FormattedMessageText.isFormatted(message.text, ranges)
|
if (decryption?.state == MessageDecryptionState.wrongKey) {
|
||||||
? FormattedMessageText(
|
baseTextWidget = Text(
|
||||||
text: message.text!,
|
'неверный ключ',
|
||||||
ranges: ranges,
|
style: textStyle.copyWith(
|
||||||
style: textStyle,
|
color: ctx.cs.error,
|
||||||
)
|
fontStyle: FontStyle.italic,
|
||||||
: Text(message.text ?? '', style: textStyle));
|
),
|
||||||
|
);
|
||||||
|
} else if (decryptedText != null) {
|
||||||
|
baseTextWidget = Text(decryptedText, style: textStyle);
|
||||||
|
} else if (isForwarded) {
|
||||||
|
baseTextWidget = _buildForwardedInlineText(ctx, forwarded);
|
||||||
|
} else if (FormattedMessageText.isFormatted(message.text, ranges)) {
|
||||||
|
baseTextWidget = FormattedMessageText(
|
||||||
|
text: message.text!,
|
||||||
|
ranges: ranges,
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
baseTextWidget = Text(message.text ?? '', style: textStyle);
|
||||||
|
}
|
||||||
final textWidget = _wrapSelectable(baseTextWidget);
|
final textWidget = _wrapSelectable(baseTextWidget);
|
||||||
|
|
||||||
final metaWidget = Text(
|
final metaWidget = Row(
|
||||||
message.status == 'EDITED' ? '${ctx.clockText} ред.' : ctx.clockText,
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(color: ctx.dim, fontSize: 10),
|
children: [
|
||||||
|
if (decryption?.isDecrypted ?? false) ...[
|
||||||
|
Icon(Symbols.lock, size: 11, weight: 700, fill: 1, color: ctx.dim),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
message.status == 'EDITED' ? '${ctx.clockText} ред.' : ctx.clockText,
|
||||||
|
style: TextStyle(color: ctx.dim, fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasReactions) {
|
if (hasReactions) {
|
||||||
@@ -1353,7 +1389,8 @@ class MessageBubble extends StatelessWidget {
|
|||||||
final name = reply.senderId == myId
|
final name = reply.senderId == myId
|
||||||
? 'Вы'
|
? 'Вы'
|
||||||
: (ContactCache.get(reply.senderId) ?? 'Сообщение');
|
: (ContactCache.get(reply.senderId) ?? 'Сообщение');
|
||||||
final preview = reply.previewText();
|
final rawPreview = reply.previewText();
|
||||||
|
final quotedId = reply.messageId;
|
||||||
|
|
||||||
final quote = Container(
|
final quote = Container(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 3, 8, 3),
|
padding: const EdgeInsets.fromLTRB(8, 3, 8, 3),
|
||||||
@@ -1376,14 +1413,28 @@ class MessageBubble extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (preview.isNotEmpty)
|
if (rawPreview.isNotEmpty)
|
||||||
Text(
|
DecryptedContent(
|
||||||
preview,
|
accountId: message.accountId,
|
||||||
maxLines: 1,
|
chatId: message.chatId,
|
||||||
overflow: TextOverflow.ellipsis,
|
messageId: quotedId ?? '',
|
||||||
style: TextStyle(
|
cipherText: quotedId == null ? '' : rawPreview,
|
||||||
color: textColor.withValues(alpha: 0.85),
|
builder: (decryption) => Text(
|
||||||
fontSize: 13,
|
decryption?.state == MessageDecryptionState.wrongKey
|
||||||
|
? 'неверный ключ'
|
||||||
|
: (decryption?.plaintext ?? rawPreview),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: decryption?.state == MessageDecryptionState.wrongKey
|
||||||
|
? cs.error
|
||||||
|
: textColor.withValues(alpha: 0.85),
|
||||||
|
fontSize: 13,
|
||||||
|
fontStyle:
|
||||||
|
decryption?.state == MessageDecryptionState.wrongKey
|
||||||
|
? FontStyle.italic
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ class PhotoViewerScreen extends StatefulWidget {
|
|||||||
final CachedMessage? message;
|
final CachedMessage? message;
|
||||||
final PhotoViewerActions? actions;
|
final PhotoViewerActions? actions;
|
||||||
|
|
||||||
|
/// The opened item is a file attachment rendered as an image, so the counter
|
||||||
|
/// reads "FILE of N" — it has no position within the chat's photo feed.
|
||||||
|
final bool isFile;
|
||||||
|
|
||||||
const PhotoViewerScreen({
|
const PhotoViewerScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.photos,
|
required this.photos,
|
||||||
@@ -87,6 +91,7 @@ class PhotoViewerScreen extends StatefulWidget {
|
|||||||
this.chatId,
|
this.chatId,
|
||||||
this.message,
|
this.message,
|
||||||
this.actions,
|
this.actions,
|
||||||
|
this.isFile = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
PhotoViewerScreen.single(String baseUrl, {super.key})
|
PhotoViewerScreen.single(String baseUrl, {super.key})
|
||||||
@@ -94,7 +99,8 @@ class PhotoViewerScreen extends StatefulWidget {
|
|||||||
initialIndex = 0,
|
initialIndex = 0,
|
||||||
chatId = null,
|
chatId = null,
|
||||||
message = null,
|
message = null,
|
||||||
actions = null;
|
actions = null,
|
||||||
|
isFile = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PhotoViewerScreen> createState() => _PhotoViewerScreenState();
|
State<PhotoViewerScreen> createState() => _PhotoViewerScreenState();
|
||||||
@@ -632,7 +638,9 @@ class _PhotoViewerScreenState extends State<PhotoViewerScreen> {
|
|||||||
children: [
|
children: [
|
||||||
if (_feedLoaded)
|
if (_feedLoaded)
|
||||||
Text(
|
Text(
|
||||||
l10n.photoViewerCounter(_total - _index, _total),
|
widget.isFile
|
||||||
|
? l10n.photoViewerCounterFile(_total)
|
||||||
|
: l10n.photoViewerCounter(_total - _index, _total),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -545,6 +545,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"photoViewerCounterFile": "FILE of {total}",
|
||||||
|
"@photoViewerCounterFile": {
|
||||||
|
"placeholders": {
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"photoViewerSentToday": "{sender} • today at {time}",
|
"photoViewerSentToday": "{sender} • today at {time}",
|
||||||
"@photoViewerSentToday": {
|
"@photoViewerSentToday": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -2606,6 +2606,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Photo {index} of {total}'**
|
/// **'Photo {index} of {total}'**
|
||||||
String photoViewerCounter(int index, int total);
|
String photoViewerCounter(int index, int total);
|
||||||
|
|
||||||
|
/// No description provided for @photoViewerCounterFile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'FILE of {total}'**
|
||||||
|
String photoViewerCounterFile(int total);
|
||||||
|
|
||||||
/// No description provided for @photoViewerSentToday.
|
/// No description provided for @photoViewerSentToday.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -1336,6 +1336,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Photo $index of $total';
|
return 'Photo $index of $total';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String photoViewerCounterFile(int total) {
|
||||||
|
return 'FILE of $total';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String photoViewerSentToday(String sender, String time) {
|
String photoViewerSentToday(String sender, String time) {
|
||||||
return '$sender • today at $time';
|
return '$sender • today at $time';
|
||||||
|
|||||||
@@ -1344,6 +1344,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Фото $index из $total';
|
return 'Фото $index из $total';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String photoViewerCounterFile(int total) {
|
||||||
|
return 'ФАЙЛ из $total';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String photoViewerSentToday(String sender, String time) {
|
String photoViewerSentToday(String sender, String time) {
|
||||||
return '$sender • сегодня в $time';
|
return '$sender • сегодня в $time';
|
||||||
|
|||||||
@@ -441,6 +441,7 @@
|
|||||||
"sharedGoToMessage": "Перейти к сообщению",
|
"sharedGoToMessage": "Перейти к сообщению",
|
||||||
"sharedDownload": "Скачать",
|
"sharedDownload": "Скачать",
|
||||||
"photoViewerCounter": "Фото {index} из {total}",
|
"photoViewerCounter": "Фото {index} из {total}",
|
||||||
|
"photoViewerCounterFile": "ФАЙЛ из {total}",
|
||||||
"photoViewerSentToday": "{sender} • сегодня в {time}",
|
"photoViewerSentToday": "{sender} • сегодня в {time}",
|
||||||
"photoViewerSentOn": "{sender} • {date} в {time}",
|
"photoViewerSentOn": "{sender} • {date} в {time}",
|
||||||
"photoViewerSaveAs": "Сохранить как…",
|
"photoViewerSaveAs": "Сохранить как…",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'core/cache/self_presence.dart';
|
|||||||
import 'core/storage/app_instance.dart';
|
import 'core/storage/app_instance.dart';
|
||||||
import 'core/storage/draft_store.dart';
|
import 'core/storage/draft_store.dart';
|
||||||
import 'core/storage/archived_chats_store.dart';
|
import 'core/storage/archived_chats_store.dart';
|
||||||
|
import 'core/storage/chat_encryption_store.dart';
|
||||||
import 'core/config/app_accent.dart';
|
import 'core/config/app_accent.dart';
|
||||||
import 'core/config/app_amoled.dart';
|
import 'core/config/app_amoled.dart';
|
||||||
import 'core/config/app_show_extra_info.dart';
|
import 'core/config/app_show_extra_info.dart';
|
||||||
@@ -239,6 +240,7 @@ void main(List<String> args) async {
|
|||||||
await FileHistoryCache.load(prefs);
|
await FileHistoryCache.load(prefs);
|
||||||
await DraftStore.instance.load();
|
await DraftStore.instance.load();
|
||||||
await ArchivedChatsStore.instance.load();
|
await ArchivedChatsStore.instance.load();
|
||||||
|
await ChatEncryptionStore.instance.load();
|
||||||
await KometSettings.load();
|
await KometSettings.load();
|
||||||
if (KometSettings.ghostMode.value) SelfPresence.markOffline();
|
if (KometSettings.ghostMode.value) SelfPresence.markOffline();
|
||||||
await ContactCache.load();
|
await ContactCache.load();
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.cxx
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// The Android Gradle Plugin builds the native code with the Android NDK.
|
||||||
|
|
||||||
|
group 'com.flutter_rust_bridge.komet_crypto'
|
||||||
|
version '1.0'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// The Android Gradle Plugin knows how to build native code with the NDK.
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
if (project.android.hasProperty("namespace")) {
|
||||||
|
namespace 'com.flutter_rust_bridge.komet_crypto'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bumping the plugin compileSdkVersion requires all clients of this plugin
|
||||||
|
// to bump the version in their app.
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
// Use the NDK version
|
||||||
|
// declared in /android/app/build.gradle file of the Flutter project.
|
||||||
|
// Replace it with a version number if this plugin requires a specfic NDK version.
|
||||||
|
// (e.g. ndkVersion "23.1.7779620")
|
||||||
|
ndkVersion android.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../cargokit/gradle/plugin.gradle"
|
||||||
|
cargokit {
|
||||||
|
manifestDir = "../rust"
|
||||||
|
libname = "komet_crypto"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'komet_crypto'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.flutter_rust_bridge.komet_crypto">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
.dart_tool
|
||||||
|
*.iml
|
||||||
|
!pubspec.lock
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Copyright 2022 Matej Knopp
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
MIT LICENSE
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
APACHE LICENSE, VERSION 2.0
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||||
|
with flutter plugins and packages.
|
||||||
|
|
||||||
|
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||||
|
for a tutorial on how to use Cargokit.
|
||||||
|
|
||||||
|
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||||
|
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||||
|
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||||
|
|
||||||
|
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||||
|
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||||
|
|
||||||
|
export PATH=${NEW_PATH%?} # remove trailing :
|
||||||
|
|
||||||
|
env
|
||||||
|
|
||||||
|
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||||
|
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||||
|
|
||||||
|
# Arctive architectures (arm64, armv7, x86_64), space separated.
|
||||||
|
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||||
|
|
||||||
|
# Current build configuration (Debug, Release)
|
||||||
|
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||||
|
|
||||||
|
# Path to directory containing Cargo.toml.
|
||||||
|
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||||
|
|
||||||
|
# Temporary directory for build artifacts.
|
||||||
|
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||||
|
|
||||||
|
# Output directory for final artifacts.
|
||||||
|
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||||
|
|
||||||
|
# Directory to store built tool artifacts.
|
||||||
|
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||||
|
|
||||||
|
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||||
|
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||||
|
|
||||||
|
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||||
|
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||||
|
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||||
|
do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
source "$path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sh "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||||
|
|
||||||
|
# Make a symlink from built framework to phony file, which will be used as input to
|
||||||
|
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||||
|
# attribute on custom build phase)
|
||||||
|
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||||
|
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
A sample command-line application with an entrypoint in `bin/`, library code
|
||||||
|
in `lib/`, and example unit test in `test/`.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
- prefer_relative_imports
|
||||||
|
- directives_ordering
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
void main(List<String> arguments) {
|
||||||
|
build_tool.runMain(arguments);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'src/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
return build_tool.runMain(args);
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class AndroidEnvironment {
|
||||||
|
AndroidEnvironment({
|
||||||
|
required this.sdkPath,
|
||||||
|
required this.ndkVersion,
|
||||||
|
required this.minSdkVersion,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.target,
|
||||||
|
});
|
||||||
|
|
||||||
|
static void clangLinkerWrapper(List<String> args) {
|
||||||
|
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||||
|
if (clang == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||||
|
}
|
||||||
|
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(clang, [
|
||||||
|
target,
|
||||||
|
...args,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full path to Android SDK.
|
||||||
|
final String sdkPath;
|
||||||
|
|
||||||
|
/// Full version of Android NDK.
|
||||||
|
final String ndkVersion;
|
||||||
|
|
||||||
|
/// Minimum supported SDK version.
|
||||||
|
final int minSdkVersion;
|
||||||
|
|
||||||
|
/// Target directory for build artifacts.
|
||||||
|
final String targetTempDir;
|
||||||
|
|
||||||
|
/// Target being built.
|
||||||
|
final Target target;
|
||||||
|
|
||||||
|
bool ndkIsInstalled() {
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||||
|
return ndkPackageXml.existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void installNdk({
|
||||||
|
required String javaHome,
|
||||||
|
}) {
|
||||||
|
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||||
|
final sdkManager = path.join(
|
||||||
|
sdkPath,
|
||||||
|
'cmdline-tools',
|
||||||
|
'latest',
|
||||||
|
'bin',
|
||||||
|
'sdkmanager$sdkManagerExtension',
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info('Installing NDK $ndkVersion');
|
||||||
|
runCommand(sdkManager, [
|
||||||
|
'--install',
|
||||||
|
'ndk;$ndkVersion',
|
||||||
|
], environment: {
|
||||||
|
'JAVA_HOME': javaHome,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> buildEnvironment() async {
|
||||||
|
final hostArch = Platform.isMacOS
|
||||||
|
? "darwin-x86_64"
|
||||||
|
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||||
|
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final toolchainPath = path.join(
|
||||||
|
ndkPath,
|
||||||
|
'toolchains',
|
||||||
|
'llvm',
|
||||||
|
'prebuilt',
|
||||||
|
hostArch,
|
||||||
|
'bin',
|
||||||
|
);
|
||||||
|
|
||||||
|
final minSdkVersion =
|
||||||
|
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||||
|
|
||||||
|
final exe = Platform.isWindows ? '.exe' : '';
|
||||||
|
|
||||||
|
final arKey = 'AR_${target.rust}';
|
||||||
|
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||||
|
.map((e) => path.join(toolchainPath, e))
|
||||||
|
.firstWhereOrNull((element) => File(element).existsSync());
|
||||||
|
if (arValue == null) {
|
||||||
|
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||||
|
|
||||||
|
final ccKey = 'CC_${target.rust}';
|
||||||
|
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||||
|
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||||
|
final cFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final cxxKey = 'CXX_${target.rust}';
|
||||||
|
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||||
|
final cxxFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||||
|
final cxxFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final linkerKey =
|
||||||
|
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||||
|
|
||||||
|
final ranlibKey = 'RANLIB_${target.rust}';
|
||||||
|
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||||
|
|
||||||
|
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||||
|
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||||
|
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||||
|
|
||||||
|
final runRustTool =
|
||||||
|
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||||
|
|
||||||
|
final packagePath = (await Isolate.resolvePackageUri(
|
||||||
|
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||||
|
.toFilePath();
|
||||||
|
final selfPath = path.canonicalize(path.join(
|
||||||
|
packagePath,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
runRustTool,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Make sure that run_build_tool is working properly even initially launched directly
|
||||||
|
// through dart run.
|
||||||
|
final toolTempDir =
|
||||||
|
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||||
|
|
||||||
|
return {
|
||||||
|
arKey: arValue,
|
||||||
|
ccKey: ccValue,
|
||||||
|
cfFlagsKey: cFlagsValue,
|
||||||
|
cxxKey: cxxValue,
|
||||||
|
cxxFlagsKey: cxxFlagsValue,
|
||||||
|
ranlibKey: ranlibValue,
|
||||||
|
rustFlagsKey: rustFlagsValue,
|
||||||
|
linkerKey: selfPath,
|
||||||
|
// Recognized by main() so we know when we're acting as a wrapper
|
||||||
|
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||||
|
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||||
|
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||||
|
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||||
|
final workaroundDir = path.join(
|
||||||
|
buildDir,
|
||||||
|
'cargokit',
|
||||||
|
'libgcc_workaround',
|
||||||
|
'${ndkVersion.major}',
|
||||||
|
);
|
||||||
|
Directory(workaroundDir).createSync(recursive: true);
|
||||||
|
if (ndkVersion.major >= 23) {
|
||||||
|
File(path.join(workaroundDir, 'libgcc.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lunwind)');
|
||||||
|
} else {
|
||||||
|
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||||
|
// gets updated for NDK23+.
|
||||||
|
File(path.join(workaroundDir, 'libunwind.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lgcc)');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||||
|
if (rustFlags.isNotEmpty) {
|
||||||
|
rustFlags = '$rustFlags\x1f';
|
||||||
|
}
|
||||||
|
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||||
|
return rustFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class Artifact {
|
||||||
|
/// File system location of the artifact.
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
/// Actual file name that the artifact should have in destination folder.
|
||||||
|
final String finalFileName;
|
||||||
|
|
||||||
|
AritifactType get type {
|
||||||
|
if (finalFileName.endsWith('.dll') ||
|
||||||
|
finalFileName.endsWith('.dll.lib') ||
|
||||||
|
finalFileName.endsWith('.pdb') ||
|
||||||
|
finalFileName.endsWith('.so') ||
|
||||||
|
finalFileName.endsWith('.dylib')) {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
throw Exception('Unknown artifact type for $finalFileName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Artifact({
|
||||||
|
required this.path,
|
||||||
|
required this.finalFileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _log = Logger('artifacts_provider');
|
||||||
|
|
||||||
|
class ArtifactProvider {
|
||||||
|
ArtifactProvider({
|
||||||
|
required this.environment,
|
||||||
|
required this.userOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||||
|
final result = await _getPrecompiledArtifacts(targets);
|
||||||
|
|
||||||
|
final pendingTargets = List.of(targets);
|
||||||
|
pendingTargets.removeWhere((element) => result.containsKey(element));
|
||||||
|
|
||||||
|
if (pendingTargets.isEmpty) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
for (final target in targets) {
|
||||||
|
final builder = RustBuilder(target: target, environment: environment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||||
|
final targetDir = await builder.build();
|
||||||
|
// For local build accept both static and dynamic libraries.
|
||||||
|
final artifactNames = <String>{
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.dylib,
|
||||||
|
remote: false,
|
||||||
|
),
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.staticlib,
|
||||||
|
remote: false,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
final artifacts = artifactNames
|
||||||
|
.map((artifactName) => Artifact(
|
||||||
|
path: path.join(targetDir, artifactName),
|
||||||
|
finalFileName: artifactName,
|
||||||
|
))
|
||||||
|
.where((element) => File(element.path).existsSync())
|
||||||
|
.toList();
|
||||||
|
result[target] = artifacts;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||||
|
List<Target> targets) async {
|
||||||
|
if (userOptions.usePrecompiledBinaries == false) {
|
||||||
|
_log.info('Precompiled binaries are disabled');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (environment.crateOptions.precompiledBinaries == null) {
|
||||||
|
_log.fine('Precompiled binaries not enabled for this crate');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
final start = Stopwatch()..start();
|
||||||
|
final crateHash = CrateHash.compute(environment.manifestDir,
|
||||||
|
tempStorage: environment.targetTempDir);
|
||||||
|
_log.fine(
|
||||||
|
'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
|
final downloadedArtifactsDir =
|
||||||
|
path.join(environment.targetTempDir, 'precompiled', crateHash);
|
||||||
|
Directory(downloadedArtifactsDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final res = <Target, List<Artifact>>{};
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final requiredArtifacts = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
final artifactsForTarget = <Artifact>[];
|
||||||
|
|
||||||
|
for (final artifact in requiredArtifacts) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||||
|
final downloadedPath = path.join(downloadedArtifactsDir, fileName);
|
||||||
|
if (!File(downloadedPath).existsSync()) {
|
||||||
|
final signatureFileName =
|
||||||
|
PrecompileBinaries.signatureFileName(target, artifact);
|
||||||
|
await _tryDownloadArtifacts(
|
||||||
|
crateHash: crateHash,
|
||||||
|
fileName: fileName,
|
||||||
|
signatureFileName: signatureFileName,
|
||||||
|
finalPath: downloadedPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (File(downloadedPath).existsSync()) {
|
||||||
|
artifactsForTarget.add(Artifact(
|
||||||
|
path: downloadedPath,
|
||||||
|
finalFileName: artifact,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only provide complete set of artifacts.
|
||||||
|
if (artifactsForTarget.length == requiredArtifacts.length) {
|
||||||
|
_log.fine('Found precompiled artifacts for $target');
|
||||||
|
res[target] = artifactsForTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> _get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
int attempt = 0;
|
||||||
|
const maxAttempts = 10;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await get(url, headers: headers);
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
// Try to detect reset by peer error and retry.
|
||||||
|
if (attempt++ < maxAttempts &&
|
||||||
|
(e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...');
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tryDownloadArtifacts({
|
||||||
|
required String crateHash,
|
||||||
|
required String fileName,
|
||||||
|
required String signatureFileName,
|
||||||
|
required String finalPath,
|
||||||
|
}) async {
|
||||||
|
final precompiledBinaries = environment.crateOptions.precompiledBinaries!;
|
||||||
|
final prefix = precompiledBinaries.uriPrefix;
|
||||||
|
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||||
|
final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||||
|
_log.fine('Downloading signature from $signatureUrl');
|
||||||
|
final signature = await _get(signatureUrl);
|
||||||
|
if (signature.statusCode == 404) {
|
||||||
|
_log.warning(
|
||||||
|
'Precompiled binaries not available for crate hash $crateHash ($fileName)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (signature.statusCode != 200) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download signature $signatureUrl: status ${signature.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log.fine('Downloading binary from $url');
|
||||||
|
final res = await _get(url);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe('Failed to download binary $url: status ${res.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (verify(
|
||||||
|
precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) {
|
||||||
|
File(finalPath).writeAsBytesSync(res.bodyBytes);
|
||||||
|
} else {
|
||||||
|
_log.shout('Signature verification failed! Ignoring binary.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AritifactType {
|
||||||
|
staticlib,
|
||||||
|
dylib,
|
||||||
|
}
|
||||||
|
|
||||||
|
AritifactType artifactTypeForTarget(Target target) {
|
||||||
|
if (target.darwinPlatform != null) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getArtifactNames({
|
||||||
|
required Target target,
|
||||||
|
required String libraryName,
|
||||||
|
required bool remote,
|
||||||
|
AritifactType? aritifactType,
|
||||||
|
}) {
|
||||||
|
aritifactType ??= artifactTypeForTarget(target);
|
||||||
|
if (target.darwinArch != null) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.dylib'];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-windows-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['$libraryName.lib'];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'$libraryName.dll',
|
||||||
|
'$libraryName.dll.lib',
|
||||||
|
if (!remote) '$libraryName.pdb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-linux-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.so'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("Unsupported target: ${target.rust}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class BuildCMake {
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
BuildCMake({required this.userOptions});
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targetPlatform = Environment.targetPlatform;
|
||||||
|
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception("Unknown target platform: $targetPlatform");
|
||||||
|
}
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts([target]);
|
||||||
|
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path)
|
||||||
|
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_gradle');
|
||||||
|
|
||||||
|
class BuildGradle {
|
||||||
|
BuildGradle({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.targetPlatforms.map((arch) {
|
||||||
|
final target = Target.forFlutterName(arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class BuildPod {
|
||||||
|
BuildPod({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.darwinArchs.map((arch) {
|
||||||
|
final target = Target.forDarwin(
|
||||||
|
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||||
|
runCommand("lipo", [
|
||||||
|
'-create',
|
||||||
|
...sourceFiles,
|
||||||
|
'-output',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final outputDir = Environment.outputDir;
|
||||||
|
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final staticLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.staticlib)
|
||||||
|
.toList();
|
||||||
|
final dynamicLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.dylib)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final libName = environment.crateInfo.packageName;
|
||||||
|
|
||||||
|
// If there is static lib, use it and link it with pod
|
||||||
|
if (staticLibs.isNotEmpty) {
|
||||||
|
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||||
|
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||||
|
} else {
|
||||||
|
// Otherwise try to replace bundle dylib with our dylib
|
||||||
|
final bundlePaths = [
|
||||||
|
'$libName.framework/Versions/A/$libName',
|
||||||
|
'$libName.framework/$libName',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final bundlePath in bundlePaths) {
|
||||||
|
final targetFile = path.join(outputDir, bundlePath);
|
||||||
|
if (File(targetFile).existsSync()) {
|
||||||
|
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||||
|
|
||||||
|
// Replace absolute id with @rpath one so that it works properly
|
||||||
|
// when moved to Frameworks.
|
||||||
|
runCommand("install_name_tool", [
|
||||||
|
'-id',
|
||||||
|
'@rpath/$bundlePath',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception('Unable to find bundle for dynamic library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'build_cmake.dart';
|
||||||
|
import 'build_gradle.dart';
|
||||||
|
import 'build_pod.dart';
|
||||||
|
import 'logging.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
import 'verify_binaries.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_tool');
|
||||||
|
|
||||||
|
abstract class BuildCommand extends Command {
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final options = CargokitUserOptions.load();
|
||||||
|
|
||||||
|
if (options.verboseLogging ||
|
||||||
|
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
await runBuildCommand(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildPodCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-pod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build cocoa pod library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildPod(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildGradleCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-gradle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build android library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildGradle(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildCMakeCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-cmake';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build CMake library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildCMake(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenKeyCommand extends Command {
|
||||||
|
@override
|
||||||
|
final name = 'gen-key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Generate key pair for signing precompiled binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void run() {
|
||||||
|
final kp = generateKey();
|
||||||
|
final private = HEX.encode(kp.privateKey.bytes);
|
||||||
|
final public = HEX.encode(kp.publicKey.bytes);
|
||||||
|
print("Private Key: $private");
|
||||||
|
print("Public Key: $public");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompileBinariesCommand extends Command {
|
||||||
|
PrecompileBinariesCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'repository',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Github repository slug in format owner/name',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
)
|
||||||
|
..addMultiOption('target',
|
||||||
|
help: 'Rust target triple of artifact to build.\n'
|
||||||
|
'Can be specified multiple times or omitted in which case\n'
|
||||||
|
'all targets for current platform will be built.')
|
||||||
|
..addOption(
|
||||||
|
'android-sdk-location',
|
||||||
|
help: 'Location of Android SDK (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-ndk-version',
|
||||||
|
help: 'Android NDK version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-min-sdk-version',
|
||||||
|
help: 'Android minimum rquired version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'temp-dir',
|
||||||
|
help: 'Directory to store temporary build artifacts',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'glibc-version',
|
||||||
|
help: 'GLIBC version to use for linux builds',
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
"verbose",
|
||||||
|
abbr: "v",
|
||||||
|
defaultsTo: false,
|
||||||
|
help: "Enable verbose logging",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = 'precompile-binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Prebuild and upload binaries\n'
|
||||||
|
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||||
|
'Use gen_key through generate priave key.\n'
|
||||||
|
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final verbose = argResults!['verbose'] as bool;
|
||||||
|
if (verbose) {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||||
|
if (privateKeyString == null) {
|
||||||
|
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||||
|
}
|
||||||
|
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||||
|
if (githubToken == null) {
|
||||||
|
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||||
|
}
|
||||||
|
final privateKey = HEX.decode(privateKeyString);
|
||||||
|
if (privateKey.length != 64) {
|
||||||
|
throw ArgumentError('Private key must be 64 bytes long');
|
||||||
|
}
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
if (!Directory(manifestDir).existsSync()) {
|
||||||
|
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||||
|
}
|
||||||
|
String? androidMinSdkVersionString =
|
||||||
|
argResults!['android-min-sdk-version'] as String?;
|
||||||
|
int? androidMinSdkVersion;
|
||||||
|
if (androidMinSdkVersionString != null) {
|
||||||
|
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||||
|
if (androidMinSdkVersion == null) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final targetStrigns = argResults!['target'] as List<String>;
|
||||||
|
final targets = targetStrigns.map((target) {
|
||||||
|
final res = Target.forRustTriple(target);
|
||||||
|
if (res == null) {
|
||||||
|
throw ArgumentError('Invalid target: $target');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}).toList(growable: false);
|
||||||
|
final precompileBinaries = PrecompileBinaries(
|
||||||
|
privateKey: PrivateKey(privateKey),
|
||||||
|
githubToken: githubToken,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||||
|
targets: targets,
|
||||||
|
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||||
|
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
tempDir: argResults!['temp-dir'] as String?,
|
||||||
|
glibcVersion: argResults!['glibc-version'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
await precompileBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerifyBinariesCommand extends Command {
|
||||||
|
VerifyBinariesCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = "verify-binaries";
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Verifies published binaries\n'
|
||||||
|
'Checks whether there is a binary published for each targets\n'
|
||||||
|
'and checks the signature.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
final verifyBinaries = VerifyBinaries(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
await verifyBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
try {
|
||||||
|
// Init logging before options are loaded
|
||||||
|
initLogging();
|
||||||
|
|
||||||
|
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||||
|
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||||
|
..addCommand(BuildPodCommand())
|
||||||
|
..addCommand(BuildGradleCommand())
|
||||||
|
..addCommand(BuildCMakeCommand())
|
||||||
|
..addCommand(GenKeyCommand())
|
||||||
|
..addCommand(PrecompileBinariesCommand())
|
||||||
|
..addCommand(VerifyBinariesCommand());
|
||||||
|
|
||||||
|
await runner.run(args);
|
||||||
|
} on ArgumentError catch (e) {
|
||||||
|
stderr.writeln(e.toString());
|
||||||
|
exit(1);
|
||||||
|
} catch (e, s) {
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
log.severe('Cargokit BuildTool failed with error:');
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(e);
|
||||||
|
// This tells user to install Rust, there's no need to pollute the log with
|
||||||
|
// stack trace.
|
||||||
|
if (e is! RustupNotFoundException) {
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(s);
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe('BuildTool arguments: $args');
|
||||||
|
}
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
final _log = Logger('builder');
|
||||||
|
|
||||||
|
enum BuildConfiguration {
|
||||||
|
debug,
|
||||||
|
release,
|
||||||
|
profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on BuildConfiguration {
|
||||||
|
bool get isDebug => this == BuildConfiguration.debug;
|
||||||
|
String get rustName => switch (this) {
|
||||||
|
BuildConfiguration.debug => 'debug',
|
||||||
|
BuildConfiguration.release => 'release',
|
||||||
|
BuildConfiguration.profile => 'release',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
BuildException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BuildException: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildEnvironment {
|
||||||
|
final BuildConfiguration configuration;
|
||||||
|
final CargokitCrateOptions crateOptions;
|
||||||
|
final String targetTempDir;
|
||||||
|
final String manifestDir;
|
||||||
|
final CrateInfo crateInfo;
|
||||||
|
|
||||||
|
final bool isAndroid;
|
||||||
|
final String? androidSdkPath;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? javaHome;
|
||||||
|
|
||||||
|
final String? glibcVersion;
|
||||||
|
|
||||||
|
BuildEnvironment({
|
||||||
|
required this.configuration,
|
||||||
|
required this.crateOptions,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.crateInfo,
|
||||||
|
required this.isAndroid,
|
||||||
|
this.androidSdkPath,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.javaHome,
|
||||||
|
this.glibcVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||||
|
// XCode configuration adds the flavor to configuration name.
|
||||||
|
final firstSegment = value.split('-').first;
|
||||||
|
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||||
|
(e) => e.name == firstSegment,
|
||||||
|
);
|
||||||
|
if (buildConfiguration == null) {
|
||||||
|
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||||
|
return BuildConfiguration.release;
|
||||||
|
}
|
||||||
|
return buildConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BuildEnvironment fromEnvironment({
|
||||||
|
required bool isAndroid,
|
||||||
|
}) {
|
||||||
|
final buildConfiguration =
|
||||||
|
parseBuildConfiguration(Environment.configuration);
|
||||||
|
final manifestDir = Environment.manifestDir;
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
return BuildEnvironment(
|
||||||
|
configuration: buildConfiguration,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: Environment.targetTempDir,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: isAndroid,
|
||||||
|
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||||
|
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||||
|
androidMinSdkVersion:
|
||||||
|
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||||
|
javaHome: isAndroid ? Environment.javaHome : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustBuilder {
|
||||||
|
final Target target;
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
|
||||||
|
RustBuilder({
|
||||||
|
required this.target,
|
||||||
|
required this.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
void prepare(
|
||||||
|
Rustup rustup,
|
||||||
|
) {
|
||||||
|
final toolchain = _toolchain;
|
||||||
|
if (rustup.installedTargets(toolchain) == null) {
|
||||||
|
rustup.installToolchain(toolchain);
|
||||||
|
}
|
||||||
|
if (toolchain == 'nightly') {
|
||||||
|
rustup.installRustSrcForNightly();
|
||||||
|
}
|
||||||
|
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||||
|
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||||
|
}
|
||||||
|
if (environment.glibcVersion != null) {
|
||||||
|
rustup.installZigBuild(toolchain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CargoBuildOptions? get _buildOptions =>
|
||||||
|
environment.crateOptions.cargo[environment.configuration];
|
||||||
|
|
||||||
|
String get _toolchain => _buildOptions?.toolchain.name ?? 'stable';
|
||||||
|
|
||||||
|
/// Returns the path of directory containing build artifacts.
|
||||||
|
Future<String> build() async {
|
||||||
|
final extraArgs = _buildOptions?.flags ?? [];
|
||||||
|
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||||
|
runCommand(
|
||||||
|
'rustup',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
_toolchain,
|
||||||
|
'cargo',
|
||||||
|
(target.android == null && environment.glibcVersion != null)
|
||||||
|
? 'zigbuild'
|
||||||
|
: 'build',
|
||||||
|
...extraArgs,
|
||||||
|
'--manifest-path',
|
||||||
|
manifestPath,
|
||||||
|
'-p',
|
||||||
|
environment.crateInfo.packageName,
|
||||||
|
if (!environment.configuration.isDebug) '--release',
|
||||||
|
'--target',
|
||||||
|
target.rust +
|
||||||
|
((target.android == null && environment.glibcVersion != null)
|
||||||
|
? '.${environment.glibcVersion!}'
|
||||||
|
: ""),
|
||||||
|
'--target-dir',
|
||||||
|
environment.targetTempDir,
|
||||||
|
],
|
||||||
|
environment: await _buildEnvironment(),
|
||||||
|
);
|
||||||
|
return path.join(
|
||||||
|
environment.targetTempDir,
|
||||||
|
target.rust,
|
||||||
|
environment.configuration.rustName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _buildEnvironment() async {
|
||||||
|
if (target.android == null) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
final sdkPath = environment.androidSdkPath;
|
||||||
|
final ndkVersion = environment.androidNdkVersion;
|
||||||
|
final minSdkVersion = environment.androidMinSdkVersion;
|
||||||
|
if (sdkPath == null) {
|
||||||
|
throw BuildException('androidSdkPath is not set');
|
||||||
|
}
|
||||||
|
if (ndkVersion == null) {
|
||||||
|
throw BuildException('androidNdkVersion is not set');
|
||||||
|
}
|
||||||
|
if (minSdkVersion == null) {
|
||||||
|
throw BuildException('androidMinSdkVersion is not set');
|
||||||
|
}
|
||||||
|
final env = AndroidEnvironment(
|
||||||
|
sdkPath: sdkPath,
|
||||||
|
ndkVersion: ndkVersion,
|
||||||
|
minSdkVersion: minSdkVersion,
|
||||||
|
targetTempDir: environment.targetTempDir,
|
||||||
|
target: target,
|
||||||
|
);
|
||||||
|
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||||
|
env.installNdk(javaHome: environment.javaHome!);
|
||||||
|
}
|
||||||
|
return env.buildEnvironment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:toml/toml.dart';
|
||||||
|
|
||||||
|
class ManifestException {
|
||||||
|
ManifestException(this.message, {required this.fileName});
|
||||||
|
|
||||||
|
final String? fileName;
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (fileName != null) {
|
||||||
|
return 'Failed to parse package manifest at $fileName: $message';
|
||||||
|
} else {
|
||||||
|
return 'Failed to parse package manifest: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrateInfo {
|
||||||
|
CrateInfo({required this.packageName});
|
||||||
|
|
||||||
|
final String packageName;
|
||||||
|
|
||||||
|
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||||
|
final toml = TomlDocument.parse(manifest);
|
||||||
|
final package = toml.toMap()['package'];
|
||||||
|
if (package == null) {
|
||||||
|
throw ManifestException('Missing package section', fileName: fileName);
|
||||||
|
}
|
||||||
|
final name = package['name'];
|
||||||
|
if (name == null) {
|
||||||
|
throw ManifestException('Missing package name', fileName: fileName);
|
||||||
|
}
|
||||||
|
return CrateInfo(packageName: name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CrateInfo load(String manifestDir) {
|
||||||
|
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||||
|
final manifest = manifestFile.readAsStringSync();
|
||||||
|
return parseManifest(manifest, fileName: manifestFile.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class CrateHash {
|
||||||
|
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||||
|
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||||
|
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||||
|
///
|
||||||
|
/// If [tempStorage] is provided, computed hash is stored in a file in that directory
|
||||||
|
/// and reused on subsequent calls if the crate content hasn't changed.
|
||||||
|
static String compute(String manifestDir, {String? tempStorage}) {
|
||||||
|
return CrateHash._(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
tempStorage: tempStorage,
|
||||||
|
)._compute();
|
||||||
|
}
|
||||||
|
|
||||||
|
CrateHash._({
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.tempStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _compute() {
|
||||||
|
final files = getFiles();
|
||||||
|
final tempStorage = this.tempStorage;
|
||||||
|
if (tempStorage != null) {
|
||||||
|
final quickHash = _computeQuickHash(files);
|
||||||
|
final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash'));
|
||||||
|
quickHashFolder.createSync(recursive: true);
|
||||||
|
final quickHashFile = File(path.join(quickHashFolder.path, quickHash));
|
||||||
|
if (quickHashFile.existsSync()) {
|
||||||
|
return quickHashFile.readAsStringSync();
|
||||||
|
}
|
||||||
|
final hash = _computeHash(files);
|
||||||
|
quickHashFile.writeAsStringSync(hash);
|
||||||
|
return hash;
|
||||||
|
} else {
|
||||||
|
return _computeHash(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes a quick hash based on files stat (without reading contents). This
|
||||||
|
/// is used to cache the real hash, which is slower to compute since it involves
|
||||||
|
/// reading every single file.
|
||||||
|
String _computeQuickHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
final data = ByteData(8);
|
||||||
|
for (final file in files) {
|
||||||
|
input.add(utf8.encode(file.path));
|
||||||
|
final stat = file.statSync();
|
||||||
|
data.setUint64(0, stat.size);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
data.setUint64(0, stat.modified.millisecondsSinceEpoch);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
return base64Url.encode(output.events.single.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _computeHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
void addTextFile(File file) {
|
||||||
|
// text Files are hashed by lines in case we're dealing with github checkout
|
||||||
|
// that auto-converts line endings.
|
||||||
|
final splitter = LineSplitter();
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final data = file.readAsStringSync();
|
||||||
|
final lines = splitter.convert(data);
|
||||||
|
for (final line in lines) {
|
||||||
|
input.add(utf8.encode(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
addTextFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
final res = output.events.single;
|
||||||
|
|
||||||
|
// Truncate to 128bits.
|
||||||
|
final hash = res.bytes.sublist(0, 16);
|
||||||
|
return hex.encode(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> getFiles() {
|
||||||
|
final src = Directory(path.join(manifestDir, 'src'));
|
||||||
|
final files = src
|
||||||
|
.listSync(recursive: true, followLinks: false)
|
||||||
|
.whereType<File>()
|
||||||
|
.toList();
|
||||||
|
files.sortBy((element) => element.path);
|
||||||
|
void addFile(String relative) {
|
||||||
|
final file = File(path.join(manifestDir, relative));
|
||||||
|
if (file.existsSync()) {
|
||||||
|
files.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile('Cargo.toml');
|
||||||
|
addFile('Cargo.lock');
|
||||||
|
addFile('build.rs');
|
||||||
|
addFile('cargokit.yaml');
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String manifestDir;
|
||||||
|
final String? tempStorage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
extension on String {
|
||||||
|
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Environment {
|
||||||
|
/// Current build configuration (debug or release).
|
||||||
|
static String get configuration =>
|
||||||
|
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||||
|
|
||||||
|
static bool get isDebug => configuration == 'debug';
|
||||||
|
static bool get isRelease => configuration == 'release';
|
||||||
|
|
||||||
|
/// Temporary directory where Rust build artifacts are placed.
|
||||||
|
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||||
|
|
||||||
|
/// Final output directory where the build artifacts are placed.
|
||||||
|
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||||
|
|
||||||
|
/// Path to the crate manifest (containing Cargo.toml).
|
||||||
|
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||||
|
|
||||||
|
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||||
|
/// not resolved on purpose.
|
||||||
|
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||||
|
|
||||||
|
// Pod
|
||||||
|
|
||||||
|
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||||
|
static String get darwinPlatformName =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||||
|
|
||||||
|
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||||
|
static List<String> get darwinArchs =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||||
|
|
||||||
|
// Gradle
|
||||||
|
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||||
|
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||||
|
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||||
|
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||||
|
static List<String> get targetPlatforms =>
|
||||||
|
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||||
|
|
||||||
|
// CMAKE
|
||||||
|
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||||
|
|
||||||
|
static String _getEnv(String key) {
|
||||||
|
final res = Platform.environment[key];
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception("Missing environment variable $key");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getEnvPath(String key) {
|
||||||
|
final res = _getEnv(key);
|
||||||
|
if (Directory(res).existsSync()) {
|
||||||
|
return res.resolveSymlink();
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
const String kSeparator = "--";
|
||||||
|
const String kDoubleSeparator = "==";
|
||||||
|
|
||||||
|
bool _lastMessageWasSeparator = false;
|
||||||
|
|
||||||
|
void _log(LogRecord rec) {
|
||||||
|
final prefix = '${rec.level.name}: ';
|
||||||
|
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||||
|
if (rec.message == kSeparator) {
|
||||||
|
if (!_lastMessageWasSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('-' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (rec.message == kDoubleSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('=' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln(rec.message);
|
||||||
|
_lastMessageWasSeparator = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initLogging() {
|
||||||
|
Logger.root.level = Level.INFO;
|
||||||
|
Logger.root.onRecord.listen((LogRecord rec) {
|
||||||
|
final lines = rec.message.split('\n');
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||||
|
_log(LogRecord(
|
||||||
|
rec.level,
|
||||||
|
line,
|
||||||
|
rec.loggerName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableVerboseLogging() {
|
||||||
|
Logger.root.level = Level.ALL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
|
||||||
|
final _log = Logger('options');
|
||||||
|
|
||||||
|
/// A class for exceptions that have source span information attached.
|
||||||
|
class SourceSpanException implements Exception {
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// A message describing the exception.
|
||||||
|
String get message => _message;
|
||||||
|
final String _message;
|
||||||
|
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// The span associated with this exception.
|
||||||
|
///
|
||||||
|
/// This may be `null` if the source location can't be determined.
|
||||||
|
SourceSpan? get span => _span;
|
||||||
|
final SourceSpan? _span;
|
||||||
|
|
||||||
|
SourceSpanException(this._message, this._span);
|
||||||
|
|
||||||
|
/// Returns a string representation of `this`.
|
||||||
|
///
|
||||||
|
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||||||
|
/// it indicates an ANSI terminal color escape that should be used to
|
||||||
|
/// highlight the span's text. If it's `true`, it indicates that the text
|
||||||
|
/// should be highlighted using the default color. If it's `false` or `null`,
|
||||||
|
/// it indicates that the text shouldn't be highlighted.
|
||||||
|
@override
|
||||||
|
String toString({Object? color}) {
|
||||||
|
if (span == null) return message;
|
||||||
|
return 'Error on ${span!.message(message, color: color)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Toolchain {
|
||||||
|
stable,
|
||||||
|
beta,
|
||||||
|
nightly,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargoBuildOptions {
|
||||||
|
final Toolchain toolchain;
|
||||||
|
final List<String> flags;
|
||||||
|
|
||||||
|
CargoBuildOptions({
|
||||||
|
required this.toolchain,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Toolchain _toolchainFromNode(YamlNode node) {
|
||||||
|
if (node case YamlScalar(value: String name)) {
|
||||||
|
final toolchain =
|
||||||
|
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (toolchain != null) {
|
||||||
|
return toolchain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargoBuildOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
Toolchain toolchain = Toolchain.stable;
|
||||||
|
List<String> flags = [];
|
||||||
|
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: 'toolchain')) {
|
||||||
|
toolchain = _toolchainFromNode(value);
|
||||||
|
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||||||
|
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||||||
|
if (list.every((element) {
|
||||||
|
if (element case YamlScalar(value: String _)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})) {
|
||||||
|
flags = list.map((e) => e.value as String).toList();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Extra flags must be a list of strings', value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on YamlMap {
|
||||||
|
/// Map that extracts keys so that we can do map case check on them.
|
||||||
|
Map<dynamic, YamlNode> get valueMap =>
|
||||||
|
nodes.map((key, value) => MapEntry(key.value, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompiledBinaries {
|
||||||
|
final String uriPrefix;
|
||||||
|
final PublicKey publicKey;
|
||||||
|
|
||||||
|
PrecompiledBinaries({
|
||||||
|
required this.uriPrefix,
|
||||||
|
required this.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||||||
|
final bytes = HEX.decode(key);
|
||||||
|
if (bytes.length != 32) {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid public key. Must be 32 bytes long.', span);
|
||||||
|
}
|
||||||
|
return PublicKey(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PrecompiledBinaries parse(YamlNode node) {
|
||||||
|
if (node case YamlMap(valueMap: Map<dynamic, YamlNode> map)) {
|
||||||
|
if (map
|
||||||
|
case {
|
||||||
|
'url_prefix': YamlNode urlPrefixNode,
|
||||||
|
'public_key': YamlNode publicKeyNode,
|
||||||
|
}) {
|
||||||
|
final urlPrefix = switch (urlPrefixNode) {
|
||||||
|
YamlScalar(value: String urlPrefix) => urlPrefix,
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid URL prefix value.', urlPrefixNode.span),
|
||||||
|
};
|
||||||
|
final publicKey = switch (publicKeyNode) {
|
||||||
|
YamlScalar(value: String publicKey) =>
|
||||||
|
_publicKeyFromHex(publicKey, publicKeyNode.span),
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid public key value.', publicKeyNode.span),
|
||||||
|
};
|
||||||
|
return PrecompiledBinaries(
|
||||||
|
uriPrefix: urlPrefix,
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid precompiled binaries value. '
|
||||||
|
'Expected Map with "url_prefix" and "public_key".',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cargokit options specified for Rust crate.
|
||||||
|
class CargokitCrateOptions {
|
||||||
|
CargokitCrateOptions({
|
||||||
|
this.cargo = const {},
|
||||||
|
this.precompiledBinaries,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||||||
|
final PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
static CargokitCrateOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||||||
|
PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry
|
||||||
|
case MapEntry(
|
||||||
|
key: YamlScalar(value: 'cargo'),
|
||||||
|
value: YamlNode node,
|
||||||
|
)) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: String name)) {
|
||||||
|
final configuration = BuildConfiguration.values
|
||||||
|
.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (configuration != null) {
|
||||||
|
options[configuration] = CargoBuildOptions.parse(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||||||
|
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitCrateOptions(
|
||||||
|
cargo: options,
|
||||||
|
precompiledBinaries: precompiledBinaries,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitCrateOptions load({
|
||||||
|
required String manifestDir,
|
||||||
|
}) {
|
||||||
|
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||||||
|
final file = File.fromUri(uri);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||||||
|
return parse(contents);
|
||||||
|
} else {
|
||||||
|
return CargokitCrateOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargokitUserOptions {
|
||||||
|
// When Rustup is installed always build locally unless user opts into
|
||||||
|
// using precompiled binaries.
|
||||||
|
static bool defaultUsePrecompiledBinaries() {
|
||||||
|
return Rustup.executablePath() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CargokitUserOptions({
|
||||||
|
required this.usePrecompiledBinaries,
|
||||||
|
required this.verboseLogging,
|
||||||
|
});
|
||||||
|
|
||||||
|
CargokitUserOptions._()
|
||||||
|
: usePrecompiledBinaries = defaultUsePrecompiledBinaries(),
|
||||||
|
verboseLogging = false;
|
||||||
|
|
||||||
|
static CargokitUserOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
bool usePrecompiledBinaries = defaultUsePrecompiledBinaries();
|
||||||
|
bool verboseLogging = false;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
usePrecompiledBinaries = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
verboseLogging = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "verbose_logging". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitUserOptions(
|
||||||
|
usePrecompiledBinaries: usePrecompiledBinaries,
|
||||||
|
verboseLogging: verboseLogging,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitUserOptions load() {
|
||||||
|
String fileName = "cargokit_options.yaml";
|
||||||
|
var userProjectDir = Directory(Environment.rootProjectDir);
|
||||||
|
|
||||||
|
while (userProjectDir.parent.path != userProjectDir.path) {
|
||||||
|
final configFile = File(path.join(userProjectDir.path, fileName));
|
||||||
|
if (configFile.existsSync()) {
|
||||||
|
final contents = loadYamlNode(
|
||||||
|
configFile.readAsStringSync(),
|
||||||
|
sourceUrl: configFile.uri,
|
||||||
|
);
|
||||||
|
final res = parse(contents);
|
||||||
|
if (res.verboseLogging) {
|
||||||
|
_log.info('Found user options file at ${configFile.path}');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
userProjectDir = userProjectDir.parent;
|
||||||
|
}
|
||||||
|
return CargokitUserOptions._();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool usePrecompiledBinaries;
|
||||||
|
final bool verboseLogging;
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final _log = Logger('precompile_binaries');
|
||||||
|
|
||||||
|
class PrecompileBinaries {
|
||||||
|
PrecompileBinaries({
|
||||||
|
required this.privateKey,
|
||||||
|
required this.githubToken,
|
||||||
|
required this.repositorySlug,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.targets,
|
||||||
|
this.androidSdkLocation,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.tempDir,
|
||||||
|
this.glibcVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PrivateKey privateKey;
|
||||||
|
final String githubToken;
|
||||||
|
final RepositorySlug repositorySlug;
|
||||||
|
final String manifestDir;
|
||||||
|
final List<Target> targets;
|
||||||
|
final String? androidSdkLocation;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? tempDir;
|
||||||
|
final String? glibcVersion;
|
||||||
|
|
||||||
|
static String fileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String signatureFileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name.sig';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> run() async {
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
|
||||||
|
final targets = List.of(this.targets);
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
targets.addAll([
|
||||||
|
...Target.buildableTargets(),
|
||||||
|
if (androidSdkLocation != null) ...Target.androidTargets(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Precompiling binaries for $targets');
|
||||||
|
|
||||||
|
final hash = CrateHash.compute(manifestDir);
|
||||||
|
_log.info('Computed crate hash: $hash');
|
||||||
|
|
||||||
|
final String tagName = 'precompiled_$hash';
|
||||||
|
|
||||||
|
final github = GitHub(auth: Authentication.withToken(githubToken));
|
||||||
|
final repo = github.repositories;
|
||||||
|
final release = await _getOrCreateRelease(
|
||||||
|
repo: repo,
|
||||||
|
tagName: tagName,
|
||||||
|
packageName: crateInfo.packageName,
|
||||||
|
hash: hash,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tempDir = this.tempDir != null
|
||||||
|
? Directory(this.tempDir!)
|
||||||
|
: Directory.systemTemp.createTempSync('precompiled_');
|
||||||
|
|
||||||
|
tempDir.createSync(recursive: true);
|
||||||
|
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
final buildEnvironment = BuildEnvironment(
|
||||||
|
configuration: BuildConfiguration.release,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: tempDir.path,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: androidSdkLocation != null,
|
||||||
|
androidSdkPath: androidSdkLocation,
|
||||||
|
androidNdkVersion: androidNdkVersion,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
glibcVersion: glibcVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final artifactNames = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (artifactNames.every((name) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, name);
|
||||||
|
return (release.assets ?? []).any((e) => e.name == fileName);
|
||||||
|
})) {
|
||||||
|
_log.info("All artifacts for $target already exist - skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Building for $target');
|
||||||
|
|
||||||
|
final builder =
|
||||||
|
RustBuilder(target: target, environment: buildEnvironment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
final res = await builder.build();
|
||||||
|
|
||||||
|
final assets = <CreateReleaseAsset>[];
|
||||||
|
for (final name in artifactNames) {
|
||||||
|
final file = File(path.join(res, name));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
throw Exception('Missing artifact: ${file.path}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = file.readAsBytesSync();
|
||||||
|
final create = CreateReleaseAsset(
|
||||||
|
name: PrecompileBinaries.fileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: data,
|
||||||
|
);
|
||||||
|
final signature = sign(privateKey, data);
|
||||||
|
final signatureCreate = CreateReleaseAsset(
|
||||||
|
name: signatureFileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: signature,
|
||||||
|
);
|
||||||
|
bool verified = verify(public(privateKey), data, signature);
|
||||||
|
if (!verified) {
|
||||||
|
throw Exception('Signature verification failed');
|
||||||
|
}
|
||||||
|
assets.add(create);
|
||||||
|
assets.add(signatureCreate);
|
||||||
|
}
|
||||||
|
_log.info('Uploading assets: ${assets.map((e) => e.name)}');
|
||||||
|
for (final asset in assets) {
|
||||||
|
// This seems to be failing on CI so do it one by one
|
||||||
|
int retryCount = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await repo.uploadReleaseAssets(release, [asset]);
|
||||||
|
break;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (retryCount == 10) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
++retryCount;
|
||||||
|
_log.shout(
|
||||||
|
'Upload failed (attempt $retryCount, will retry): ${e.toString()}');
|
||||||
|
await Future.delayed(Duration(seconds: 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Cleaning up');
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Release> _getOrCreateRelease({
|
||||||
|
required RepositoriesService repo,
|
||||||
|
required String tagName,
|
||||||
|
required String packageName,
|
||||||
|
required String hash,
|
||||||
|
}) async {
|
||||||
|
Release release;
|
||||||
|
try {
|
||||||
|
_log.info('Fetching release $tagName');
|
||||||
|
release = await repo.getReleaseByTagName(repositorySlug, tagName);
|
||||||
|
} on ReleaseNotFound {
|
||||||
|
_log.info('Release not found - creating release $tagName');
|
||||||
|
release = await repo.createRelease(
|
||||||
|
repositorySlug,
|
||||||
|
CreateRelease.from(
|
||||||
|
tagName: tagName,
|
||||||
|
name: 'Precompiled binaries ${hash.substring(0, 8)}',
|
||||||
|
targetCommitish: null,
|
||||||
|
isDraft: false,
|
||||||
|
isPrerelease: false,
|
||||||
|
body: 'Precompiled binaries for crate $packageName, '
|
||||||
|
'crate hash $hash.',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class _Toolchain {
|
||||||
|
_Toolchain(
|
||||||
|
this.name,
|
||||||
|
this.targets,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<String> targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rustup {
|
||||||
|
List<String>? installedTargets(String toolchain) {
|
||||||
|
final targets = _installedTargets(toolchain);
|
||||||
|
return targets != null ? List.unmodifiable(targets) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void installToolchain(String toolchain) {
|
||||||
|
log.info("Installing Rust toolchain: $toolchain");
|
||||||
|
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||||
|
_installedToolchains
|
||||||
|
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void installTarget(
|
||||||
|
String target, {
|
||||||
|
required String toolchain,
|
||||||
|
}) {
|
||||||
|
log.info("Installing Rust target: $target");
|
||||||
|
runCommand("rustup", ['target', 'add', '--toolchain', toolchain, target]);
|
||||||
|
_installedTargets(toolchain)?.add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _didInstallZigBuild = false;
|
||||||
|
|
||||||
|
void installZigBuild(String toolchain) {
|
||||||
|
if (_didInstallZigBuild) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Installing Zig build");
|
||||||
|
runCommand("rustup", [
|
||||||
|
'run',
|
||||||
|
toolchain,
|
||||||
|
'cargo',
|
||||||
|
'install',
|
||||||
|
'--locked',
|
||||||
|
'cargo-zigbuild',
|
||||||
|
]);
|
||||||
|
_didInstallZigBuild = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_Toolchain> _installedToolchains;
|
||||||
|
|
||||||
|
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||||
|
|
||||||
|
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||||
|
?.targets;
|
||||||
|
|
||||||
|
static List<_Toolchain> _getInstalledToolchains() {
|
||||||
|
String extractToolchainName(String line) {
|
||||||
|
// ignore (default) after toolchain name
|
||||||
|
final parts = line.split(' ');
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||||
|
|
||||||
|
// To list all non-custom toolchains, we need to filter out lines that
|
||||||
|
// don't start with "stable", "beta", or "nightly".
|
||||||
|
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||||
|
.map(extractToolchainName)
|
||||||
|
.toList(growable: true);
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map(
|
||||||
|
(name) => _Toolchain(
|
||||||
|
name,
|
||||||
|
_getInstalledTargets(name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> _getInstalledTargets(String toolchain) {
|
||||||
|
final res = runCommand("rustup", [
|
||||||
|
'target',
|
||||||
|
'list',
|
||||||
|
'--toolchain',
|
||||||
|
toolchain,
|
||||||
|
'--installed',
|
||||||
|
]);
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList(growable: true);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _didInstallRustSrcForNightly = false;
|
||||||
|
|
||||||
|
void installRustSrcForNightly() {
|
||||||
|
if (_didInstallRustSrcForNightly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Useful for -Z build-std
|
||||||
|
runCommand(
|
||||||
|
"rustup",
|
||||||
|
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||||
|
);
|
||||||
|
_didInstallRustSrcForNightly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? executablePath() {
|
||||||
|
final envPath = Platform.environment['PATH'];
|
||||||
|
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||||
|
final home = Platform.isWindows
|
||||||
|
? Platform.environment['USERPROFILE']
|
||||||
|
: Platform.environment['HOME'];
|
||||||
|
final paths = [
|
||||||
|
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||||
|
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||||
|
];
|
||||||
|
for (final p in paths) {
|
||||||
|
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||||
|
final rustupPath = path.join(p, rustup);
|
||||||
|
if (File(rustupPath).existsSync()) {
|
||||||
|
return rustupPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class Target {
|
||||||
|
Target({
|
||||||
|
required this.rust,
|
||||||
|
this.flutter,
|
||||||
|
this.android,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.darwinPlatform,
|
||||||
|
this.darwinArch,
|
||||||
|
});
|
||||||
|
|
||||||
|
static final all = [
|
||||||
|
Target(
|
||||||
|
rust: 'armv7-linux-androideabi',
|
||||||
|
flutter: 'android-arm',
|
||||||
|
android: 'armeabi-v7a',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-linux-android',
|
||||||
|
flutter: 'android-arm64',
|
||||||
|
android: 'arm64-v8a',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'i686-linux-android',
|
||||||
|
flutter: 'android-x86',
|
||||||
|
android: 'x86',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-linux-android',
|
||||||
|
flutter: 'android-x64',
|
||||||
|
android: 'x86_64',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-pc-windows-msvc',
|
||||||
|
flutter: 'windows-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-pc-windows-msvc',
|
||||||
|
flutter: 'windows-arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-arm64',
|
||||||
|
),
|
||||||
|
Target(rust: 'riscv64gc-unknown-linux-gnu', flutter: 'linux-riscv64'),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios',
|
||||||
|
darwinPlatform: 'iphoneos',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios-sim',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-ios',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static Target? forFlutterName(String flutterName) {
|
||||||
|
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forDarwin({
|
||||||
|
required String platformName,
|
||||||
|
required String darwinAarch,
|
||||||
|
}) {
|
||||||
|
return all.firstWhereOrNull((element) => //
|
||||||
|
element.darwinPlatform == platformName &&
|
||||||
|
element.darwinArch == darwinAarch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forRustTriple(String triple) {
|
||||||
|
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Target> androidTargets() {
|
||||||
|
return all
|
||||||
|
.where((element) => element.android != null)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||||
|
static List<Target> buildableTargets() {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
// Right now we don't support cross-compiling on Linux. So we just return
|
||||||
|
// the host target.
|
||||||
|
final arch = (runCommand('arch', []).stdout as String).trim();
|
||||||
|
if (arch == 'aarch64') {
|
||||||
|
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||||
|
} else if (arch == 'riscv64') {
|
||||||
|
return [Target.forRustTriple('riscv64gc-unknown-linux-gnu')!];
|
||||||
|
} else {
|
||||||
|
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all.where((target) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return target.rust.contains('-windows-');
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return target.darwinPlatform != null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return rust;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? flutter;
|
||||||
|
final String rust;
|
||||||
|
final String? android;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? darwinPlatform;
|
||||||
|
final String? darwinArch;
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'logging.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
|
||||||
|
final log = Logger("process");
|
||||||
|
|
||||||
|
class CommandFailedException implements Exception {
|
||||||
|
final String executable;
|
||||||
|
final List<String> arguments;
|
||||||
|
final ProcessResult result;
|
||||||
|
|
||||||
|
CommandFailedException({
|
||||||
|
required this.executable,
|
||||||
|
required this.arguments,
|
||||||
|
required this.result,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final stdout = result.stdout.toString().trim();
|
||||||
|
final stderr = result.stderr.toString().trim();
|
||||||
|
return [
|
||||||
|
"External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}",
|
||||||
|
"Returned Exit Code: ${result.exitCode}",
|
||||||
|
kSeparator,
|
||||||
|
"STDOUT:",
|
||||||
|
if (stdout.isNotEmpty) stdout,
|
||||||
|
kSeparator,
|
||||||
|
"STDERR:",
|
||||||
|
if (stderr.isNotEmpty) stderr,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRunCommandArgs {
|
||||||
|
final String executable;
|
||||||
|
final List<String> arguments;
|
||||||
|
final String? workingDirectory;
|
||||||
|
final Map<String, String>? environment;
|
||||||
|
final bool includeParentEnvironment;
|
||||||
|
final bool runInShell;
|
||||||
|
final Encoding? stdoutEncoding;
|
||||||
|
final Encoding? stderrEncoding;
|
||||||
|
|
||||||
|
TestRunCommandArgs({
|
||||||
|
required this.executable,
|
||||||
|
required this.arguments,
|
||||||
|
this.workingDirectory,
|
||||||
|
this.environment,
|
||||||
|
this.includeParentEnvironment = true,
|
||||||
|
this.runInShell = false,
|
||||||
|
this.stdoutEncoding,
|
||||||
|
this.stderrEncoding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRunCommandResult {
|
||||||
|
TestRunCommandResult({
|
||||||
|
this.pid = 1,
|
||||||
|
this.exitCode = 0,
|
||||||
|
this.stdout = '',
|
||||||
|
this.stderr = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
final int pid;
|
||||||
|
final int exitCode;
|
||||||
|
final String stdout;
|
||||||
|
final String stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride;
|
||||||
|
|
||||||
|
ProcessResult runCommand(
|
||||||
|
String executable,
|
||||||
|
List<String> arguments, {
|
||||||
|
String? workingDirectory,
|
||||||
|
Map<String, String>? environment,
|
||||||
|
bool includeParentEnvironment = true,
|
||||||
|
bool runInShell = false,
|
||||||
|
Encoding? stdoutEncoding = systemEncoding,
|
||||||
|
Encoding? stderrEncoding = systemEncoding,
|
||||||
|
}) {
|
||||||
|
if (testRunCommandOverride != null) {
|
||||||
|
final result = testRunCommandOverride!(TestRunCommandArgs(
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
includeParentEnvironment: includeParentEnvironment,
|
||||||
|
runInShell: runInShell,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
));
|
||||||
|
return ProcessResult(
|
||||||
|
result.pid,
|
||||||
|
result.exitCode,
|
||||||
|
result.stdout,
|
||||||
|
result.stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.finer('Running command $executable ${arguments.join(' ')}');
|
||||||
|
final res = Process.runSync(
|
||||||
|
_resolveExecutable(executable),
|
||||||
|
arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
includeParentEnvironment: includeParentEnvironment,
|
||||||
|
runInShell: runInShell,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
);
|
||||||
|
if (res.exitCode != 0) {
|
||||||
|
throw CommandFailedException(
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
result: res,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustupNotFoundException implements Exception {
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return [
|
||||||
|
' ',
|
||||||
|
'rustup not found in PATH.',
|
||||||
|
' ',
|
||||||
|
'Maybe you need to install Rust? It only takes a minute:',
|
||||||
|
' ',
|
||||||
|
if (Platform.isWindows) 'https://www.rust-lang.org/tools/install',
|
||||||
|
if (hasHomebrewRustInPath()) ...[
|
||||||
|
'\$ brew unlink rust # Unlink homebrew Rust from PATH',
|
||||||
|
],
|
||||||
|
if (!Platform.isWindows)
|
||||||
|
"\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
||||||
|
' ',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasHomebrewRustInPath() {
|
||||||
|
if (!Platform.isMacOS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final envPath = Platform.environment['PATH'] ?? '';
|
||||||
|
final paths = envPath.split(':');
|
||||||
|
return paths.any((p) {
|
||||||
|
return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveExecutable(String executable) {
|
||||||
|
if (executable == 'rustup') {
|
||||||
|
final resolved = Rustup.executablePath();
|
||||||
|
if (resolved != null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
throw RustupNotFoundException();
|
||||||
|
} else {
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class VerifyBinaries {
|
||||||
|
VerifyBinaries({
|
||||||
|
required this.manifestDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String manifestDir;
|
||||||
|
|
||||||
|
Future<void> run() async {
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
|
||||||
|
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||||
|
final precompiledBinaries = config.precompiledBinaries;
|
||||||
|
if (precompiledBinaries == null) {
|
||||||
|
stdout.writeln('Crate does not support precompiled binaries.');
|
||||||
|
} else {
|
||||||
|
final crateHash = CrateHash.compute(manifestDir);
|
||||||
|
stdout.writeln('Crate hash: $crateHash');
|
||||||
|
|
||||||
|
for (final target in Target.all) {
|
||||||
|
final message = 'Checking ${target.rust}...';
|
||||||
|
stdout.write(message.padRight(40));
|
||||||
|
stdout.flush();
|
||||||
|
|
||||||
|
final artifacts = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final prefix = precompiledBinaries.uriPrefix;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
for (final artifact in artifacts) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||||
|
final signatureFileName =
|
||||||
|
PrecompileBinaries.signatureFileName(target, artifact);
|
||||||
|
|
||||||
|
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||||
|
final signatureUrl =
|
||||||
|
Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||||
|
|
||||||
|
final signature = await get(signatureUrl);
|
||||||
|
if (signature.statusCode != 200) {
|
||||||
|
stdout.writeln('MISSING');
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final asset = await get(url);
|
||||||
|
if (asset.statusCode != 200) {
|
||||||
|
stdout.writeln('MISSING');
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verify(precompiledBinaries.publicKey, asset.bodyBytes,
|
||||||
|
signature.bodyBytes)) {
|
||||||
|
stdout.writeln('INVALID SIGNATURE');
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
stdout.writeln('OK');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "64.0.0"
|
||||||
|
adaptive_number:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: adaptive_number
|
||||||
|
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.0"
|
||||||
|
args:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
collection:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
convert:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.3"
|
||||||
|
crypto:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
ed25519_edwards:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: ed25519_edwards
|
||||||
|
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
|
github:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: github
|
||||||
|
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.17.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
hex:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hex
|
||||||
|
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.2"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.1"
|
||||||
|
lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
logging:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
node_preamble:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: node_preamble
|
||||||
|
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
path:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
shelf_packages_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_packages_handler
|
||||||
|
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
shelf_static:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_static
|
||||||
|
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
source_map_stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_map_stack_trace
|
||||||
|
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
source_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_maps
|
||||||
|
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.12"
|
||||||
|
source_span:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.24.6"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.1"
|
||||||
|
test_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_core
|
||||||
|
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.6"
|
||||||
|
toml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: toml
|
||||||
|
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.0"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
version:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: version
|
||||||
|
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.9.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
webkit_inspection_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webkit_inspection_protocol
|
||||||
|
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.0.0 <4.0.0"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
name: build_tool
|
||||||
|
description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build.
|
||||||
|
publish_to: none
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
|
# Add regular dependencies here.
|
||||||
|
dependencies:
|
||||||
|
# these are pinned on purpose because the bundle_tool_runner doesn't have
|
||||||
|
# pubspec.lock. See run_build_tool.sh
|
||||||
|
logging: 1.2.0
|
||||||
|
path: 1.8.0
|
||||||
|
version: 3.0.0
|
||||||
|
collection: 1.18.0
|
||||||
|
ed25519_edwards: 0.3.1
|
||||||
|
hex: 0.2.0
|
||||||
|
yaml: 3.1.2
|
||||||
|
source_span: 1.10.0
|
||||||
|
github: 9.17.0
|
||||||
|
args: 2.4.2
|
||||||
|
crypto: 3.0.3
|
||||||
|
convert: 3.1.1
|
||||||
|
http: 1.1.0
|
||||||
|
toml: 0.14.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^2.1.0
|
||||||
|
test: ^1.24.0
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||||
|
|
||||||
|
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||||
|
get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
# REALPATH does not properly resolve symlinks on windows :-/
|
||||||
|
execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
# - target: CMAKE target to which rust library is linked
|
||||||
|
# - manifest_dir: relative path from current folder to directory containing cargo manifest
|
||||||
|
# - lib_name: cargo package name
|
||||||
|
# - any_symbol_name: name of any exported symbol from the library.
|
||||||
|
# used on windows to force linking with library.
|
||||||
|
function(apply_cargokit target manifest_dir lib_name any_symbol_name)
|
||||||
|
|
||||||
|
set(CARGOKIT_LIB_NAME "${lib_name}")
|
||||||
|
set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}")
|
||||||
|
if (CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>")
|
||||||
|
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/${CARGOKIT_LIB_FULL_NAME}")
|
||||||
|
else()
|
||||||
|
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}")
|
||||||
|
endif()
|
||||||
|
set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build")
|
||||||
|
|
||||||
|
if (FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}")
|
||||||
|
else()
|
||||||
|
set(CARGOKIT_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CARGOKIT_ENV
|
||||||
|
"CARGOKIT_CMAKE=${CMAKE_COMMAND}"
|
||||||
|
"CARGOKIT_CONFIGURATION=$<CONFIG>"
|
||||||
|
"CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}"
|
||||||
|
"CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}"
|
||||||
|
"CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}"
|
||||||
|
"CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}"
|
||||||
|
"CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool"
|
||||||
|
"CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
set(SCRIPT_EXTENSION ".cmd")
|
||||||
|
set(IMPORT_LIB_EXTENSION ".lib")
|
||||||
|
else()
|
||||||
|
set(SCRIPT_EXTENSION ".sh")
|
||||||
|
set(IMPORT_LIB_EXTENSION "")
|
||||||
|
execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Using generators in custom command is only supported in CMake 3.20+
|
||||||
|
if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0")
|
||||||
|
foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||||
|
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
else()
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
${OUTPUT_LIB}
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||||
|
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
|
||||||
|
if (TARGET ${target})
|
||||||
|
# If we have actual cmake target provided create target and make existing
|
||||||
|
# target depend on it
|
||||||
|
add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB})
|
||||||
|
add_dependencies("${target}" "${target}_cargokit")
|
||||||
|
target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}")
|
||||||
|
if(WIN32)
|
||||||
|
target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
# Otherwise (FFI) just use ALL to force building always
|
||||||
|
add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Allow adding the output library to plugin bundled libraries
|
||||||
|
set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE)
|
||||||
|
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
function Resolve-Symlinks {
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([string])]
|
||||||
|
param(
|
||||||
|
[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||||
|
[string] $Path
|
||||||
|
)
|
||||||
|
|
||||||
|
[string] $separator = '/'
|
||||||
|
[string[]] $parts = $Path.Split($separator)
|
||||||
|
|
||||||
|
[string] $realPath = ''
|
||||||
|
foreach ($part in $parts) {
|
||||||
|
if ($realPath -and !$realPath.EndsWith($separator)) {
|
||||||
|
$realPath += $separator
|
||||||
|
}
|
||||||
|
|
||||||
|
$realPath += $part.Replace('\', '/')
|
||||||
|
|
||||||
|
# The slash is important when using Get-Item on Drive letters in pwsh.
|
||||||
|
if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) {
|
||||||
|
$realPath += '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = Get-Item $realPath
|
||||||
|
if ($item.LinkTarget) {
|
||||||
|
$realPath = $item.LinkTarget.Replace('\', '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$realPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = Resolve-Symlinks -Path $args[0]
|
||||||
|
Write-Host $path
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
|
CargoKitPlugin.file = buildscript.sourceFile
|
||||||
|
|
||||||
|
apply plugin: CargoKitPlugin
|
||||||
|
|
||||||
|
class CargoKitExtension {
|
||||||
|
String manifestDir; // Relative path to folder containing Cargo.toml
|
||||||
|
String libname; // Library name within Cargo.toml. Must be a cdylib
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CargoKitBuildTask extends DefaultTask {
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String buildMode
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String buildDir
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String outputDir
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String ndkVersion
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String sdkDirectory
|
||||||
|
|
||||||
|
@Input
|
||||||
|
int compileSdkVersion;
|
||||||
|
|
||||||
|
@Input
|
||||||
|
int minSdkVersion;
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String pluginFile
|
||||||
|
|
||||||
|
@Input
|
||||||
|
List<String> targetPlatforms
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
def build() {
|
||||||
|
if (project.cargokit.manifestDir == null) {
|
||||||
|
throw new GradleException("Property 'manifestDir' must be set on cargokit extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.cargokit.libname == null) {
|
||||||
|
throw new GradleException("Property 'libname' must be set on cargokit extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
||||||
|
def path = Paths.get(new File(pluginFile).parent, "..", executableName);
|
||||||
|
|
||||||
|
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir)
|
||||||
|
|
||||||
|
def rootProjectDir = project.rootProject.projectDir
|
||||||
|
|
||||||
|
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
project.exec {
|
||||||
|
commandLine 'chmod', '+x', path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
executable path
|
||||||
|
args "build-gradle"
|
||||||
|
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
||||||
|
environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool"
|
||||||
|
environment "CARGOKIT_MANIFEST_DIR", manifestDir
|
||||||
|
environment "CARGOKIT_CONFIGURATION", buildMode
|
||||||
|
environment "CARGOKIT_TARGET_TEMP_DIR", buildDir
|
||||||
|
environment "CARGOKIT_OUTPUT_DIR", outputDir
|
||||||
|
environment "CARGOKIT_NDK_VERSION", ndkVersion
|
||||||
|
environment "CARGOKIT_SDK_DIR", sdkDirectory
|
||||||
|
environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion
|
||||||
|
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
||||||
|
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
||||||
|
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargoKitPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
static String file;
|
||||||
|
|
||||||
|
private Plugin findFlutterPlugin(Project rootProject) {
|
||||||
|
_findFlutterPlugin(rootProject.childProjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plugin _findFlutterPlugin(Map projects) {
|
||||||
|
for (project in projects) {
|
||||||
|
for (plugin in project.value.getPlugins()) {
|
||||||
|
if (plugin.class.name == "com.flutter.gradle.FlutterPlugin" || plugin.class.name == "FlutterPlugin") {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def plugin = _findFlutterPlugin(project.value.childProjects);
|
||||||
|
if (plugin != null) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void apply(Project project) {
|
||||||
|
def plugin = findFlutterPlugin(project.rootProject);
|
||||||
|
|
||||||
|
project.extensions.create("cargokit", CargoKitExtension)
|
||||||
|
|
||||||
|
if (plugin == null) {
|
||||||
|
print("Flutter plugin not found, CargoKit plugin will not be applied.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
def cargoBuildDir = "${project.buildDir}/build"
|
||||||
|
|
||||||
|
// Determine if the project is an application or library
|
||||||
|
def isApplication = plugin.project.plugins.hasPlugin('com.android.application')
|
||||||
|
def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants
|
||||||
|
|
||||||
|
variants.all { variant ->
|
||||||
|
|
||||||
|
final buildType = variant.buildType.name
|
||||||
|
|
||||||
|
def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}";
|
||||||
|
def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs;
|
||||||
|
jniLibs.srcDir(new File(cargoOutputDir))
|
||||||
|
|
||||||
|
def List<String> platforms
|
||||||
|
try {
|
||||||
|
platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect()
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
platforms = plugin.getTargetPlatforms().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same thing addFlutterDependencies does in flutter.gradle
|
||||||
|
if (buildType == "debug") {
|
||||||
|
platforms.add("android-x86")
|
||||||
|
platforms.add("android-x64")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The task name depends on plugin properties, which are not available
|
||||||
|
// at this point
|
||||||
|
project.getGradle().afterProject {
|
||||||
|
def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}";
|
||||||
|
|
||||||
|
if (project.tasks.findByName(taskName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.project.android.ndkVersion == null) {
|
||||||
|
throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
def task = project.tasks.create(taskName, CargoKitBuildTask.class) {
|
||||||
|
buildMode = variant.buildType.name
|
||||||
|
buildDir = cargoBuildDir
|
||||||
|
outputDir = cargoOutputDir
|
||||||
|
ndkVersion = plugin.project.android.ndkVersion
|
||||||
|
sdkDirectory = plugin.project.android.sdkDirectory
|
||||||
|
minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int
|
||||||
|
compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int
|
||||||
|
targetPlatforms = platforms
|
||||||
|
pluginFile = CargoKitPlugin.file
|
||||||
|
}
|
||||||
|
def onTask = { newTask ->
|
||||||
|
if (newTask.name == "merge${buildType.capitalize()}NativeLibs") {
|
||||||
|
newTask.dependsOn task
|
||||||
|
// Fix gradle 7.4.2 not picking up JNI library changes
|
||||||
|
newTask.outputs.upToDateWhen { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.tasks.each onTask
|
||||||
|
project.tasks.whenTaskAdded onTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
setlocal ENABLEDELAYEDEXPANSION
|
||||||
|
|
||||||
|
SET BASEDIR=%~dp0
|
||||||
|
|
||||||
|
if not exist "%CARGOKIT_TOOL_TEMP_DIR%" (
|
||||||
|
mkdir "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||||
|
)
|
||||||
|
cd /D "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||||
|
|
||||||
|
SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool
|
||||||
|
SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart
|
||||||
|
|
||||||
|
set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/%
|
||||||
|
|
||||||
|
(
|
||||||
|
echo name: build_tool_runner
|
||||||
|
echo version: 1.0.0
|
||||||
|
echo publish_to: none
|
||||||
|
echo.
|
||||||
|
echo environment:
|
||||||
|
echo sdk: '^>=3.0.0 ^<4.0.0'
|
||||||
|
echo.
|
||||||
|
echo dependencies:
|
||||||
|
echo build_tool:
|
||||||
|
echo path: %BUILD_TOOL_PKG_DIR_POSIX%
|
||||||
|
) >pubspec.yaml
|
||||||
|
|
||||||
|
if not exist bin (
|
||||||
|
mkdir bin
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
echo import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
echo void main^(List^<String^> args^) ^{
|
||||||
|
echo build_tool.runMain^(args^);
|
||||||
|
echo ^}
|
||||||
|
) >bin\build_tool_runner.dart
|
||||||
|
|
||||||
|
SET PRECOMPILED=bin\build_tool_runner.dill
|
||||||
|
|
||||||
|
REM To detect changes in package we compare output of DIR /s (recursive)
|
||||||
|
set PREV_PACKAGE_INFO=.dart_tool\package_info.prev
|
||||||
|
set CUR_PACKAGE_INFO=.dart_tool\package_info.cur
|
||||||
|
|
||||||
|
DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig"
|
||||||
|
|
||||||
|
REM Last line in dir output is free space on harddrive. That is bound to
|
||||||
|
REM change between invocation so we need to remove it
|
||||||
|
(
|
||||||
|
Set "Line="
|
||||||
|
For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do (
|
||||||
|
SetLocal EnableDelayedExpansion
|
||||||
|
If Defined Line Echo !Line!
|
||||||
|
EndLocal
|
||||||
|
Set "Line=%%A")
|
||||||
|
) >"%CUR_PACKAGE_INFO%"
|
||||||
|
DEL "%CUR_PACKAGE_INFO%_orig"
|
||||||
|
|
||||||
|
REM Compare current directory listing with previous
|
||||||
|
FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1
|
||||||
|
|
||||||
|
If %ERRORLEVEL% neq 0 (
|
||||||
|
REM Changed - copy current to previous and remove precompiled kernel
|
||||||
|
if exist "%PREV_PACKAGE_INFO%" (
|
||||||
|
DEL "%PREV_PACKAGE_INFO%"
|
||||||
|
)
|
||||||
|
MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%"
|
||||||
|
if exist "%PRECOMPILED%" (
|
||||||
|
DEL "%PRECOMPILED%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO%
|
||||||
|
REM which means we need to do pub get and precompile
|
||||||
|
if not exist "%PRECOMPILED%" (
|
||||||
|
echo Running pub get in "%cd%"
|
||||||
|
"%DART%" pub get --no-precompile
|
||||||
|
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||||
|
)
|
||||||
|
|
||||||
|
"%DART%" "%PRECOMPILED%" %*
|
||||||
|
|
||||||
|
REM 253 means invalid snapshot version.
|
||||||
|
If %ERRORLEVEL% equ 253 (
|
||||||
|
"%DART%" pub get --no-precompile
|
||||||
|
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||||
|
"%DART%" "%PRECOMPILED%" %*
|
||||||
|
)
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
mkdir -p "$CARGOKIT_TOOL_TEMP_DIR"
|
||||||
|
|
||||||
|
cd "$CARGOKIT_TOOL_TEMP_DIR"
|
||||||
|
|
||||||
|
# Write a very simple bin package in temp folder that depends on build_tool package
|
||||||
|
# from Cargokit. This is done to ensure that we don't pollute Cargokit folder
|
||||||
|
# with .dart_tool contents.
|
||||||
|
|
||||||
|
BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool"
|
||||||
|
|
||||||
|
if [[ -z $FLUTTER_ROOT ]]; then # not defined
|
||||||
|
DART=dart
|
||||||
|
else
|
||||||
|
DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF > "pubspec.yaml"
|
||||||
|
name: build_tool_runner
|
||||||
|
version: 1.0.0
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
build_tool:
|
||||||
|
path: "$BUILD_TOOL_PKG_DIR"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "bin"
|
||||||
|
|
||||||
|
cat << EOF > "bin/build_tool_runner.dart"
|
||||||
|
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
void main(List<String> args) {
|
||||||
|
build_tool.runMain(args);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create alias for `shasum` if it does not exist and `sha1sum` exists
|
||||||
|
if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then
|
||||||
|
shopt -s expand_aliases
|
||||||
|
alias shasum="sha1sum"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dart run will not cache any package that has a path dependency, which
|
||||||
|
# is the case for our build_tool_runner. So instead we precompile the package
|
||||||
|
# ourselves.
|
||||||
|
# To invalidate the cached kernel we use the hash of ls -LR of the build_tool
|
||||||
|
# package directory. This should be good enough, as the build_tool package
|
||||||
|
# itself is not meant to have any path dependencies.
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||||
|
else
|
||||||
|
PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGE_HASH_FILE=".package_hash"
|
||||||
|
|
||||||
|
if [ -f "$PACKAGE_HASH_FILE" ]; then
|
||||||
|
EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE")
|
||||||
|
if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then
|
||||||
|
rm "$PACKAGE_HASH_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run pub get if needed.
|
||||||
|
if [ ! -f "$PACKAGE_HASH_FILE" ]; then
|
||||||
|
"$DART" pub get --no-precompile
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuild the tool if it was deleted by Android Studio
|
||||||
|
if [ ! -f "bin/build_tool_runner.dill" ]; then
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
"$DART" bin/build_tool_runner.dill "$@"
|
||||||
|
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
# 253 means invalid snapshot version.
|
||||||
|
if [ $exit_code == 253 ]; then
|
||||||
|
"$DART" pub get --no-precompile
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
"$DART" bin/build_tool_runner.dill "$@"
|
||||||
|
exit_code=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
rust_input: crate::api
|
||||||
|
rust_root: rust/
|
||||||
|
dart_output: lib/src/rust
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// This is an empty file to force CocoaPods to create a framework.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint komet_crypto.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'komet_crypto'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new Flutter FFI plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new Flutter FFI plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
s.module_name = 'komet_crypto'
|
||||||
|
|
||||||
|
# This will ensure the source files in Classes/ are included in the native
|
||||||
|
# builds of apps using this FFI plugin. Podspec does not support relative
|
||||||
|
# paths, so Classes contains a forwarder C file that relatively imports
|
||||||
|
# `../src/*` so that the C sources can be shared among all target platforms.
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.dependency 'Flutter'
|
||||||
|
s.platform = :ios, '11.0'
|
||||||
|
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
|
||||||
|
s.script_phase = {
|
||||||
|
:name => 'Build Rust library',
|
||||||
|
# First argument is relative path to the `rust` folder, second is name of rust library
|
||||||
|
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust komet_crypto',
|
||||||
|
:execution_position => :before_compile,
|
||||||
|
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||||
|
# Let XCode know that the static library referenced in -force_load below is
|
||||||
|
# created by this build step.
|
||||||
|
:output_files => ["${PODS_CONFIGURATION_BUILD_DIR}/komet_crypto/libkomet_crypto.a"],
|
||||||
|
}
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||||
|
'OTHER_LDFLAGS' => '-force_load ${PODS_CONFIGURATION_BUILD_DIR}/komet_crypto/libkomet_crypto.a',
|
||||||
|
}
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
export 'src/rust/api/crypto.dart';
|
||||||
|
export 'src/rust/frb_generated.dart' show RustLib;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import '../frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
// These functions are ignored because they are not marked as `pub`: `transform_file`
|
||||||
|
|
||||||
|
Future<Uint8List> deriveKey({required String password}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoDeriveKey(password: password);
|
||||||
|
|
||||||
|
Future<String> encryptMessage(
|
||||||
|
{required String plaintext, required List<int> key}) =>
|
||||||
|
RustLib.instance.api
|
||||||
|
.crateApiCryptoEncryptMessage(plaintext: plaintext, key: key);
|
||||||
|
|
||||||
|
Future<String> decryptMessage({required String text, required List<int> key}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoDecryptMessage(text: text, key: key);
|
||||||
|
|
||||||
|
Future<bool> looksEncrypted({required String text}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoLooksEncrypted(text: text);
|
||||||
|
|
||||||
|
Future<void> encryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoEncryptImageFile(
|
||||||
|
sourcePath: sourcePath, destPath: destPath, key: key);
|
||||||
|
|
||||||
|
Future<void> decryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoDecryptImageFile(
|
||||||
|
sourcePath: sourcePath, destPath: destPath, key: key);
|
||||||
|
|
||||||
|
Future<bool> looksEncryptedImageFile({required String path}) =>
|
||||||
|
RustLib.instance.api.crateApiCryptoLooksEncryptedImageFile(path: path);
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/crypto.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'frb_generated.io.dart'
|
||||||
|
if (dart.library.js_interop) 'frb_generated.web.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
/// Main entrypoint of the Rust API
|
||||||
|
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
|
@internal
|
||||||
|
static final instance = RustLib._();
|
||||||
|
|
||||||
|
RustLib._();
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge
|
||||||
|
static Future<void> init({
|
||||||
|
RustLibApi? api,
|
||||||
|
BaseHandler? handler,
|
||||||
|
ExternalLibrary? externalLibrary,
|
||||||
|
bool forceSameCodegenVersion = true,
|
||||||
|
}) async {
|
||||||
|
await instance.initImpl(
|
||||||
|
api: api,
|
||||||
|
handler: handler,
|
||||||
|
externalLibrary: externalLibrary,
|
||||||
|
forceSameCodegenVersion: forceSameCodegenVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge in mock mode.
|
||||||
|
/// No libraries for FFI are loaded.
|
||||||
|
static void initMock({
|
||||||
|
required RustLibApi api,
|
||||||
|
}) {
|
||||||
|
instance.initMockImpl(
|
||||||
|
api: api,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose flutter_rust_bridge
|
||||||
|
///
|
||||||
|
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
|
||||||
|
/// is automatically disposed when the app stops.
|
||||||
|
static void dispose() => instance.disposeImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ApiImplConstructor<RustLibApiImpl, RustLibWire> get apiImplConstructor =>
|
||||||
|
RustLibApiImpl.new;
|
||||||
|
|
||||||
|
@override
|
||||||
|
WireConstructor<RustLibWire> get wireConstructor =>
|
||||||
|
RustLibWire.fromExternalLibrary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> executeRustInitializers() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig =>
|
||||||
|
kDefaultExternalLibraryLoaderConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get rustContentHash => -2021377439;
|
||||||
|
|
||||||
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
|
ExternalLibraryLoaderConfig(
|
||||||
|
stem: 'komet_crypto',
|
||||||
|
ioDirectory: 'rust/target/release/',
|
||||||
|
webPrefix: 'pkg/',
|
||||||
|
wasmBindgenName: 'wasm_bindgen',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RustLibApi extends BaseApi {
|
||||||
|
Future<void> crateApiCryptoDecryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key});
|
||||||
|
|
||||||
|
Future<String> crateApiCryptoDecryptMessage(
|
||||||
|
{required String text, required List<int> key});
|
||||||
|
|
||||||
|
Future<Uint8List> crateApiCryptoDeriveKey({required String password});
|
||||||
|
|
||||||
|
Future<void> crateApiCryptoEncryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key});
|
||||||
|
|
||||||
|
Future<String> crateApiCryptoEncryptMessage(
|
||||||
|
{required String plaintext, required List<int> key});
|
||||||
|
|
||||||
|
Future<bool> crateApiCryptoLooksEncrypted({required String text});
|
||||||
|
|
||||||
|
Future<bool> crateApiCryptoLooksEncryptedImageFile({required String path});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
RustLibApiImpl({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiCryptoDecryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(sourcePath, serializer);
|
||||||
|
sse_encode_String(destPath, serializer);
|
||||||
|
sse_encode_list_prim_u_8_loose(key, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 1, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoDecryptImageFileConstMeta,
|
||||||
|
argValues: [sourcePath, destPath, key],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoDecryptImageFileConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "decrypt_image_file",
|
||||||
|
argNames: ["sourcePath", "destPath", "key"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> crateApiCryptoDecryptMessage(
|
||||||
|
{required String text, required List<int> key}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(text, serializer);
|
||||||
|
sse_encode_list_prim_u_8_loose(key, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 2, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_String,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoDecryptMessageConstMeta,
|
||||||
|
argValues: [text, key],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoDecryptMessageConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "decrypt_message",
|
||||||
|
argNames: ["text", "key"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> crateApiCryptoDeriveKey({required String password}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(password, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 3, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_list_prim_u_8_strict,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoDeriveKeyConstMeta,
|
||||||
|
argValues: [password],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoDeriveKeyConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "derive_key",
|
||||||
|
argNames: ["password"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiCryptoEncryptImageFile(
|
||||||
|
{required String sourcePath,
|
||||||
|
required String destPath,
|
||||||
|
required List<int> key}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(sourcePath, serializer);
|
||||||
|
sse_encode_String(destPath, serializer);
|
||||||
|
sse_encode_list_prim_u_8_loose(key, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 4, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoEncryptImageFileConstMeta,
|
||||||
|
argValues: [sourcePath, destPath, key],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoEncryptImageFileConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "encrypt_image_file",
|
||||||
|
argNames: ["sourcePath", "destPath", "key"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> crateApiCryptoEncryptMessage(
|
||||||
|
{required String plaintext, required List<int> key}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(plaintext, serializer);
|
||||||
|
sse_encode_list_prim_u_8_loose(key, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 5, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_String,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoEncryptMessageConstMeta,
|
||||||
|
argValues: [plaintext, key],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoEncryptMessageConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "encrypt_message",
|
||||||
|
argNames: ["plaintext", "key"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> crateApiCryptoLooksEncrypted({required String text}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(text, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 6, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_bool,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoLooksEncryptedConstMeta,
|
||||||
|
argValues: [text],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoLooksEncryptedConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "looks_encrypted",
|
||||||
|
argNames: ["text"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> crateApiCryptoLooksEncryptedImageFile({required String path}) {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(path, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 7, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_bool,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoLooksEncryptedImageFileConstMeta,
|
||||||
|
argValues: [path],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoLooksEncryptedImageFileConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "looks_encrypted_image_file",
|
||||||
|
argNames: ["path"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool dco_decode_bool(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> dco_decode_list_prim_u_8_loose(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as List<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as Uint8List;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var inner = sse_decode_list_prim_u_8_strict(deserializer);
|
||||||
|
return utf8.decoder.convert(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var len_ = sse_decode_i_32(deserializer);
|
||||||
|
return deserializer.buffer.getUint8List(len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var len_ = sse_decode_i_32(deserializer);
|
||||||
|
return deserializer.buffer.getUint8List(len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_loose(
|
||||||
|
List<int> self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_i_32(self.length, serializer);
|
||||||
|
serializer.buffer
|
||||||
|
.putUint8List(self is Uint8List ? self : Uint8List.fromList(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_i_32(self.length, serializer);
|
||||||
|
serializer.buffer.putUint8List(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putInt32(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/crypto.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi' as ffi;
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
|
||||||
|
RustLibWire(lib.ffiDynamicLibrary);
|
||||||
|
|
||||||
|
/// Holds the symbol lookup function.
|
||||||
|
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||||
|
_lookup;
|
||||||
|
|
||||||
|
/// The symbols are looked up in [dynamicLibrary].
|
||||||
|
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||||
|
: _lookup = dynamicLibrary.lookup;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
// Static analysis wrongly picks the IO variant, thus ignore this
|
||||||
|
// ignore_for_file: argument_type_not_assignable
|
||||||
|
|
||||||
|
import 'api/crypto.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('wasm_bindgen')
|
||||||
|
external RustLibWasmModule get wasmModule;
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
@anonymous
|
||||||
|
extension type RustLibWasmModule._(JSObject _) implements JSObject {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# The Flutter tooling requires that developers have CMake 3.10 or later
|
||||||
|
# installed. You should not increase this version, as doing so will cause
|
||||||
|
# the plugin to fail to compile for some customers of the plugin.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
# Project-level configuration.
|
||||||
|
set(PROJECT_NAME "komet_crypto")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
include("../cargokit/cmake/cargokit.cmake")
|
||||||
|
apply_cargokit(${PROJECT_NAME} ../rust komet_crypto "")
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||||
|
# This list could contain prebuilt libraries, or libraries created by an
|
||||||
|
# external build triggered from this build file.
|
||||||
|
set(komet_crypto_bundled_libraries
|
||||||
|
"${${PROJECT_NAME}_cargokit_lib}"
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// This is an empty file to force CocoaPods to create a framework.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint komet_crypto.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'komet_crypto'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new Flutter FFI plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new Flutter FFI plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
s.module_name = 'komet_crypto'
|
||||||
|
|
||||||
|
# This will ensure the source files in Classes/ are included in the native
|
||||||
|
# builds of apps using this FFI plugin. Podspec does not support relative
|
||||||
|
# paths, so Classes contains a forwarder C file that relatively imports
|
||||||
|
# `../src/*` so that the C sources can be shared among all target platforms.
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.dependency 'FlutterMacOS'
|
||||||
|
|
||||||
|
s.platform = :osx, '10.11'
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
|
||||||
|
s.script_phase = {
|
||||||
|
:name => 'Build Rust library',
|
||||||
|
# First argument is relative path to the `rust` folder, second is name of rust library
|
||||||
|
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust komet_crypto',
|
||||||
|
:execution_position => :before_compile,
|
||||||
|
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||||
|
# Let XCode know that the static library referenced in -force_load below is
|
||||||
|
# created by this build step.
|
||||||
|
:output_files => ["${PODS_CONFIGURATION_BUILD_DIR}/komet_crypto/libkomet_crypto.a"],
|
||||||
|
}
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||||
|
'OTHER_LDFLAGS' => '-force_load ${PODS_CONFIGURATION_BUILD_DIR}/komet_crypto/libkomet_crypto.a',
|
||||||
|
}
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: komet_crypto
|
||||||
|
description: "Message encryption core for Komet (Argon2id + ChaCha20-Poly1305 + Cyrillic base32)."
|
||||||
|
version: 0.1.0
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
flutter: ">=3.3.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_rust_bridge: 2.12.0
|
||||||
|
plugin_platform_interface: ^2.0.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
ffiPlugin: true
|
||||||
|
ios:
|
||||||
|
ffiPlugin: true
|
||||||
|
linux:
|
||||||
|
ffiPlugin: true
|
||||||
|
macos:
|
||||||
|
ffiPlugin: true
|
||||||
|
windows:
|
||||||
|
ffiPlugin: true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "komet_crypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Message encryption core for Komet (Argon2id + ChaCha20-Poly1305 + Cyrillic base32)"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argon2 = "0.5"
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
|
data-encoding = "2"
|
||||||
|
flutter_rust_bridge = "=2.12.0"
|
||||||
|
png = "0.17"
|
||||||
|
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "unwind"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use komet_crypto::cipher;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let password = "мой ключ 2026";
|
||||||
|
let started = Instant::now();
|
||||||
|
let key = cipher::derive_key(password).expect("derive");
|
||||||
|
println!("derive_key: {:?}", started.elapsed());
|
||||||
|
|
||||||
|
for text in [
|
||||||
|
"привет",
|
||||||
|
"встречаемся в 19:00 у метро",
|
||||||
|
"Hello! Это смешанный текст с эмодзи 🔐",
|
||||||
|
] {
|
||||||
|
let encrypted = cipher::encrypt(text, &key).expect("encrypt");
|
||||||
|
let decrypted = cipher::decrypt(&encrypted, &key).expect("decrypt");
|
||||||
|
println!(
|
||||||
|
"\n{} символов -> {} символов",
|
||||||
|
text.chars().count(),
|
||||||
|
encrypted.chars().count()
|
||||||
|
);
|
||||||
|
println!(" {text}");
|
||||||
|
println!(" {encrypted}");
|
||||||
|
assert_eq!(decrypted, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrong = cipher::derive_key("не тот ключ").expect("derive");
|
||||||
|
let sample = cipher::encrypt("секрет", &key).expect("encrypt");
|
||||||
|
println!("\nчужой ключ: {:?}", cipher::decrypt(&sample, &wrong));
|
||||||
|
println!(
|
||||||
|
"обычный текст: {:?}",
|
||||||
|
cipher::decrypt("привет как дела", &key)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
use data_encoding::BASE32_NOPAD;
|
||||||
|
|
||||||
|
use crate::error::CryptoError;
|
||||||
|
|
||||||
|
pub const RU_LOWERCASE_WITHOUT_YO: [char; 32] = [
|
||||||
|
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т',
|
||||||
|
'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я',
|
||||||
|
];
|
||||||
|
|
||||||
|
const MIN_WORD_LEN: usize = 4;
|
||||||
|
const WORD_LEN_SPREAD: usize = 5;
|
||||||
|
|
||||||
|
fn base32_symbol(index: usize) -> u8 {
|
||||||
|
if index < 26 {
|
||||||
|
b'A' + index as u8
|
||||||
|
} else {
|
||||||
|
b'2' + (index - 26) as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base32_index(symbol: u8) -> Option<usize> {
|
||||||
|
match symbol {
|
||||||
|
b'A'..=b'Z' => Some((symbol - b'A') as usize),
|
||||||
|
b'2'..=b'7' => Some((symbol - b'2') as usize + 26),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn letter_index(letter: char) -> Option<usize> {
|
||||||
|
RU_LOWERCASE_WITHOUT_YO.iter().position(|&l| l == letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(bytes: &[u8]) -> String {
|
||||||
|
let base32 = BASE32_NOPAD.encode(bytes);
|
||||||
|
let mut out = String::with_capacity(base32.len() * 3);
|
||||||
|
let mut run = 0usize;
|
||||||
|
let mut word_len = MIN_WORD_LEN;
|
||||||
|
for symbol in base32.bytes() {
|
||||||
|
let Some(index) = base32_index(symbol) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if run == word_len {
|
||||||
|
out.push(' ');
|
||||||
|
run = 0;
|
||||||
|
word_len = MIN_WORD_LEN + index % WORD_LEN_SPREAD;
|
||||||
|
}
|
||||||
|
out.push(RU_LOWERCASE_WITHOUT_YO[index]);
|
||||||
|
run += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(text: &str) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let mut symbols = Vec::with_capacity(text.len());
|
||||||
|
for raw in text.chars() {
|
||||||
|
if raw.is_whitespace() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let letter = raw.to_lowercase().next().unwrap_or(raw);
|
||||||
|
let index = letter_index(letter).ok_or(CryptoError::NotEncrypted)?;
|
||||||
|
symbols.push(base32_symbol(index));
|
||||||
|
}
|
||||||
|
if symbols.is_empty() {
|
||||||
|
return Err(CryptoError::NotEncrypted);
|
||||||
|
}
|
||||||
|
BASE32_NOPAD
|
||||||
|
.decode(&symbols)
|
||||||
|
.map_err(|_| CryptoError::Malformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alphabet_has_no_duplicates_and_no_yo() {
|
||||||
|
let mut sorted = RU_LOWERCASE_WITHOUT_YO.to_vec();
|
||||||
|
sorted.sort_unstable();
|
||||||
|
sorted.dedup();
|
||||||
|
assert_eq!(sorted.len(), 32);
|
||||||
|
assert!(!RU_LOWERCASE_WITHOUT_YO.contains(&'ё'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_preserves_bytes() {
|
||||||
|
for len in 1..64usize {
|
||||||
|
let bytes: Vec<u8> = (0..len).map(|i| (i * 37 + 11) as u8).collect();
|
||||||
|
let encoded = encode(&bytes);
|
||||||
|
assert_eq!(decode(&encoded).unwrap(), bytes, "len {len}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_input_encodes_to_empty_string() {
|
||||||
|
assert_eq!(encode(&[]), "");
|
||||||
|
assert_eq!(decode(""), Err(CryptoError::NotEncrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_is_lowercase_cyrillic_and_spaces() {
|
||||||
|
let encoded = encode(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||||
|
for ch in encoded.chars() {
|
||||||
|
assert!(ch == ' ' || RU_LOWERCASE_WITHOUT_YO.contains(&ch), "{ch}");
|
||||||
|
}
|
||||||
|
assert!(encoded.contains(' '));
|
||||||
|
assert!(!encoded.contains(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaces_are_decorative_only() {
|
||||||
|
let bytes = b"komet encryption core";
|
||||||
|
let encoded = encode(bytes);
|
||||||
|
let stripped: String = encoded.chars().filter(|c| *c != ' ').collect();
|
||||||
|
let padded = format!(" {} ", encoded.replace(' ', " "));
|
||||||
|
let newlined = encoded.replace(' ', "\n");
|
||||||
|
assert_eq!(decode(&stripped).unwrap(), bytes);
|
||||||
|
assert_eq!(decode(&padded).unwrap(), bytes);
|
||||||
|
assert_eq!(decode(&newlined).unwrap(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_is_case_insensitive() {
|
||||||
|
let bytes = b"autocapitalized";
|
||||||
|
let encoded = encode(bytes);
|
||||||
|
assert_eq!(decode(&encoded.to_uppercase()).unwrap(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foreign_characters_are_rejected() {
|
||||||
|
assert_eq!(decode("привет!"), Err(CryptoError::NotEncrypted));
|
||||||
|
assert_eq!(decode("hello"), Err(CryptoError::NotEncrypted));
|
||||||
|
assert_eq!(decode("ёжик"), Err(CryptoError::NotEncrypted));
|
||||||
|
assert_eq!(decode(" "), Err(CryptoError::NotEncrypted));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::cipher;
|
||||||
|
use crate::error::CryptoError;
|
||||||
|
use crate::image;
|
||||||
|
|
||||||
|
pub fn derive_key(password: String) -> Result<Vec<u8>, String> {
|
||||||
|
cipher::derive_key(&password).map_err(|e| e.code().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_message(plaintext: String, key: Vec<u8>) -> Result<String, String> {
|
||||||
|
cipher::encrypt(&plaintext, &key).map_err(|e| e.code().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_message(text: String, key: Vec<u8>) -> Result<String, String> {
|
||||||
|
cipher::decrypt(&text, &key).map_err(|e| e.code().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn looks_encrypted(text: String) -> bool {
|
||||||
|
cipher::looks_encrypted(&text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_image_file(
|
||||||
|
source_path: String,
|
||||||
|
dest_path: String,
|
||||||
|
key: Vec<u8>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
transform_file(&source_path, &dest_path, |bytes| {
|
||||||
|
image::encrypt(bytes, &key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_image_file(
|
||||||
|
source_path: String,
|
||||||
|
dest_path: String,
|
||||||
|
key: Vec<u8>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
transform_file(&source_path, &dest_path, |bytes| {
|
||||||
|
image::decrypt(bytes, &key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn looks_encrypted_image_file(path: String) -> bool {
|
||||||
|
fs::read(&path)
|
||||||
|
.map(|bytes| image::looks_encrypted(&bytes))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_file(
|
||||||
|
source_path: &str,
|
||||||
|
dest_path: &str,
|
||||||
|
transform: impl FnOnce(&[u8]) -> Result<Vec<u8>, CryptoError>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let bytes = fs::read(source_path).map_err(|e| format!("read: {e}"))?;
|
||||||
|
let out = transform(&bytes).map_err(|e| e.code().to_string())?;
|
||||||
|
fs::write(dest_path, out).map_err(|e| format!("write: {e}"))
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod crypto;
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
|
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng, Payload};
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::alphabet;
|
||||||
|
use crate::error::CryptoError;
|
||||||
|
|
||||||
|
pub const KEY_LEN: usize = 32;
|
||||||
|
|
||||||
|
pub(crate) const MAGIC: u8 = 0x4B;
|
||||||
|
const VERSION: u8 = 0x01;
|
||||||
|
const HEADER_LEN: usize = 2;
|
||||||
|
pub(crate) const NONCE_LEN: usize = 12;
|
||||||
|
pub(crate) const TAG_LEN: usize = 16;
|
||||||
|
const MIN_BLOB_LEN: usize = HEADER_LEN + NONCE_LEN + TAG_LEN;
|
||||||
|
|
||||||
|
const SALT_CONTEXT: &[u8] = b"komet-enc-v1";
|
||||||
|
const SALT_LEN: usize = 16;
|
||||||
|
const ARGON_MEMORY_KIB: u32 = 65536;
|
||||||
|
const ARGON_ITERATIONS: u32 = 3;
|
||||||
|
const ARGON_PARALLELISM: u32 = 1;
|
||||||
|
|
||||||
|
pub fn derive_key(password: &str) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
if password.is_empty() {
|
||||||
|
return Err(CryptoError::EmptyPassword);
|
||||||
|
}
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(SALT_CONTEXT);
|
||||||
|
hasher.update(password.as_bytes());
|
||||||
|
let digest = hasher.finalize();
|
||||||
|
|
||||||
|
let params = Params::new(
|
||||||
|
ARGON_MEMORY_KIB,
|
||||||
|
ARGON_ITERATIONS,
|
||||||
|
ARGON_PARALLELISM,
|
||||||
|
Some(KEY_LEN),
|
||||||
|
)
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||||
|
|
||||||
|
let mut key = vec![0u8; KEY_LEN];
|
||||||
|
argon
|
||||||
|
.hash_password_into(password.as_bytes(), &digest[..SALT_LEN], &mut key)
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(plaintext: &str, key: &[u8]) -> Result<String, CryptoError> {
|
||||||
|
let cipher = cipher_from(key)?;
|
||||||
|
let header = [MAGIC, VERSION];
|
||||||
|
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||||
|
let sealed = cipher
|
||||||
|
.encrypt(
|
||||||
|
&nonce,
|
||||||
|
Payload {
|
||||||
|
msg: plaintext.as_bytes(),
|
||||||
|
aad: &header,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
|
||||||
|
let mut blob = Vec::with_capacity(HEADER_LEN + NONCE_LEN + sealed.len());
|
||||||
|
blob.extend_from_slice(&header);
|
||||||
|
blob.extend_from_slice(&nonce);
|
||||||
|
blob.extend_from_slice(&sealed);
|
||||||
|
Ok(alphabet::encode(&blob))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(text: &str, key: &[u8]) -> Result<String, CryptoError> {
|
||||||
|
let blob = alphabet::decode(text)?;
|
||||||
|
if !has_envelope(&blob) {
|
||||||
|
return Err(CryptoError::NotEncrypted);
|
||||||
|
}
|
||||||
|
let cipher = cipher_from(key)?;
|
||||||
|
let nonce = Nonce::from_slice(&blob[HEADER_LEN..HEADER_LEN + NONCE_LEN]);
|
||||||
|
let plain = cipher
|
||||||
|
.decrypt(
|
||||||
|
nonce,
|
||||||
|
Payload {
|
||||||
|
msg: &blob[HEADER_LEN + NONCE_LEN..],
|
||||||
|
aad: &blob[..HEADER_LEN],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| CryptoError::WrongKey)?;
|
||||||
|
String::from_utf8(plain).map_err(|_| CryptoError::Malformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn looks_encrypted(text: &str) -> bool {
|
||||||
|
alphabet::decode(text)
|
||||||
|
.map(|b| has_envelope(&b))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_envelope(blob: &[u8]) -> bool {
|
||||||
|
blob.len() >= MIN_BLOB_LEN && blob[0] == MAGIC && blob[1] == VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cipher_from(key: &[u8]) -> Result<ChaCha20Poly1305, CryptoError> {
|
||||||
|
if key.len() != KEY_LEN {
|
||||||
|
return Err(CryptoError::BadKeyLength);
|
||||||
|
}
|
||||||
|
Ok(ChaCha20Poly1305::new(Key::from_slice(key)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const KEY: [u8; KEY_LEN] = [7u8; KEY_LEN];
|
||||||
|
const OTHER_KEY: [u8; KEY_LEN] = [8u8; KEY_LEN];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_returns_original_text() {
|
||||||
|
for text in [
|
||||||
|
"привет",
|
||||||
|
"Hello, World!",
|
||||||
|
"эмодзи 🔐 и переносы\nстрок",
|
||||||
|
"a",
|
||||||
|
"\u{0}\u{1}",
|
||||||
|
] {
|
||||||
|
let encrypted = encrypt(text, &KEY).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &KEY).unwrap(), text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_plaintext_roundtrips() {
|
||||||
|
let encrypted = encrypt("", &KEY).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &KEY).unwrap(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ciphertext_looks_like_russian_words() {
|
||||||
|
let encrypted = encrypt("привет", &KEY).unwrap();
|
||||||
|
for ch in encrypted.chars() {
|
||||||
|
assert!(
|
||||||
|
ch == ' ' || alphabet::RU_LOWERCASE_WITHOUT_YO.contains(&ch),
|
||||||
|
"unexpected char {ch}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(encrypted.split(' ').count() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_plaintext_produces_different_ciphertext() {
|
||||||
|
let a = encrypt("одно и то же", &KEY).unwrap();
|
||||||
|
let b = encrypt("одно и то же", &KEY).unwrap();
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_key_is_rejected() {
|
||||||
|
let encrypted = encrypt("секрет", &KEY).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &OTHER_KEY), Err(CryptoError::WrongKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swap_letter_at(text: &str, position: usize) -> String {
|
||||||
|
text.chars()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| {
|
||||||
|
if i != position {
|
||||||
|
c
|
||||||
|
} else if c == 'а' {
|
||||||
|
'б'
|
||||||
|
} else {
|
||||||
|
'а'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tampered_ciphertext_is_rejected() {
|
||||||
|
let encrypted = encrypt("секрет", &KEY).unwrap();
|
||||||
|
let letters: Vec<usize> = encrypted
|
||||||
|
.char_indices()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, (_, c))| *c != ' ')
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
for position in &letters {
|
||||||
|
assert!(
|
||||||
|
decrypt(&swap_letter_at(&encrypted, *position), &KEY).is_err(),
|
||||||
|
"tamper at {position} slipped through"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let middle = letters[letters.len() / 2];
|
||||||
|
assert_eq!(
|
||||||
|
decrypt(&swap_letter_at(&encrypted, middle), &KEY),
|
||||||
|
Err(CryptoError::WrongKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whitespace_mangling_survives() {
|
||||||
|
let encrypted = encrypt("пробелы декоративные", &KEY).unwrap();
|
||||||
|
let no_spaces: String = encrypted.chars().filter(|c| *c != ' ').collect();
|
||||||
|
let doubled = encrypted.replace(' ', " ");
|
||||||
|
let trimmed = format!(" {encrypted}\n");
|
||||||
|
for variant in [no_spaces, doubled, trimmed] {
|
||||||
|
assert_eq!(decrypt(&variant, &KEY).unwrap(), "пробелы декоративные");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_text_is_not_mistaken_for_ciphertext() {
|
||||||
|
for text in ["привет как дела", "ёлка", "hello", "", "12345"] {
|
||||||
|
assert!(!looks_encrypted(text), "{text}");
|
||||||
|
}
|
||||||
|
let encrypted = encrypt("настоящее", &KEY).unwrap();
|
||||||
|
assert!(looks_encrypted(&encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_text_decrypt_reports_not_encrypted() {
|
||||||
|
assert_eq!(
|
||||||
|
decrypt("привет как дела", &KEY),
|
||||||
|
Err(CryptoError::NotEncrypted)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_key_length_is_reported() {
|
||||||
|
assert_eq!(encrypt("x", &[0u8; 8]), Err(CryptoError::BadKeyLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overhead_is_48_letters() {
|
||||||
|
let encrypted = encrypt("", &KEY).unwrap();
|
||||||
|
let letters = encrypted.chars().filter(|c| *c != ' ').count();
|
||||||
|
assert_eq!(letters, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_derivation_is_deterministic_and_password_bound() {
|
||||||
|
let a = derive_key("общий ключ").unwrap();
|
||||||
|
let b = derive_key("общий ключ").unwrap();
|
||||||
|
let c = derive_key("другой ключ").unwrap();
|
||||||
|
assert_eq!(a, b);
|
||||||
|
assert_ne!(a, c);
|
||||||
|
assert_eq!(a.len(), KEY_LEN);
|
||||||
|
assert_eq!(derive_key(""), Err(CryptoError::EmptyPassword));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CryptoError {
|
||||||
|
EmptyPassword,
|
||||||
|
BadKeyLength,
|
||||||
|
NotEncrypted,
|
||||||
|
Malformed,
|
||||||
|
WrongKey,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoError {
|
||||||
|
pub fn code(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CryptoError::EmptyPassword => "empty_password",
|
||||||
|
CryptoError::BadKeyLength => "bad_key_length",
|
||||||
|
CryptoError::NotEncrypted => "not_encrypted",
|
||||||
|
CryptoError::Malformed => "malformed",
|
||||||
|
CryptoError::WrongKey => "wrong_key",
|
||||||
|
CryptoError::Internal => "internal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CryptoError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(self.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CryptoError {}
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
non_camel_case_types,
|
||||||
|
unused,
|
||||||
|
non_snake_case,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::redundant_closure_call,
|
||||||
|
clippy::redundant_closure,
|
||||||
|
clippy::useless_conversion,
|
||||||
|
clippy::unit_arg,
|
||||||
|
clippy::unused_unit,
|
||||||
|
clippy::double_parens,
|
||||||
|
clippy::let_and_return,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::match_single_binding,
|
||||||
|
clippy::clone_on_copy,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
clippy::deref_addrof,
|
||||||
|
clippy::explicit_auto_deref,
|
||||||
|
clippy::borrow_deref_ref,
|
||||||
|
clippy::uninlined_format_args,
|
||||||
|
clippy::needless_borrow
|
||||||
|
)]
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
|
default_stream_sink_codec = SseCodec,
|
||||||
|
default_rust_opaque = RustOpaqueMoi,
|
||||||
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
|
);
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2021377439;
|
||||||
|
|
||||||
|
// Section: executor
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_default_handler!();
|
||||||
|
|
||||||
|
// Section: wire_funcs
|
||||||
|
|
||||||
|
fn wire__crate__api__crypto__decrypt_image_file_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "decrypt_image_file",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_source_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_dest_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::crypto::decrypt_image_file(
|
||||||
|
api_source_path,
|
||||||
|
api_dest_path,
|
||||||
|
api_key,
|
||||||
|
)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__decrypt_message_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "decrypt_message",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_text = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::crypto::decrypt_message(api_text, api_key)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__derive_key_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "derive_key",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_password = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::crypto::derive_key(api_password)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__encrypt_image_file_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "encrypt_image_file",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_source_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_dest_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::crypto::encrypt_image_file(
|
||||||
|
api_source_path,
|
||||||
|
api_dest_path,
|
||||||
|
api_key,
|
||||||
|
)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__encrypt_message_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "encrypt_message",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_plaintext = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::crypto::encrypt_message(api_plaintext, api_key)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__looks_encrypted_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "looks_encrypted",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_text = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok =
|
||||||
|
Result::<_, ()>::Ok(crate::api::crypto::looks_encrypted(api_text))?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__crypto__looks_encrypted_image_file_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "looks_encrypted_image_file",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok(
|
||||||
|
crate::api::crypto::looks_encrypted_image_file(api_path),
|
||||||
|
)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: dart2rust
|
||||||
|
|
||||||
|
impl SseDecode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut inner = <Vec<u8>>::sse_decode(deserializer);
|
||||||
|
return String::from_utf8(inner).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap() != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut len_ = <i32>::sse_decode(deserializer);
|
||||||
|
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||||
|
for idx_ in 0..len_ {
|
||||||
|
ans_.push(<u8>::sse_decode(deserializer));
|
||||||
|
}
|
||||||
|
return ans_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_primary_impl(
|
||||||
|
func_id: i32,
|
||||||
|
port: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
1 => wire__crate__api__crypto__decrypt_image_file_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
2 => wire__crate__api__crypto__decrypt_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
3 => wire__crate__api__crypto__derive_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
4 => wire__crate__api__crypto__encrypt_image_file_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
5 => wire__crate__api__crypto__encrypt_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
6 => wire__crate__api__crypto__looks_encrypted_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
7 => wire__crate__api__crypto__looks_encrypted_image_file_impl(
|
||||||
|
port,
|
||||||
|
ptr,
|
||||||
|
rust_vec_len,
|
||||||
|
data_len,
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_sync_impl(
|
||||||
|
func_id: i32,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: rust2dart
|
||||||
|
|
||||||
|
impl SseEncode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<Vec<u8>>::sse_encode(self.into_bytes(), serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self as _).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<i32>::sse_encode(self.len() as _, serializer);
|
||||||
|
for item in self {
|
||||||
|
<u8>::sse_encode(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod io {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use io::*;
|
||||||
|
|
||||||
|
/// cbindgen:ignore
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
mod web {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen;
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub use web::*;
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use chacha20poly1305::aead::{Aead, AeadCore, OsRng, Payload};
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
use crate::cipher::{cipher_from, MAGIC, NONCE_LEN, TAG_LEN};
|
||||||
|
use crate::error::CryptoError;
|
||||||
|
|
||||||
|
const VERSION_IMAGE: u8 = 0x02;
|
||||||
|
const HEADER_LEN: usize = 6;
|
||||||
|
const CHANNELS: usize = 3;
|
||||||
|
const MAX_DIMENSION: u32 = 16384;
|
||||||
|
|
||||||
|
fn header(payload_len: u32) -> [u8; HEADER_LEN] {
|
||||||
|
let n = payload_len.to_be_bytes();
|
||||||
|
[MAGIC, VERSION_IMAGE, n[0], n[1], n[2], n[3]]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(plain_png: &[u8], key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let cipher = cipher_from(key)?;
|
||||||
|
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||||
|
let payload_len = NONCE_LEN + plain_png.len() + TAG_LEN;
|
||||||
|
if u32::try_from(payload_len).is_err() {
|
||||||
|
return Err(CryptoError::Malformed);
|
||||||
|
}
|
||||||
|
let head = header(payload_len as u32);
|
||||||
|
|
||||||
|
let sealed = cipher
|
||||||
|
.encrypt(
|
||||||
|
&nonce,
|
||||||
|
Payload {
|
||||||
|
msg: plain_png,
|
||||||
|
aad: &head,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
|
||||||
|
let mut blob = Vec::with_capacity(HEADER_LEN + payload_len);
|
||||||
|
blob.extend_from_slice(&head);
|
||||||
|
blob.extend_from_slice(&nonce);
|
||||||
|
blob.extend_from_slice(&sealed);
|
||||||
|
|
||||||
|
to_noise_png(&blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(noise_png: &[u8], key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let blob = from_noise_png(noise_png)?;
|
||||||
|
let payload_len = envelope_len(&blob).ok_or(CryptoError::NotEncrypted)?;
|
||||||
|
let end = HEADER_LEN + payload_len;
|
||||||
|
if blob.len() < end {
|
||||||
|
return Err(CryptoError::Malformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cipher = cipher_from(key)?;
|
||||||
|
let nonce = Nonce::from_slice(&blob[HEADER_LEN..HEADER_LEN + NONCE_LEN]);
|
||||||
|
cipher
|
||||||
|
.decrypt(
|
||||||
|
nonce,
|
||||||
|
Payload {
|
||||||
|
msg: &blob[HEADER_LEN + NONCE_LEN..end],
|
||||||
|
aad: &blob[..HEADER_LEN],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| CryptoError::WrongKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn looks_encrypted(noise_png: &[u8]) -> bool {
|
||||||
|
from_noise_png(noise_png)
|
||||||
|
.ok()
|
||||||
|
.and_then(|blob| envelope_len(&blob))
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn envelope_len(blob: &[u8]) -> Option<usize> {
|
||||||
|
if blob.len() < HEADER_LEN || blob[0] != MAGIC || blob[1] != VERSION_IMAGE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let len = u32::from_be_bytes([blob[2], blob[3], blob[4], blob[5]]) as usize;
|
||||||
|
if len < NONCE_LEN + TAG_LEN || HEADER_LEN + len > blob.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_noise_png(blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let pixels = blob.len().div_ceil(CHANNELS);
|
||||||
|
let width = (pixels as f64).sqrt().ceil().max(1.0) as u32;
|
||||||
|
if width > MAX_DIMENSION {
|
||||||
|
return Err(CryptoError::Malformed);
|
||||||
|
}
|
||||||
|
let height = (pixels as u32).div_ceil(width).max(1);
|
||||||
|
|
||||||
|
let mut raw = vec![0u8; width as usize * height as usize * CHANNELS];
|
||||||
|
raw[..blob.len()].copy_from_slice(blob);
|
||||||
|
OsRng.fill_bytes(&mut raw[blob.len()..]);
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
{
|
||||||
|
let mut encoder = png::Encoder::new(&mut out, width, height);
|
||||||
|
encoder.set_color(png::ColorType::Rgb);
|
||||||
|
encoder.set_depth(png::BitDepth::Eight);
|
||||||
|
encoder.set_compression(png::Compression::Fast);
|
||||||
|
let mut writer = encoder
|
||||||
|
.write_header()
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
writer
|
||||||
|
.write_image_data(&raw)
|
||||||
|
.map_err(|_| CryptoError::Internal)?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_noise_png(noise_png: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let decoder = png::Decoder::new(noise_png);
|
||||||
|
let mut reader = decoder.read_info().map_err(|_| CryptoError::NotEncrypted)?;
|
||||||
|
let info = reader.info();
|
||||||
|
if info.color_type != png::ColorType::Rgb || info.bit_depth != png::BitDepth::Eight {
|
||||||
|
return Err(CryptoError::NotEncrypted);
|
||||||
|
}
|
||||||
|
let mut raw = vec![0u8; reader.output_buffer_size()];
|
||||||
|
let frame = reader
|
||||||
|
.next_frame(&mut raw)
|
||||||
|
.map_err(|_| CryptoError::Malformed)?;
|
||||||
|
raw.truncate(frame.buffer_size());
|
||||||
|
Ok(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const KEY: [u8; 32] = [7u8; 32];
|
||||||
|
const OTHER_KEY: [u8; 32] = [8u8; 32];
|
||||||
|
|
||||||
|
fn sample_png(width: u32, height: u32) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
{
|
||||||
|
let mut encoder = png::Encoder::new(&mut out, width, height);
|
||||||
|
encoder.set_color(png::ColorType::Rgb);
|
||||||
|
encoder.set_depth(png::BitDepth::Eight);
|
||||||
|
let mut writer = encoder.write_header().unwrap();
|
||||||
|
let raw: Vec<u8> = (0..width as usize * height as usize * 3)
|
||||||
|
.map(|i| (i * 7 % 251) as u8)
|
||||||
|
.collect();
|
||||||
|
writer.write_image_data(&raw).unwrap();
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_returns_original_bytes() {
|
||||||
|
for (w, h) in [(1, 1), (16, 9), (64, 64), (200, 137)] {
|
||||||
|
let original = sample_png(w, h);
|
||||||
|
let encrypted = encrypt(&original, &KEY).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &KEY).unwrap(), original, "{w}x{h}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_is_a_valid_rgb_png() {
|
||||||
|
let encrypted = encrypt(&sample_png(32, 32), &KEY).unwrap();
|
||||||
|
assert_eq!(&encrypted[1..4], b"PNG");
|
||||||
|
let decoder = png::Decoder::new(encrypted.as_slice());
|
||||||
|
let reader = decoder.read_info().unwrap();
|
||||||
|
let info = reader.info();
|
||||||
|
assert_eq!(info.color_type, png::ColorType::Rgb);
|
||||||
|
assert_eq!(info.bit_depth, png::BitDepth::Eight);
|
||||||
|
assert!(info.width >= 1 && info.height >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_input_produces_different_output() {
|
||||||
|
let original = sample_png(16, 16);
|
||||||
|
let a = encrypt(&original, &KEY).unwrap();
|
||||||
|
let b = encrypt(&original, &KEY).unwrap();
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_key_is_rejected() {
|
||||||
|
let encrypted = encrypt(&sample_png(16, 16), &KEY).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &OTHER_KEY), Err(CryptoError::WrongKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tampered_pixels_are_rejected() {
|
||||||
|
let mut encrypted = encrypt(&sample_png(16, 16), &KEY).unwrap();
|
||||||
|
let raw = from_noise_png(&encrypted).unwrap();
|
||||||
|
let mut tampered = raw.clone();
|
||||||
|
tampered[HEADER_LEN + NONCE_LEN + 4] ^= 0x01;
|
||||||
|
encrypted = to_noise_png(&tampered).unwrap();
|
||||||
|
assert_eq!(decrypt(&encrypted, &KEY), Err(CryptoError::WrongKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_png_is_not_mistaken_for_ciphertext() {
|
||||||
|
let plain = sample_png(24, 24);
|
||||||
|
assert!(!looks_encrypted(&plain));
|
||||||
|
assert_eq!(decrypt(&plain, &KEY), Err(CryptoError::NotEncrypted));
|
||||||
|
assert!(looks_encrypted(&encrypt(&plain, &KEY).unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn garbage_input_is_rejected() {
|
||||||
|
assert!(!looks_encrypted(b"not a png at all"));
|
||||||
|
assert_eq!(
|
||||||
|
decrypt(b"not a png at all", &KEY),
|
||||||
|
Err(CryptoError::NotEncrypted)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_key_length_is_reported() {
|
||||||
|
assert_eq!(
|
||||||
|
encrypt(&sample_png(8, 8), &[0u8; 8]),
|
||||||
|
Err(CryptoError::BadKeyLength)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod alphabet;
|
||||||
|
pub mod api;
|
||||||
|
pub mod cipher;
|
||||||
|
pub mod error;
|
||||||
|
pub mod image;
|
||||||
|
|
||||||
|
mod frb_generated;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
flutter/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# The Flutter tooling requires that developers have a version of Visual Studio
|
||||||
|
# installed that includes CMake 3.14 or later. You should not increase this
|
||||||
|
# version, as doing so will cause the plugin to fail to compile for some
|
||||||
|
# customers of the plugin.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
# Project-level configuration.
|
||||||
|
set(PROJECT_NAME "komet_crypto")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
include("../cargokit/cmake/cargokit.cmake")
|
||||||
|
apply_cargokit(${PROJECT_NAME} ../rust komet_crypto "")
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||||
|
# This list could contain prebuilt libraries, or libraries created by an
|
||||||
|
# external build triggered from this build file.
|
||||||
|
set(komet_crypto_bundled_libraries
|
||||||
|
"${${PROJECT_NAME}_cargokit_lib}"
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
+8
-1
@@ -508,7 +508,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.35"
|
version: "2.0.35"
|
||||||
flutter_rust_bridge:
|
flutter_rust_bridge:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_rust_bridge
|
name: flutter_rust_bridge
|
||||||
sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a
|
sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a
|
||||||
@@ -764,6 +764,13 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
komet_crypto:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "native/komet_crypto"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.1.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ dependencies:
|
|||||||
kolibri:
|
kolibri:
|
||||||
path: third_party/kolibri/kolibri-dart
|
path: third_party/kolibri/kolibri-dart
|
||||||
|
|
||||||
|
# Rust message-encryption core — Argon2id + ChaCha20-Poly1305, output encoded
|
||||||
|
# as lowercase Cyrillic base32. Separate from kolibri: that is vendored
|
||||||
|
# transport, this is Komet's own crypto.
|
||||||
|
komet_crypto:
|
||||||
|
path: native/komet_crypto
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
crypto: ^3.0.7
|
crypto: ^3.0.7
|
||||||
@@ -94,6 +100,10 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# Needed by test/chat_crypto_roundtrip_test.dart to open the built
|
||||||
|
# komet_crypto shared library directly.
|
||||||
|
flutter_rust_bridge: 2.12.0
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:komet_crypto/komet_crypto.dart' as kc;
|
||||||
|
|
||||||
|
const _libPath = 'build/linux/x64/debug/bundle/lib/libkomet_crypto.so';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (!File(_libPath).existsSync()) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('skipping: run `flutter build linux --debug` first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
await kc.RustLib.init(
|
||||||
|
externalLibrary: ExternalLibrary.open(_libPath),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('round-trips through the native bridge', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'общий ключ');
|
||||||
|
expect(key.length, 32);
|
||||||
|
|
||||||
|
const plaintext = 'встречаемся в 19:00 у метро';
|
||||||
|
final encrypted = await kc.encryptMessage(plaintext: plaintext, key: key);
|
||||||
|
|
||||||
|
expect(encrypted, isNot(contains(RegExp(r'[a-zA-Z0-9]'))));
|
||||||
|
expect(encrypted, contains(' '));
|
||||||
|
expect(await kc.decryptMessage(text: encrypted, key: key), plaintext);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('derives the same key from the same password', () async {
|
||||||
|
final a = await kc.deriveKey(password: 'один ключ');
|
||||||
|
final b = await kc.deriveKey(password: 'один ключ');
|
||||||
|
expect(a, b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a wrong key', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'правильный');
|
||||||
|
final wrong = await kc.deriveKey(password: 'неправильный');
|
||||||
|
final encrypted = await kc.encryptMessage(plaintext: 'секрет', key: key);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => kc.decryptMessage(text: encrypted, key: wrong),
|
||||||
|
throwsA(predicate((e) => e.toString().contains('wrong_key'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports plain text as not encrypted', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'ключ');
|
||||||
|
expect(await kc.looksEncrypted(text: 'привет как дела'), isFalse);
|
||||||
|
expect(
|
||||||
|
() => kc.decryptMessage(text: 'привет как дела', key: key),
|
||||||
|
throwsA(predicate((e) => e.toString().contains('not_encrypted'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('images', _imageTests);
|
||||||
|
|
||||||
|
test('survives whitespace mangling', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'ключ');
|
||||||
|
final encrypted = await kc.encryptMessage(
|
||||||
|
plaintext: 'пробелы декоративные',
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
final mangled = ' ${encrypted.replaceAll(' ', ' ')}\n';
|
||||||
|
expect(
|
||||||
|
await kc.decryptMessage(text: mangled, key: key),
|
||||||
|
'пробелы декоративные',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<int> _tinyPng = [
|
||||||
|
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 4, 0,
|
||||||
|
0, 0, 4, 8, 2, 0, 0, 0, 38, 147, 9, 41, 0, 0, 0, 63, 73, 68, 65, 84, 120,
|
||||||
|
156, 1, 52, 0, 203, 255, 0, 0, 40, 80, 120, 160, 200, 240, 24, 64, 104, 144,
|
||||||
|
184, 0, 17, 57, 97, 137, 177, 217, 1, 41, 81, 121, 161, 201, 0, 34, 74, 114,
|
||||||
|
154, 194, 234, 18, 58, 98, 138, 178, 218, 0, 51, 91, 131, 171, 211, 251, 35,
|
||||||
|
75, 115, 155, 195, 235, 36, 246, 23, 9, 123, 15, 58, 142, 0, 0, 0, 0, 73, 69,
|
||||||
|
78, 68, 174, 66, 96, 130,
|
||||||
|
];
|
||||||
|
|
||||||
|
void _imageTests() {
|
||||||
|
late Directory tmp;
|
||||||
|
|
||||||
|
setUp(() => tmp = Directory.systemTemp.createTempSync('komet_img'));
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
File writePlain() =>
|
||||||
|
File('${tmp.path}/plain.png')..writeAsBytesSync(_tinyPng);
|
||||||
|
|
||||||
|
test('round-trips a photo through the native bridge', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'фото-ключ');
|
||||||
|
final plain = writePlain();
|
||||||
|
final enc = '${tmp.path}/enc.png';
|
||||||
|
final out = '${tmp.path}/out.png';
|
||||||
|
|
||||||
|
await kc.encryptImageFile(
|
||||||
|
sourcePath: plain.path,
|
||||||
|
destPath: enc,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
|
||||||
|
final encBytes = File(enc).readAsBytesSync();
|
||||||
|
expect(encBytes.sublist(1, 4), 'PNG'.codeUnits);
|
||||||
|
expect(encBytes, isNot(_tinyPng));
|
||||||
|
expect(await kc.looksEncryptedImageFile(path: enc), isTrue);
|
||||||
|
expect(await kc.looksEncryptedImageFile(path: plain.path), isFalse);
|
||||||
|
|
||||||
|
await kc.decryptImageFile(sourcePath: enc, destPath: out, key: key);
|
||||||
|
expect(File(out).readAsBytesSync(), _tinyPng);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a photo decrypted with a wrong key', () async {
|
||||||
|
final key = await kc.deriveKey(password: 'правильный');
|
||||||
|
final wrong = await kc.deriveKey(password: 'неправильный');
|
||||||
|
final enc = '${tmp.path}/enc.png';
|
||||||
|
|
||||||
|
await kc.encryptImageFile(
|
||||||
|
sourcePath: writePlain().path,
|
||||||
|
destPath: enc,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() => kc.decryptImageFile(
|
||||||
|
sourcePath: enc,
|
||||||
|
destPath: '${tmp.path}/out.png',
|
||||||
|
key: wrong,
|
||||||
|
),
|
||||||
|
throwsA(predicate((e) => e.toString().contains('wrong_key'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user