| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:fxtest/fxtest.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as p; |
| |
| const _defaultManifest = 'package-repositories.json'; |
| |
| /// Contains information about a Fuchsia TUF package repository. |
| /// Meta information, like file system location, is collected from the file |
| /// 'package-repositories.json' produced by the Fuchsia build, while the |
| /// mapping of Fuchsia package URLs to their Merkle root hashes is collected |
| /// from a 'targets.json' file linked by the 'targets' property of |
| /// 'package-repositories.json'. |
| /// The 'targets.json' file is expected to follow the syntax defined in |
| /// https://fuchsia.dev/fuchsia-src/concepts/system/software_update_system |
| /// and https://github.com/theupdateframework/specification/blob/HEAD/tuf-spec.md. |
| class PackageRepository { |
| late String targetsFile; |
| late String blobsDirectory; |
| late String rootPath; |
| |
| final Map<String, PackageInfo> _packages = {}; |
| |
| /// Parses a package-repositories.json manifest file, which points to a |
| /// [TUF](https://github.com/theupdateframework/specification) repository. |
| /// The repository targets.json file is then parsed and produces a map of |
| /// package name and package variant to their corresponding Merkle root |
| /// hashes. |
| /// Returns null if the 'package-repositories.json' manifest doesn't exist |
| /// or if the 'targets.json' file referenced in property 'targets' of |
| /// 'package-repositories.json' cannot be opened. |
| static Future<PackageRepository>? fromManifest( |
| {required String buildDir, String repositoriesFile = _defaultManifest}) { |
| // The package-repositories manifest is usually very small, so it's ok to |
| // read it all at once. |
| File file = File(p.join(buildDir, repositoriesFile)); |
| if (!file.existsSync()) { |
| return null; |
| } |
| String content = file.readAsStringSync(); |
| |
| try { |
| PackageRepository repository = |
| PackageRepository.fromJson(jsonDecode(content)); |
| File targetsFile = File(p.join(buildDir, repository.targetsFile)); |
| if (!targetsFile.existsSync()) { |
| return null; |
| } |
| |
| // The targets.json file is usually large, so using a stream instead |
| return repository |
| .loadTargetsFromJson(targetsFile |
| .openRead() |
| .transform(utf8.decoder) |
| .transform(json.decoder) |
| .cast()) |
| .then((v) => repository); |
| } on PackageRepositoryException catch (e) { |
| // Wrap the exception to include the manifest filename |
| e.file = repositoriesFile; |
| rethrow; |
| } |
| } |
| |
| // ignore: prefer_constructors_over_static_methods |
| static PackageUrl? decoratePackageUrlWithHash( |
| PackageRepository? repository, String? packageUrl) { |
| if (packageUrl == null) { |
| return null; |
| } |
| PackageUrl parsed = PackageUrl.fromString(packageUrl); |
| if (repository == null) { |
| return parsed; |
| } |
| PackageInfo? info = repository[parsed.packageName]; |
| if (info == null) { |
| return parsed; |
| } |
| var hash = parsed.packageVariant == null |
| ? info.merkle |
| : info[parsed.packageVariant!]; |
| return PackageUrl.copyWithHash(other: parsed, hash: hash); |
| } |
| |
| @visibleForTesting |
| PackageRepository(this.targetsFile, this.blobsDirectory, this.rootPath); |
| |
| /// Constructs a [PackageRepository] from the contents of a |
| /// package-repositories.json manifest file. |
| @visibleForTesting |
| PackageRepository.fromJson(List<dynamic> manifest) { |
| if (manifest.length > 1) { |
| throw PackageRepositoryParseException( |
| 'Multiple repositories are not supported'); |
| } |
| if (manifest.isEmpty) { |
| throw PackageRepositoryParseException('No repository found in manifest'); |
| } |
| Map<String, dynamic> repositoryJson = manifest[0]; |
| targetsFile = repositoryJson['targets']; |
| blobsDirectory = repositoryJson['blobs']; |
| rootPath = repositoryJson['path']; |
| } |
| |
| @visibleForTesting |
| Future loadTargetsFromJson(Stream<Map<String, dynamic>> jsonStream) { |
| return jsonStream |
| // Filters objects with signed.targets content |
| .where((jsonObj) => |
| jsonObj['signed'] != null && jsonObj['signed']['targets'] != null) |
| // Expands each signed.targets map into its entries |
| .expand((jsonObj) => jsonObj['signed']['targets'].entries) |
| // Merges target entries to _packages |
| // ignore: unnecessary_lambdas |
| .forEach((targetEntry) => _mergeTarget(targetEntry)); |
| } |
| |
| void _mergeTarget(MapEntry<String, dynamic> targetEntry) { |
| var split = targetEntry.key.split('/'); |
| var name = split[0]; |
| var variant = split[1]; |
| _packages[name] = |
| PackageInfo.fromJson(name, variant, targetEntry.value, _packages[name]); |
| } |
| |
| Map<String, PackageInfo> asMap() => UnmodifiableMapView(_packages); |
| |
| PackageInfo? operator [](String packageName) => _packages[packageName]; |
| } |
| |
| class PackageInfo { |
| final String packageName; |
| |
| /// Map package variant to its Merkle root hash |
| final Map<String, String> _merkle = {}; |
| |
| PackageInfo._internal(this.packageName); |
| |
| factory PackageInfo.fromJson( |
| String name, String variant, Map<String, dynamic> json, |
| [PackageInfo? current]) { |
| var packageInfo = current ?? PackageInfo._internal(name); |
| |
| if (packageInfo._merkle.containsKey(variant)) { |
| throw PackageRepositoryParseException( |
| 'Duplicated variant $variant for package $name in' |
| 'package repository targets file'); |
| } |
| packageInfo._merkle[variant] = json['custom']['merkle']; |
| return packageInfo; |
| } |
| |
| /// Merkle root hash of the package for a given variant. |
| String? operator [](String? variant) { |
| return variant == null ? merkle : _merkle[variant]; |
| } |
| |
| /// Merkle root hash of the package. |
| /// Throws [PackageRepositoryException] if package has more than one variant. |
| String? get merkle { |
| if (_merkle.length > 1) { |
| throw PackageRepositoryException( |
| 'Package $packageName has more than one variant, please specify one'); |
| } |
| return _merkle.isEmpty ? null : _merkle.values.first; |
| } |
| |
| @override |
| String toString() { |
| return 'PackageInfo $packageName: $_merkle'; |
| } |
| } |