Files
Obtainium/lib/custom_errors.dart
Imran Remtulla c911cba670 fix: resolve 20 bugs, safety issues, and code quality problems
Critical fixes:
- Remove setTrustedCertificatesBytes which replaced the entire TLS trust store
- Fix late final async init race causing LateInitializationError crashes
- Fix infinite recursion in ObtainiumError.message for MULTI_ERROR code

High fixes:
- Fix identical() always returning false on badge icon (update count never shown)
- Clone form items before mutation to prevent cross-contamination between sources
- Log transient errors before removing apps on load failure
- Await uninstallApp calls instead of fire-and-forget
- Revert optimistic install on background workaround failure
- Log SAF errors before silently disabling export directory

Medium fixes:
- Remove dead GeneratedForm.fromDefinitions constructor and unused fields
- Replace Shizuku-specific param with generic installOptions map in Installer interface
- Add importable-keys allowlist to prevent arbitrary SharedPreferences writes
- Guard against stale Shizuku permission check callbacks with sequence counter
- Prevent ReceivePort leak in listenForDownloadCancelFromMain
- Add 500MB tarball size guard to prevent OOM on large archives
- Simplify ReDoS-vulnerable URL regex in html.dart
- Fix TOCTOU race between existsSync and length calls
- Log rename failures instead of empty catch blocks
- Use safe null-aware defaults in setFormValuesFromMap
2026-07-03 13:44:04 +01:00

264 lines
7.5 KiB
Dart

import 'dart:async';
import 'dart:ui' show Locale;
import 'package:easy_localization/easy_localization.dart';
import 'package:android_package_installer/android_package_installer.dart';
import 'package:obtainium/providers/logs_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
class ObtainiumError {
final String code;
final String _message;
final StackTrace? stack;
final Map<String, dynamic> data;
final bool unexpected;
ObtainiumError(
this._message, {
this.code = 'UNKNOWN',
this.unexpected = false,
this.stack,
this.data = const {},
});
ObtainiumError.withCode(
this.code, {
String message = '',
this.unexpected = false,
this.stack,
this.data = const {},
}) : _message = message;
String get message =>
code == 'UNKNOWN' ||
code == 'UNEXPECTED' ||
code == 'CHECK_UPDATES_FAILED' ||
code == 'HTTP_ERROR'
? _message
: code == 'MULTI_ERROR'
? localizeErrorCode(code, data)
: localizeErrorCode(code, data);
@override
String toString() => message;
}
Never rethrowOrWrapError(Object error, {String? sourceName, StackTrace? stack}) {
if (error is ObtainiumError) {
if (error.unexpected) {
final resolvedStack = error.stack ?? StackTrace.current;
unawaited(
LogsProvider().add(
'Unexpected ObtainiumError: ${error.message}\n$resolvedStack',
level: LogLevel.error,
),
);
throw ObtainiumError(
error.message,
code: 'UNEXPECTED',
unexpected: true,
stack: resolvedStack,
data: error.data,
);
}
throw error;
}
final capturedStack = stack ?? StackTrace.current;
unawaited(
LogsProvider().add(
'Wrapping unexpected error: $error\n$capturedStack',
level: LogLevel.error,
),
);
throw ObtainiumError(
sourceName != null ? '$sourceName: $error' : error.toString(),
code: 'UNEXPECTED',
unexpected: true,
stack: capturedStack,
);
}
class RateLimitError extends ObtainiumError {
final int remainingMinutes;
RateLimitError(this.remainingMinutes)
: super.withCode(
'RATE_LIMIT',
data: {'remainingMinutes': remainingMinutes},
);
}
class InvalidURLError extends ObtainiumError {
InvalidURLError(String sourceName)
: super.withCode('INVALID_URL', data: {'sourceName': sourceName});
}
class CredsNeededError extends ObtainiumError {
CredsNeededError(String sourceName)
: super.withCode('CREDS_NEEDED', data: {'sourceName': sourceName});
}
class NoReleasesError extends ObtainiumError {
NoReleasesError({String? note})
: super.withCode('NO_RELEASES', data: {'note': note ?? ''});
}
class NoAPKError extends ObtainiumError {
NoAPKError() : super.withCode('NO_APK');
}
class NoVersionError extends ObtainiumError {
NoVersionError() : super.withCode('NO_VERSION');
}
class UnsupportedURLError extends ObtainiumError {
UnsupportedURLError() : super.withCode('UNSUPPORTED_URL');
}
class DowngradeError extends ObtainiumError {
DowngradeError(int currentVersionCode, int newVersionCode)
: super.withCode(
'DOWNGRADE',
data: {
'currentVersionCode': currentVersionCode,
'newVersionCode': newVersionCode,
},
);
}
class InstallError extends ObtainiumError {
InstallError(int code)
: super.withCode(
'INSTALL_FAILED',
data: {
'errorCode': code,
'message': PackageInstallerStatus.byCode(code).name,
},
);
}
class IDChangedError extends ObtainiumError {
IDChangedError(String newId)
: super.withCode('ID_CHANGED', data: {'newId': newId});
}
class RepositoryRenamedError extends ObtainiumError {
final String oldUrl;
final String newUrl;
RepositoryRenamedError(this.oldUrl, this.newUrl)
: super.withCode(
'REPO_RENAMED',
data: {'oldUrl': oldUrl, 'newUrl': newUrl},
);
}
class CheckUpdatesException extends ObtainiumError {
final List<App> updates;
final MultiAppMultiError errors;
CheckUpdatesException(this.updates, this.errors)
: super.withCode('CHECK_UPDATES_FAILED', unexpected: true);
@override
String toString() => errors.toString();
}
class NotImplementedError extends ObtainiumError {
NotImplementedError() : super.withCode('NOT_IMPLEMENTED');
}
class MultiAppMultiError extends ObtainiumError {
Map<String, dynamic> rawErrors = {};
Map<String, List<String>> idsByErrorString = {};
Map<String, String> appIdNames = {};
MultiAppMultiError() : super.withCode('MULTI_ERROR', unexpected: true);
void add(String appId, dynamic error, {String? appName}) {
rawErrors[appId] = error;
final string = error.toString();
var tempIds = idsByErrorString.remove(string);
if (tempIds == null) {
tempIds = [];
idsByErrorString[string] = tempIds;
}
tempIds.add(appId);
if (appName != null) {
appIdNames[appId] = appName;
}
}
String errorString(String appId, {bool includeIdsWithNames = false}) =>
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
String errorsAppsString(
String errString,
List<String> appIds, {
bool includeIdsWithNames = false,
}) =>
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
@override
String toString() => idsByErrorString.entries
.map((e) => errorsAppsString(e.key, e.value))
.join('\n\n');
}
String localizeErrorCode(String code, Map<String, dynamic>? data) {
return switch (code) {
'NO_RELEASES' =>
data?['note'] != null && (data!['note'] as String).isNotEmpty
? '${tr('noReleaseFound')}\n\n${data['note']}'
: tr('noReleaseFound'),
'RATE_LIMIT' => plural(
'tooManyRequestsTryAgainInMinutes',
data?['remainingMinutes'] ?? 0,
),
'INVALID_URL' => tr(
'invalidURLForSource',
args: [data?['sourceName'] ?? ''],
),
'CREDS_NEEDED' => tr(
'requiresCredentialsInSettings',
args: [data?['sourceName'] ?? ''],
),
'NO_APK' => tr('noAPKFound'),
'NO_VERSION' => tr('noVersionFound'),
'UNSUPPORTED_URL' => tr('urlMatchesNoSource'),
'DOWNGRADE' =>
'${tr('cantInstallOlderVersion')} (versionCode ${data?['currentVersionCode'] ?? '?'}${data?['newVersionCode'] ?? '?'})',
'INSTALL_FAILED' => data?['message']?.toString() ?? tr('installFailed'),
'ID_CHANGED' => '${tr('appIdMismatch')} - ${data?['newId'] ?? ''}',
'REPO_RENAMED' => tr('repoRenamed'),
'NOT_IMPLEMENTED' => tr('functionNotImplemented'),
_ => data?['message']?.toString() ?? tr('unexpectedError'),
};
}
Locale? _appCurrentLocale;
void setAppLocale(Locale? locale) => _appCurrentLocale = locale;
bool isEnglish() {
if (_appCurrentLocale != null) return _appCurrentLocale!.languageCode == 'en';
return false;
}
String lowerCaseIfEnglish(String str) => isEnglish() ? str.toLowerCase() : str;
String list2FriendlyString(List<String> list) {
final isUsingEnglish = isEnglish();
return list.length == 2
? '${list[0]} ${tr('and')} ${list[1]}'
: list
.asMap()
.entries
.map(
(e) =>
e.value +
(e.key == list.length - 1
? ''
: e.key == list.length - 2
? '${isUsingEnglish ? ',' : ''} ${tr('and')} '
: ', '),
)
.join('');
}