import 'dart:convert'; import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:charset/charset.dart'; import 'package:crypto/crypto.dart'; import 'package:intl/locale.dart'; import 'package:pkpass/pkpass.dart'; import 'package:pkpass/pkpass/utils/file_matcher.dart'; import 'package:pkpass/pkpass/utils/lproj_parser.dart'; class PassFile { const PassFile(this.metadata, this._folder); static Future parse(Uint8List pass) async { final codec = ZipDecoder(); Archive archive; try { archive = codec.decodeBytes(pass); } catch (e) { throw InvalidEncodingError(); } Map manifest; final file = archive.files.singleWhere((element) => element.name == 'manifest.json'); manifest = (json.decode(file.stringContent) as Map).cast(); final folder = []; await Future.wait( manifest.entries.map( (manifestEntry) async { final file = archive.files .singleWhere((element) => element.name == manifestEntry.key); final content = file.readBytes(); if (content == null) { return; } String hash = sha1.convert(content).toString(); final checksum = manifestEntry.value; if (hash != checksum) { throw ManifestChecksumError(expected: checksum, actual: hash); } folder.add(file); }, ), ); final passFile = archive.singleWhere((element) => element.name == 'pass.json'); final PassMetadata metadata = PassMetadata.fromJson( json.decode(passFile.stringContent) as Map, ); return PassFile(metadata, folder); } final PassMetadata metadata; final List _folder; Uint8List? _matchUint8ListFile({ required String name, required Locale? locale, required int scale, }) { final files = _folder.map((e) => e.name).toList(); final path = FileMatcher.matchFile( files: files, name: name, scale: scale, locale: locale, extension: 'png', ); if (path == null) return null; final file = _folder.singleWhere((element) => element.name == path); final content = file.readBytes(); return content; } Uint8List? getBackground({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'background', locale: locale, scale: scale); Uint8List? getFooter({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'footer', locale: locale, scale: scale); Uint8List? getIcon({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'icon', locale: locale, scale: scale); Uint8List? getLogo({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'logo', locale: locale, scale: scale); Uint8List? getStrip({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'strip', locale: locale, scale: scale); Uint8List? getThumbnail({Locale? locale, int scale = 1}) => _matchUint8ListFile(name: 'thumbnail', locale: locale, scale: scale); Map? getLocalizations(Locale? locale) { final files = _folder.map((e) => e.name).toList(); final paths = FileMatcher.matchLocale( files: files, name: 'pass', extension: 'strings', locale: locale, ); if (paths.isEmpty) return null; final file = _folder.singleWhere((element) => element.name == paths.first); return LProjParser.parse(file.stringContent); } } extension on ArchiveFile { String get stringContent { final codec = Charset.detect( readBytes()!, defaultEncoding: utf8, orders: [ utf8, ascii, gbk, latin1, utf16, ], ) ?? utf8; try { return codec.decode(content); } on FormatException { // utf8 and utf16 are hard to distinguish if (codec is Utf8Codec) { return utf16.decode(content); } rethrow; } } }