yubioath-flutter/lib/oath/icon_provider/icon_pack_manager.dart
2023-11-27 11:41:05 +01:00

226 lines
7.3 KiB
Dart

/*
* 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';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:io/io.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import '../../app/logging.dart';
import 'icon_cache.dart';
import 'icon_pack.dart';
final _log = Logger('icon_pack_manager');
class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
final IconCache _iconCache;
String? _lastError;
final _packSubDir = 'issuer_icons';
IconPackManager(this._iconCache) : super(const AsyncValue.data(null)) {
readPack();
}
String? get lastError => _lastError;
void readPack() async {
final packDirectory = await _packDirectory;
final packFile =
File(join(packDirectory.path, getLocalIconFileName('pack.json')));
_log.debug('Looking for file: ${packFile.path}');
if (!await packFile.exists()) {
_log.debug('Failed to find icons pack ${packFile.path}');
state = AsyncValue.error(
'Failed to find icon pack ${packFile.path}', StackTrace.current);
return;
}
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;
}
}
Future<bool> importPack(AppLocalizations l10n, String filePath) async {
// remove existing pack first
await removePack();
final packFile = File(filePath);
state = const AsyncValue.loading();
if (!await packFile.exists()) {
_log.error('Input file does not exist');
_lastError = l10n.l_file_not_found;
state = AsyncValue.error('Input file does not exist', StackTrace.current);
return false;
}
if (await packFile.length() > 5 * 1024 * 1024) {
_log.error('File size too big.');
_lastError = l10n.l_file_too_big;
state = AsyncValue.error('File size too big', StackTrace.current);
return false;
}
// copy input file to temporary folder
final tempDirectory = await Directory.systemTemp.createTemp('yubioath');
final tempCopy =
await packFile.copy(join(tempDirectory.path, basename(packFile.path)));
final bytes = await File(tempCopy.path).readAsBytes();
final unpackDirectory = Directory(join(tempDirectory.path, 'unpack'));
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;
}
for (final file in archive) {
final filename = file.name;
if (file.size > 0) {
final data = file.content as List<int>;
final extractedFile =
File(join(unpackDirectory.path, getLocalIconFileName(filename)));
_log.debug('Writing file: ${extractedFile.path} (size: ${file.size})');
final createdFile = await extractedFile.create(recursive: true);
await createdFile.writeAsBytes(data);
}
}
// check that there is pack.json
final packJsonFile =
File(join(unpackDirectory.path, getLocalIconFileName('pack.json')));
if (!await packJsonFile.exists()) {
_log.error('File is not an icon pack: missing pack.json');
_lastError = l10n.l_invalid_icon_pack;
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
await _deleteDirectory(tempDirectory);
return false;
}
// test pack.json
try {
var packContent = await packJsonFile.readAsString();
const JsonDecoder().convert(packContent);
} catch (e) {
_log.error('Failed to parse pack.json: $e');
_lastError = l10n.l_invalid_icon_pack;
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
await _deleteDirectory(tempDirectory);
return false;
}
// remove old icon pack and icon pack cache
final packDirectory = await _packDirectory;
if (!await _deleteDirectory(packDirectory)) {
_log.error('Failure when deleting original pack directory');
_lastError = l10n.l_filesystem_error;
state = AsyncValue.error(
'Failure deleting original pack directory', StackTrace.current);
await _deleteDirectory(tempDirectory);
return false;
}
await _iconCache.fsCache.clear();
_iconCache.memCache.clear();
// 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);
return false;
}
readPack();
await _deleteDirectory(tempDirectory);
return true;
}
/// removes imported icon pack
Future<bool> removePack() async {
_iconCache.memCache.clear();
await _iconCache.fsCache.clear();
final cleanupStatus = await _deleteDirectory(await _packDirectory);
state = const AsyncValue.data(null);
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();
return Directory(join(supportDirectory.path, _packSubDir));
}
}
final iconPackProvider =
StateNotifierProvider<IconPackManager, AsyncValue<IconPack?>>(
(ref) => IconPackManager(ref.watch(iconCacheProvider)));