yubioath-flutter/lib/oath/icon_provider/icon_pack_manager.dart

226 lines
7.3 KiB
Dart
Raw Normal View History

2023-02-21 13:40:38 +03:00
/*
* Copyright (C) 2023 Yubico.
*
* 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.
*/
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
2023-02-22 17:23:57 +03:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2023-02-21 13:40:38 +03:00
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:io/io.dart';
2023-02-21 13:40:38 +03:00
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import '../../app/logging.dart';
2023-02-21 13:40:38 +03:00
import 'icon_cache.dart';
2023-02-23 12:46:33 +03:00
import 'icon_pack.dart';
2023-02-21 13:40:38 +03:00
2023-02-21 15:25:59 +03:00
final _log = Logger('icon_pack_manager');
2023-02-21 13:40:38 +03:00
2023-02-23 12:46:33 +03:00
class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
2023-02-21 13:40:38 +03:00
final IconCache _iconCache;
2023-02-21 15:25:59 +03:00
String? _lastError;
2023-02-21 15:25:59 +03:00
final _packSubDir = 'issuer_icons';
2023-02-21 13:40:38 +03:00
2023-02-23 12:46:33 +03:00
IconPackManager(this._iconCache) : super(const AsyncValue.data(null)) {
readPack();
}
2023-02-21 13:40:38 +03:00
String? get lastError => _lastError;
2023-02-21 15:25:59 +03:00
void readPack() async {
final packDirectory = await _packDirectory;
2023-02-28 14:09:02 +03:00
final packFile =
File(join(packDirectory.path, getLocalIconFileName('pack.json')));
2023-02-21 13:40:38 +03:00
_log.debug('Looking for file: ${packFile.path}');
if (!await packFile.exists()) {
_log.debug('Failed to find icons pack ${packFile.path}');
2023-02-23 12:46:33 +03:00
state = AsyncValue.error(
2023-02-28 14:09:02 +03:00
'Failed to find icon pack ${packFile.path}', StackTrace.current);
2023-02-21 13:40:38 +03:00
return;
}
2023-02-28 14:09:02 +03:00
try {
var packContent = await packFile.readAsString();
Map<String, dynamic> pack = const JsonDecoder().convert(packContent);
final icons = List<IconPackIcon>.from(pack['icons'].map((icon) =>
IconPackIcon(
filename: icon['filename'],
category: icon['category'],
issuer: List<String>.from(icon['issuer'])
.map((e) => e.toUpperCase())
.toList(growable: false))));
state = AsyncValue.data(IconPack(
uuid: pack['uuid'],
name: pack['name'],
version: pack['version'],
directory: packDirectory,
icons: icons));
_log.debug(
'Parsed ${state.value?.name} with ${state.value?.icons.length} icons');
} catch (e) {
_log.debug('Failed to parse icons pack ${packFile.path}');
state = AsyncValue.error(
'Failed to parse icon pack ${packFile.path}', StackTrace.current);
return;
}
2023-02-21 13:40:38 +03:00
}
2023-02-22 17:23:57 +03:00
Future<bool> importPack(AppLocalizations l10n, String filePath) async {
2023-02-23 12:46:33 +03:00
// remove existing pack first
await removePack();
2023-02-21 13:40:38 +03:00
final packFile = File(filePath);
2023-02-23 12:46:33 +03:00
state = const AsyncValue.loading();
2023-02-21 13:40:38 +03:00
if (!await packFile.exists()) {
_log.error('Input file does not exist');
2023-03-01 11:34:13 +03:00
_lastError = l10n.l_file_not_found;
2023-02-23 12:46:33 +03:00
state = AsyncValue.error('Input file does not exist', StackTrace.current);
return false;
}
2023-02-24 11:44:53 +03:00
if (await packFile.length() > 5 * 1024 * 1024) {
2023-02-22 17:23:57 +03:00
_log.error('File size too big.');
2023-03-01 11:34:13 +03:00
_lastError = l10n.l_file_too_big;
2023-02-23 12:46:33 +03:00
state = AsyncValue.error('File size too big', StackTrace.current);
2023-02-21 13:40:38 +03:00
return false;
}
// copy input file to temporary folder
final tempDirectory = await Directory.systemTemp.createTemp('yubioath');
2023-02-23 18:38:03 +03:00
final tempCopy =
await packFile.copy(join(tempDirectory.path, basename(packFile.path)));
2023-02-21 13:40:38 +03:00
final bytes = await File(tempCopy.path).readAsBytes();
2023-02-23 18:38:03 +03:00
final unpackDirectory = Directory(join(tempDirectory.path, 'unpack'));
2023-02-21 13:40:38 +03:00
Archive archive;
try {
archive = ZipDecoder().decodeBytes(bytes, verify: true);
} on Exception catch (_) {
_log.error('File is not an icon pack: zip decoding failed');
_lastError = l10n.l_invalid_icon_pack;
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
return false;
}
2023-02-21 13:40:38 +03:00
for (final file in archive) {
final filename = file.name;
2023-02-21 16:15:40 +03:00
if (file.size > 0) {
2023-02-21 13:40:38 +03:00
final data = file.content as List<int>;
2023-02-27 19:02:30 +03:00
final extractedFile =
File(join(unpackDirectory.path, getLocalIconFileName(filename)));
2023-02-23 18:38:03 +03:00
_log.debug('Writing file: ${extractedFile.path} (size: ${file.size})');
2023-02-21 13:40:38 +03:00
final createdFile = await extractedFile.create(recursive: true);
await createdFile.writeAsBytes(data);
}
}
// check that there is pack.json
2023-02-28 14:09:02 +03:00
final packJsonFile =
File(join(unpackDirectory.path, getLocalIconFileName('pack.json')));
2023-02-21 13:40:38 +03:00
if (!await packJsonFile.exists()) {
2023-02-23 12:46:33 +03:00
_log.error('File is not an icon pack: missing pack.json');
2023-03-01 11:34:13 +03:00
_lastError = l10n.l_invalid_icon_pack;
2023-02-23 12:46:33 +03:00
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
await _deleteDirectory(tempDirectory);
2023-02-21 13:40:38 +03:00
return false;
}
2023-02-28 14:09:02 +03:00
// test pack.json
try {
var packContent = await packJsonFile.readAsString();
const JsonDecoder().convert(packContent);
} catch (e) {
_log.error('Failed to parse pack.json: $e');
2023-03-01 11:34:13 +03:00
_lastError = l10n.l_invalid_icon_pack;
2023-02-28 14:09:02 +03:00
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
await _deleteDirectory(tempDirectory);
return false;
}
2023-04-24 10:15:20 +03:00
// remove old icon pack and icon pack cache
final packDirectory = await _packDirectory;
2023-02-21 13:40:38 +03:00
if (!await _deleteDirectory(packDirectory)) {
2023-02-22 17:23:57 +03:00
_log.error('Failure when deleting original pack directory');
2023-03-01 11:34:13 +03:00
_lastError = l10n.l_filesystem_error;
2023-02-23 12:46:33 +03:00
state = AsyncValue.error(
'Failure deleting original pack directory', StackTrace.current);
2023-02-21 13:40:38 +03:00
await _deleteDirectory(tempDirectory);
return false;
}
await _iconCache.fsCache.clear();
_iconCache.memCache.clear();
2023-04-24 10:15:20 +03:00
// copy unpacked files from temporary directory to the icon pack directory
try {
await copyPath(unpackDirectory.path, packDirectory.path);
} catch (e) {
_log.error('Failed to copy icon pack files to destination: $e');
_lastError = l10n.l_icon_pack_copy_failed;
state = AsyncValue.error(
'Failed to copy icon pack files.', StackTrace.current);
2023-04-24 10:15:20 +03:00
return false;
}
2023-02-21 15:25:59 +03:00
readPack();
2023-02-21 13:40:38 +03:00
await _deleteDirectory(tempDirectory);
return true;
}
2023-02-21 15:25:59 +03:00
/// removes imported icon pack
Future<bool> removePack() async {
_iconCache.memCache.clear();
await _iconCache.fsCache.clear();
final cleanupStatus = await _deleteDirectory(await _packDirectory);
2023-02-23 12:46:33 +03:00
state = const AsyncValue.data(null);
2023-02-21 15:25:59 +03:00
return cleanupStatus;
}
Future<bool> _deleteDirectory(Directory directory) async {
if (await directory.exists()) {
await directory.delete(recursive: true);
}
if (await directory.exists()) {
_log.error('Failed to delete directory');
return false;
}
return true;
}
Future<Directory> get _packDirectory async {
final supportDirectory = await getApplicationSupportDirectory();
2023-02-23 18:38:03 +03:00
return Directory(join(supportDirectory.path, _packSubDir));
2023-02-21 15:25:59 +03:00
}
2023-02-21 13:40:38 +03:00
}
2023-02-23 12:46:33 +03:00
final iconPackProvider =
StateNotifierProvider<IconPackManager, AsyncValue<IconPack?>>(
(ref) => IconPackManager(ref.watch(iconCacheProvider)));