Handle malformatted content-hashes in cache, version listing or pubspec.lock (#3818)
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 53330fd..d790893 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -270,11 +270,29 @@ version, ResolvedHostedDescription( HostedDescription(name, url), - sha256: sha256 == null ? null : hexDecode(sha256), + sha256: _parseContentHash(sha256), ), ); } + /// Decodes a sha256 hash from a lock-file or package-listing. + /// It is expected to be a hex-encoded String of length 64. + /// + /// Throws a [FormatException] if the string cannot be decoded. + Uint8List? _parseContentHash(String? encoded) { + if (encoded == null) return null; + if (encoded.length != 64) { + throw FormatException('Content-hash has incorrect length'); + } + try { + return hexDecode(encoded); + } on FormatException catch (e) { + return throw FormatException( + 'Badly formatted content-hash: ${e.message}', + ); + } + } + /// Parses the description for a package. /// /// If the package parses correctly, this returns a (name, url) pair. If not, @@ -390,7 +408,7 @@ pubspec, Uri.parse(archiveUrl), status, - archiveSha256 == null ? null : hexDecode(archiveSha256), + _parseContentHash(archiveSha256), ); }).toList(); } @@ -893,9 +911,12 @@ /// Loads the hash at `hashPath(id)`. Uint8List? sha256FromCache(PackageId id, SystemCache cache) { try { - return hexDecode(readTextFile(hashPath(id, cache))); + return _parseContentHash(readTextFile(hashPath(id, cache))); } on io.IOException { return null; + } on FormatException catch (e) { + log.fine('Bad content-hash in cache: $e, ignoring cache entry'); + return null; } }
diff --git a/test/content_hash_test.dart b/test/content_hash_test.dart index bd106f4..074e167 100644 --- a/test/content_hash_test.dart +++ b/test/content_hash_test.dart
@@ -245,4 +245,29 @@ originalContentHash, ); }); + + test('Badly formatted hash - warning and redownload', () async { + final server = await servePackages(); + server.serveContentHashes = true; + server.serve('foo', '1.0.0'); + await appDir(dependencies: {'foo': 'any'}).create(); + await pubGet(); + final lockfile = loadYaml( + File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync(), + ); + final originalHash = lockfile['packages']['foo']['description']['sha256']; + await hostedHashesCache([ + file( + 'foo-1.0.0.sha256', + 'e', + ), + ]).create(); + + await pubGet( + warning: 'Cached version of foo-1.0.0 has wrong hash - redownloading.', + ); + await hostedHashesCache([ + file('foo-1.0.0.sha256', originalHash), + ]).validate(); + }); }
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart index 5fda64c..ce76003 100644 --- a/test/lock_file_test.dart +++ b/test/lock_file_test.dart
@@ -322,6 +322,32 @@ expectComesFromPubDev('retry'); }); + test('Complains about malformed content-hashes', () { + expect( + () => LockFile.parse( + ''' +packages: + retry: + dependency: transitive + description: + name: retry + url: "https://pub.dev" + sha256: abc # Not long enough + source: hosted + version: "1.0.0" +''', + sources, + ), + throwsA( + isA<FormatException>().having( + (e) => e.message, + 'message', + contains('Content-hash has incorrect length'), + ), + ), + ); + }); + test('ignores extra stuff in file', () { LockFile.parse( '''