Files
IKAR/lib/core/plugins/plugin_updater.dart

158 lines
5.4 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import '../../l10n/app_localizations.dart';
import 'plugin_installer.dart';
import 'plugin_models.dart';
import 'plugin_permission_label.dart';
import 'plugin_store.dart';
class PluginUpdater {
PluginUpdater({
PluginInstaller installer = const PluginInstaller(),
PluginStore? store,
}) : _installer = installer,
_store = store ?? PluginStore.instance;
final PluginInstaller _installer;
final PluginStore _store;
Future<PluginUpdateInfo?> check(PluginDescriptor plugin) async {
final manifestUrl = plugin.manifest.updateUrl;
if (manifestUrl == null) return null;
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 15);
try {
final request = await client.getUrl(manifestUrl);
request.headers.set(HttpHeaders.userAgentHeader, 'QlyraPluginUpdater/1');
final response = await request.close().timeout(
const Duration(seconds: 15),
);
if (response.statusCode != HttpStatus.ok) {
await response.drain<void>();
throw HttpException('HTTP ${response.statusCode}', uri: manifestUrl);
}
final decoded = jsonDecode(
await response.transform(const Utf8Decoder()).join(),
);
if (decoded is! Map) {
throw const FormatException('Некорректный update manifest');
}
final version = decoded['version'];
final packageUrl = decoded['packageUrl'];
final size = decoded['size'];
final digest = decoded['sha256'];
if (version is! String ||
packageUrl is! String ||
size is! int ||
digest is! String) {
throw const FormatException('Некорректный update manifest');
}
if (_compareVersions(version, plugin.manifest.version) <= 0) return null;
final uri = manifestUrl.resolve(packageUrl);
if (uri.scheme != 'https') {
throw const FormatException('packageUrl должен использовать HTTPS');
}
return PluginUpdateInfo(
plugin: plugin,
version: version,
packageUrl: uri,
size: size,
sha256: digest.toLowerCase(),
);
} finally {
client.close(force: true);
}
}
Future<PluginDescriptor> apply(
PluginUpdateInfo update,
AppLocalizations l10n,
) async {
final preview = await _installer.download(update.packageUrl);
if (preview.manifest.id != update.plugin.manifest.id ||
preview.manifest.version != update.version) {
throw const FormatException('Обновление принадлежит другому плагину');
}
if (update.size > 0 && preview.bytes.length != update.size) {
throw const FormatException('Размер обновления не совпадает');
}
if (update.sha256.isNotEmpty &&
sha256.convert(preview.bytes).toString() != update.sha256) {
throw const FormatException('SHA-256 обновления не совпадает');
}
final missing = preview.manifest.permissions.difference(
update.plugin.grantedPermissions,
);
if (missing.isNotEmpty) {
throw FormatException(
l10n.pluginUpdateNewPermissions(
missing.map((item) => item.label(l10n)).join(', '),
),
);
}
return _store.install(
preview.bytes,
grantedPermissions: update.plugin.grantedPermissions.intersection(
preview.manifest.permissions,
),
sourceUrl: _store.sourceUrl(update.plugin.manifest.id),
);
}
}
int _compareVersions(String a, String b) {
final left = _ParsedVersion.parse(a);
final right = _ParsedVersion.parse(b);
for (var i = 0; i < 3; i++) {
final comparison = left.core[i].compareTo(right.core[i]);
if (comparison != 0) return comparison;
}
if (left.preRelease.isEmpty && right.preRelease.isNotEmpty) return 1;
if (left.preRelease.isNotEmpty && right.preRelease.isEmpty) return -1;
final length = left.preRelease.length > right.preRelease.length
? left.preRelease.length
: right.preRelease.length;
for (var i = 0; i < length; i++) {
if (i >= left.preRelease.length) return -1;
if (i >= right.preRelease.length) return 1;
final leftPart = left.preRelease[i];
final rightPart = right.preRelease[i];
final leftNumber = int.tryParse(leftPart);
final rightNumber = int.tryParse(rightPart);
if (leftNumber != null && rightNumber != null) {
final comparison = leftNumber.compareTo(rightNumber);
if (comparison != 0) return comparison;
continue;
}
if (leftNumber != null) return -1;
if (rightNumber != null) return 1;
final comparison = leftPart.compareTo(rightPart);
if (comparison != 0) return comparison;
}
return 0;
}
class _ParsedVersion {
const _ParsedVersion(this.core, this.preRelease);
final List<int> core;
final List<String> preRelease;
factory _ParsedVersion.parse(String value) {
final withoutBuild = value.split('+').first;
final separator = withoutBuild.indexOf('-');
final coreSource = separator == -1
? withoutBuild
: withoutBuild.substring(0, separator);
final preRelease = separator == -1
? const <String>[]
: withoutBuild.substring(separator + 1).split('.');
final core = coreSource.split('.').map(int.parse).toList();
if (core.length != 3) throw const FormatException('Некорректная версия');
return _ParsedVersion(core, preRelease);
}
}