diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1b3ab68f6..aaf4671d2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,7 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} @@ -52,7 +52,7 @@ jobs: sdk: [dev] shard: [0, 1, 2, 3, 4, 5, 6] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} diff --git a/doc/cache_layout.md b/doc/cache_layout.md index 8cd76a7a4..47c63056c 100644 --- a/doc/cache_layout.md +++ b/doc/cache_layout.md @@ -14,14 +14,14 @@ cache conventions. ## Location The global default pub-cache is located at: - * `$HOME/.pub_cache` on Linux and Mac OS, + * `$HOME/.pub_cache` on Linux and macOS, * `%LOCALAPPDATA%/Pub/Cache` on Windows. Prior to Flutter 3.8.0, the Flutter SDK declared `PUB_CACHE=$FLUTTER_ROOT/.pub_cache` overriding the default global pub-cache. The default location of the pub-cache can be overridden using the environment variable `PUB_CACHE`. -For the remainder of this document we refer to the location of the pub-cache as `$PUB_CACHE`.``` +For the remainder of this document we refer to the location of the pub-cache as `$PUB_CACHE`. ## Layout @@ -48,7 +48,7 @@ stored in a platform specific config dir: * On Linux `$XDG_CONFIG_HOME/dart/pub-credentials.json` if `$XDG_CONFIG_HOME` is defined, otherwise `$HOME/.config/dart/pub-credentials.json` -* On Mac OS: `$HOME/Library/Application Support/dart/pub-credentials.json` +* On macOS: `$HOME/Library/Application Support/dart/pub-credentials.json` * On Windows: `%APPDATA%/dart/pub-credentials.json` ### Hosted @@ -194,7 +194,6 @@ $PUB_CACHE/global_packages/ │ └── mono_repo.dart-3.0.0-55.0.dev.snapshot ├── .dart_tool/ │ └── package_config.json - ├── incremental └── pubspec.lock ``` @@ -213,9 +212,6 @@ activated package is used by several sdk-versions (TODO: This does have some limitations, and we should probably rethink this). A re-activation of the package will delete all the existing snapshots. -The `incremental` is used while compiling them. (TODO: We should probably remove -this after succesful compilation https://github.com/dart-lang/pub/issues/3896). - For packages activated with `--source=path` the lockfile is special-cased to just point to the activated path, and `.dart_tool/package_config.json`, snapshots are stored in that folder. diff --git a/doc/dart_tool_layout.md b/doc/dart_tool_layout.md new file mode 100644 index 000000000..94f34be02 --- /dev/null +++ b/doc/dart_tool_layout.md @@ -0,0 +1,64 @@ +# Layout of the `.dart_tool/pub` folder + +The pub client creates `.dart_tool/package_config.json` as described by +[https://github.com/dart-lang/language/blob/main/accepted/2.8/language-versioning/package-config-file-v2.md]. + +But furthermore pub can use a folder called `.dart_tool/pub` for storing +artifacts. The organization of that folder is what this document is trying to describe. + +The information in this document is informational, and can be used for +understanding the cache, but we strongly encourage all manipulation of the +`.dart_tool/pub` folder happens though the `dart pub`/`flutter pub` commands to +avoid relying on accidental properties that might be broken in the future. + +## Precompilation cache + +```tree +.dart_tool/ +├── package_config.json +├── pub +│ ├── bin +│ │ ├── pub +│ │ │ └── pub.dart-3.1.0.snapshot.incremental +│ │ └── test +│ │ └── test.dart-3.2.0-36.0.dev.snapshot + +``` + +When `dart run :` is called, pub will try to find `` in +the package `` and compile it as a "dill" file (using +`package:frontend_server_client`). + +The output will be stored in The dill file will be stored in +`.dart_tool/pub/bin//.dart-.snapshot`. + +This can be used to run the executable by invoking (done implicitly by `dart run`): + +``` +dart .dart_tool/pub/bin//.dart-.snapshot +``` + +But the dill-file is also fed to the compiler for incremental compilation. This +can in many cases greatly speed up the compilation when no change has happened. + +If the compilation fails, pub avoids leaving a `.snapshot` file, but instead leaves a +`.dart_tool/pub/bin//.dart-.snapshot.incremental` file. + +This file cannot be executed. But it can still give the benefit of incremental +compilation when changes have happened to the code. + +Earlier versions of the dart sdk would put this "incremental" file in: + +`.dart_tool/pub/incremental//.dart-incremental.dill`. + +As we don't expect many of those files to linger, we don't attempt to clean them up. + +We use the `` to enable different sdk-versions to each have their +own snapshot, so they don't step on each others toes when you switch from one +sdk to another. The downside is that there is no mechanism for deleting +snapshots of old sdks. We might want change that logic. + +One could argue that a "snapshot", is a different thing from a "dill" file in +Dart VM terms. But both can be invoked by the VM, and run rather quickly without +much more pre-compilation. In the future we might want to use true "snapshots" +for executables from immutable packages, as they don't benefit from incremental compilation. diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart index 5b9023cde..ae1b6dcb9 100644 --- a/lib/src/command/deps.dart +++ b/lib/src/command/deps.dart @@ -71,7 +71,8 @@ class DepsCommand extends PubCommand { @override Future runProtected() async { - // Explicitly Run this in the directorycase we don't access `entrypoint.packageGraph`. + // Explicitly run this in the directory in case + // we don't access `entrypoint.packageGraph`. await entrypoint.ensureUpToDate(); final buffer = StringBuffer(); diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart index 521bce0a4..5ae4a72a0 100644 --- a/lib/src/command/outdated.dart +++ b/lib/src/command/outdated.dart @@ -89,7 +89,7 @@ class OutdatedCommand extends PubCommand { argParser.addFlag( 'show-all', - help: 'Include dependencies that are already fullfilling --mode.', + help: 'Include dependencies that are already fulfilling --mode.', ); // Preserve for backwards compatibility. @@ -876,7 +876,7 @@ class _MarkedVersionDetails { final String? _prefix; final String? _suffix; - /// This should be true if the mode creating this consideres the version as + /// This should be true if the mode creating this considers the version as /// "good". /// /// By default only packages with a current version that is not as desired diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 0f15430c3..12ad88907 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml_edit/yaml_edit.dart'; @@ -18,6 +19,7 @@ import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; import '../pubspec_utils.dart'; +import '../sdk.dart'; import '../solver.dart'; import '../source/hosted.dart'; import '../utils.dart'; @@ -65,6 +67,13 @@ class UpgradeCommand extends PubCommand { argParser.addFlag('packages-dir', hide: true); + argParser.addFlag( + 'tighten', + help: + 'Updates lower bounds in pubspec.yaml to match the resolved version.', + negatable: false, + ); + argParser.addFlag( 'major-versions', help: 'Upgrades packages to their latest resolvable versions, ' @@ -92,8 +101,16 @@ class UpgradeCommand extends PubCommand { bool get _dryRun => argResults.flag('dry-run'); + bool get _tighten => argResults.flag('tighten'); + bool get _precompile => argResults.flag('precompile'); + /// List of package names to upgrade, if empty then upgrade all packages. + /// + /// This allows the user to specify list of names that they want the + /// upgrade command to affect. + List get _packagesToUpgrade => argResults.rest; + bool get _upgradeNullSafety => argResults.flag('nullsafety') || argResults.flag('null-safety'); @@ -122,6 +139,20 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); await _runUpgradeMajorVersions(); } else { await _runUpgrade(entrypoint); + if (_tighten) { + final changes = tighten( + entrypoint.root.pubspec, + entrypoint.lockFile.packages.values.toList(), + ); + if (!_dryRun) { + final newPubspecText = _updatePubspec(changes); + + if (changes.isNotEmpty) { + writeTextFile(entrypoint.pubspecPath, newPubspecText); + } + } + _outputChangeSummary(changes); + } } if (argResults.flag('example') && entrypoint.example != null) { // Reload the entrypoint to ensure we pick up potential changes that has @@ -134,15 +165,71 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); Future _runUpgrade(Entrypoint e, {bool onlySummary = false}) async { await e.acquireDependencies( SolveType.upgrade, - unlock: argResults.rest, + unlock: _packagesToUpgrade, dryRun: _dryRun, precompile: _precompile, summaryOnly: onlySummary, analytics: analytics, ); + _showOfflineWarning(); } + /// Returns a list of changes to constraints in [pubspec] updated them to + /// have their lower bound match the version in [packages]. + /// + /// The return value is a mapping from the original package range to the updated. + /// + /// If packages to update where given in [_packagesToUpgrade], only those are + /// tightened. Otherwise all packages are tightened. + /// + /// If a dependency has already been updated in [existingChanges], the update + /// will apply on top of that change (eg. preserving the new upper bound). + Map tighten( + Pubspec pubspec, + List packages, { + Map existingChanges = const {}, + }) { + final result = {...existingChanges}; + if (argResults.flag('example') && entrypoint.example != null) { + log.warning( + 'Running `upgrade --tighten` only in `${entrypoint.rootDir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.', + ); + } + final toTighten = _packagesToUpgrade.isEmpty + ? [ + ...pubspec.dependencies.values, + ...pubspec.devDependencies.values, + ] + : [ + for (final name in _packagesToUpgrade) + pubspec.dependencies[name] ?? pubspec.devDependencies[name], + ].whereNotNull(); + for (final range in toTighten) { + final constraint = (result[range] ?? range).constraint; + final resolvedVersion = + packages.firstWhere((p) => p.name == range.name).version; + if (range.source is HostedSource && constraint.isAny) { + result[range] = range + .toRef() + .withConstraint(VersionConstraint.compatibleWith(resolvedVersion)); + } else if (constraint is VersionRange) { + final min = constraint.min; + if (min != null && min < resolvedVersion) { + result[range] = range.toRef().withConstraint( + VersionRange( + min: resolvedVersion, + max: constraint.max, + includeMin: true, + includeMax: constraint.includeMax, + ).asCompatibleWithIfPossible(), + ); + } + } + } + return result; + } + /// Return names of packages to be upgraded, and throws [UsageException] if /// any package names not in the direct dependencies or dev_dependencies are given. /// @@ -154,7 +241,8 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); ...entrypoint.root.pubspec.dependencies.keys, ...entrypoint.root.pubspec.devDependencies.keys, ]; - final toUpgrade = argResults.rest.isEmpty ? directDeps : argResults.rest; + final toUpgrade = + _packagesToUpgrade.isEmpty ? directDeps : _packagesToUpgrade; // Check that all package names in upgradeOnly are direct-dependencies final notInDeps = toUpgrade.where((n) => !directDeps.contains(n)); @@ -203,7 +291,7 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: // Changes to be made to `pubspec.yaml`. // Mapping from original to changed value. - final changes = {}; + var changes = {}; final declaredHostedDependencies = [ ...entrypoint.root.pubspec.dependencies.values, ...entrypoint.root.pubspec.devDependencies.values, @@ -233,7 +321,24 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: ), ); } - final newPubspecText = _updatePubspec(changes); + var newPubspecText = _updatePubspec(changes); + if (_tighten) { + // Do another solve with the updated constraints to obtain the correct + // versions to tighten to. This should be fast (everything is cached, and + // no backtracking needed) so we don't show a spinner. + + final solveResult = await resolveVersions( + SolveType.upgrade, + cache, + Package.inMemory(_updatedPubspec(newPubspecText, entrypoint)), + ); + changes = tighten( + entrypoint.root.pubspec, + solveResult.packages, + existingChanges: changes, + ); + newPubspecText = _updatePubspec(changes); + } // When doing '--majorVersions' for specific packages we try to update other // packages as little as possible to make a focused change (SolveType.get). @@ -241,7 +346,7 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: // But without a specific package we want to get as many non-major updates // as possible (SolveType.upgrade). final solveType = - argResults.rest.isEmpty ? SolveType.upgrade : SolveType.get; + _packagesToUpgrade.isEmpty ? SolveType.upgrade : SolveType.get; if (!_dryRun) { if (changes.isNotEmpty) { @@ -249,25 +354,8 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: } } - String? overridesFileContents; - final overridesPath = - p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename); - try { - overridesFileContents = readTextFile(overridesPath); - } on IOException { - overridesFileContents = null; - } - await entrypoint - .withPubspec( - Pubspec.parse( - newPubspecText, - cache.sources, - location: Uri.parse(entrypoint.pubspecPath), - overridesFileContents: overridesFileContents, - overridesLocation: Uri.file(overridesPath), - ), - ) + .withPubspec(_updatedPubspec(newPubspecText, entrypoint)) .acquireDependencies( solveType, dryRun: _dryRun, @@ -291,6 +379,24 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: _showOfflineWarning(); } + Pubspec _updatedPubspec(String contents, Entrypoint entrypoint) { + String? overridesFileContents; + final overridesPath = + p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename); + try { + overridesFileContents = readTextFile(overridesPath); + } on IOException { + overridesFileContents = null; + } + return Pubspec.parse( + contents, + cache.sources, + location: Uri.parse(entrypoint.pubspecPath), + overridesFileContents: overridesFileContents, + overridesLocation: Uri.file(overridesPath), + ); + } + /// Updates `pubspec.yaml` with given [changes]. String _updatePubspec( Map changes, @@ -311,9 +417,7 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: } /// Outputs a summary of changes made to `pubspec.yaml`. - void _outputChangeSummary( - Map changes, - ) { + void _outputChangeSummary(Map changes) { ArgumentError.checkNotNull(changes, 'changes'); if (changes.isEmpty) { diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 65af797dd..d7e235f3e 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -36,9 +36,9 @@ import 'utils.dart'; /// The name of the program that is invoking pub /// 'flutter' if we are running inside `flutter pub` 'dart' otherwise. -String topLevelProgram = _isrunningInsideFlutter ? 'flutter' : 'dart'; +String topLevelProgram = _isRunningInsideFlutter ? 'flutter' : 'dart'; -bool _isrunningInsideFlutter = +bool _isRunningInsideFlutter = (Platform.environment['PUB_ENVIRONMENT'] ?? '').contains('flutter_cli'); class PubCommandRunner extends CommandRunner implements PubTopLevel { diff --git a/lib/src/dart.dart b/lib/src/dart.dart index 4e1c4d1b7..85e309559 100644 --- a/lib/src/dart.dart +++ b/lib/src/dart.dart @@ -96,10 +96,13 @@ class AnalyzerErrorGroup implements Exception { /// /// If the compilation succeeds it is saved to a kernel file at [outputPath]. /// -/// If compilation fails, the output is cached at [incrementalDillOutputPath]. +/// If compilation fails, the output is cached at "[outputPath].incremental". /// -/// Whichever of [incrementalDillOutputPath] and [outputPath] already exists is -/// used to initialize the compiler run. +/// Whichever of "[outputPath].incremental" and [outputPath] already exists is +/// used to initialize the compiler run. To avoid the potential for +/// race-conditions, it is first copied to a temporary location, and atomically +/// moved to either [outputPath] or "[outputPath].incremental" depending on the +/// result of compilation. /// /// The [packageConfigPath] should point at the package config file to be used /// for `package:` uri resolution. @@ -114,31 +117,29 @@ class AnalyzerErrorGroup implements Exception { /// assets map. Future precompile({ required String executablePath, - required String incrementalDillPath, required String name, required String outputPath, required String packageConfigPath, List additionalSources = const [], String? nativeAssets, }) async { - ensureDir(p.dirname(outputPath)); - ensureDir(p.dirname(incrementalDillPath)); - const platformDill = 'lib/_internal/vm_platform_strong.dill'; final sdkRoot = p.relative(p.dirname(p.dirname(Platform.resolvedExecutable))); String? tempDir; FrontendServerClient? client; try { - tempDir = createTempDir(p.dirname(incrementalDillPath), 'tmp'); + ensureDir(p.dirname(outputPath)); + final incrementalDillPath = '$outputPath.incremental'; + tempDir = createTempDir(p.dirname(outputPath), 'tmp'); // To avoid potential races we copy the incremental data to a temporary file // for just this compilation. final temporaryIncrementalDill = p.join(tempDir, '${p.basename(incrementalDillPath)}.temp'); try { - if (fileExists(incrementalDillPath)) { - copyFile(incrementalDillPath, temporaryIncrementalDill); - } else if (fileExists(outputPath)) { + if (fileExists(outputPath)) { copyFile(outputPath, temporaryIncrementalDill); + } else if (fileExists(incrementalDillPath)) { + copyFile(incrementalDillPath, temporaryIncrementalDill); } } on FileSystemException { // Not able to copy existing file, compilation will start from scratch. @@ -172,11 +173,14 @@ Future precompile({ // By using rename we ensure atomicity. An external observer will either // see the old or the new snapshot. renameFile(temporaryIncrementalDill, outputPath); + // Any old incremental data is deleted in case we started from a file on + // [incrementalDillPath]. + deleteEntry(incrementalDillPath); } else { // By using rename we ensure atomicity. An external observer will either // see the old or the new snapshot. renameFile(temporaryIncrementalDill, incrementalDillPath); - // If compilation failed we don't want to leave an incorrect snapshot. + // If compilation failed, don't leave an incorrect snapshot. tryDeleteEntry(outputPath); throw ApplicationException( diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 8d6dd67c2..b149008a3 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -87,6 +87,7 @@ class Entrypoint { /// /// For a global package, this is the activated package. Package get root => _root ??= Package.load( + null, rootDir, cache.sources, withPubspecOverrides: true, @@ -177,6 +178,7 @@ class Entrypoint { var packages = { for (var packageEntry in packageConfig.nonInjectedPackages) packageEntry.name: Package.load( + packageEntry.name, packageEntry.resolvedRootDir(packageConfigPath), cache.sources, ), @@ -220,10 +222,6 @@ class Entrypoint { /// The path to the directory containing dependency executable snapshots. String get _snapshotPath => p.join(cachePath, 'bin'); - /// The path to the directory containing previous dill files for incremental - /// builds. - String get _incrementalDillsPath => p.join(cachePath, 'incremental'); - Entrypoint._( this.rootDir, this._lockFile, @@ -522,7 +520,6 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without await dart.precompile( executablePath: await resolveExecutable(executable), outputPath: pathOfExecutable(executable), - incrementalDillPath: incrementalDillPathOfExecutable(executable), packageConfigPath: packageConfigPath, name: '$package:${p.basenameWithoutExtension(executable.relativePath)}', additionalSources: additionalSources, @@ -553,20 +550,6 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without ); } - String incrementalDillPathOfExecutable(Executable executable) { - assert(p.isRelative(executable.relativePath)); - return isGlobal - ? p.join( - _incrementalDillsPath, - '${p.basename(executable.relativePath)}.incremental.dill', - ) - : p.join( - _incrementalDillsPath, - executable.package, - '${p.basename(executable.relativePath)}.incremental.dill', - ); - } - /// The absolute path of [executable] resolved relative to [this]. Future resolveExecutable(Executable executable) async { return p.join( diff --git a/lib/src/executable.dart b/lib/src/executable.dart index ea5820567..9b03a2499 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -132,7 +132,7 @@ Future runExecutable( } /// Runs the dart program (can be a snapshot) at [path] with [args] and hooks -/// its stdout, stderr, and sdtin to this process's. +/// its stdout, stderr, and stdin to this process's. /// /// [packageConfig] is the path to the ".dart_tool/package_config.json" file. /// @@ -140,7 +140,7 @@ Future runExecutable( /// /// Passes [vmArgs] to the vm. /// -/// Returns the programs's exit code. +/// Returns the program's exit code. /// /// Tries to run the program as an isolate if no special [vmArgs] are given /// otherwise starts new vm in a subprocess. If [alwaysUseSubprocess] is `true` diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index 9315c63cd..c68c6b025 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart @@ -867,7 +867,7 @@ fi // Write this as the system encoding since the system is going to // execute it and it might contain non-ASCII characters in the - // pathnames. + // path names. writeTextFile(tmpPath, binstub, encoding: const SystemEncoding()); if (Platform.isLinux || Platform.isMacOS) { diff --git a/lib/src/ignore.dart b/lib/src/ignore.dart index 2633521f1..75c0f1f08 100644 --- a/lib/src/ignore.dart +++ b/lib/src/ignore.dart @@ -55,7 +55,7 @@ final class Ignore { /// Each value in [patterns] will be interpreted as one or more lines from /// a `.gitignore` file, in compliance with the [`.gitignore` manual page][1]. /// - /// The keys of 'pattern' are the directories to intpret the rules relative + /// The keys of 'pattern' are the directories to interpret the rules relative /// to. The root should be the empty string, and sub-directories are separated /// by '/' (but no final '/'). /// diff --git a/lib/src/io.dart b/lib/src/io.dart index ab20655cb..3d105a10e 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -17,9 +17,9 @@ import 'package:http_multi_server/http_multi_server.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:pool/pool.dart'; -// ignore: prefer_relative_imports -import 'package:pub/src/third_party/tar/lib/tar.dart'; import 'package:stack_trace/stack_trace.dart'; +// ignore: prefer_relative_imports +import 'package:tar/tar.dart'; import 'error_group.dart'; import 'exceptions.dart'; @@ -148,7 +148,7 @@ String canonicalize(String pathString) { // Pop directories off `newPath` if the component links upwards in the // directory hierarchy. - while (relativeComponents.first == '..') { + while (relativeComponents.firstOrNull == '..') { newPath = path.dirname(newPath); relativeComponents.removeFirst(); } @@ -346,7 +346,7 @@ List listDir( bool includeDirs = true, Iterable allowed = const [], }) { - var allowlistFilter = createFileFilter(allowed); + var allowListFilter = createFileFilter(allowed); // This is used in some performance-sensitive paths and can list many, many // files. As such, it leans more heavily towards optimization as opposed to @@ -376,9 +376,9 @@ List listDir( // If the basename is in [allowed], don't count its "/." as making the // file hidden. - if (allowlistFilter.any(pathInDir.contains)) { + if (allowListFilter.any(pathInDir.contains)) { final allowedBasename = - allowlistFilter.firstWhere(pathInDir.contains); + allowListFilter.firstWhere(pathInDir.contains); pathInDir = pathInDir.substring(0, pathInDir.length - allowedBasename.length); } diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart index ebf04730e..cecf4072f 100644 --- a/lib/src/lock_file.dart +++ b/lib/src/lock_file.dart @@ -496,8 +496,8 @@ ${yamlToString(data)} /// Returns `true` if the [text] looks like it uses windows line endings. /// -/// The heuristic used is to count all `\n` in the text and if stricly more than -/// half of them are preceded by `\r` we report `true`. +/// The heuristic used is to count all `\n` in the text and if strictly more +/// than half of them are preceded by `\r` we report `true`. @visibleForTesting bool detectWindowsLineEndings(String text) { var index = -1; diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index d4d88c9f6..8131ceb09 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -3,13 +3,16 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:http/retry.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart' as path; -// ignore: prefer_relative_imports -import 'package:pub/src/third_party/oauth2/lib/oauth2.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; @@ -104,7 +107,7 @@ void logout() { } } -/// Asynchronously passes an OAuth2 [Client] to [fn]. +/// Asynchronously passes an OAuth2 [_Client] to [fn]. /// /// Does not close the client, since that would close the shared client. It must /// be closed elsewhere. @@ -112,7 +115,7 @@ void logout() { /// This takes care of loading and saving the client's credentials, as well as /// prompting the user for their authorization. It will also re-authorize and /// re-run [fn] if a recoverable authorization error is detected. -Future withClient(Future Function(Client) fn) { +Future withClient(Future Function(http.Client) fn) { return _getClient().then((client) { return fn(client).whenComplete(() { // TODO(sigurdm): refactor the http subsystem, so we can close [client] @@ -122,11 +125,11 @@ Future withClient(Future Function(Client) fn) { _saveCredentials(client.credentials); }); }).catchError((error) { - if (error is ExpirationException) { + if (error is _ExpirationException) { log.error("Pub's authorization to upload packages has expired and " "can't be automatically refreshed."); return withClient(fn); - } else if (error is AuthorizationException) { + } else if (error is _AuthorizationException) { var message = 'OAuth2 authorization failed'; if (error.description != null) { message = '$message (${error.description})'; @@ -145,11 +148,11 @@ Future withClient(Future Function(Client) fn) { /// /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. -Future _getClient() async { +Future<_Client> _getClient() async { var credentials = loadCredentials(); if (credentials == null) return await _authorize(); - var client = Client( + var client = _Client( credentials, identifier: _identifier, secret: _secret, @@ -184,7 +187,9 @@ Credentials? loadCredentials() { return credentials; } catch (e) { - log.error('Warning: could not load the saved OAuth2 credentials: $e\n' + // Don't print the error message itself here. I might be leaking data about + // credentials. + log.error('Warning: could not load the saved OAuth2 credentials.\n' 'Obtaining new credentials...'); return null; // null means re-authorize. } @@ -214,9 +219,9 @@ String? _credentialsFile() { /// Gets the user to authorize pub as a client of pub.dev via oauth2. /// -/// Returns a Future that completes to a fully-authorized [Client]. -Future _authorize() async { - var grant = AuthorizationCodeGrant( +/// Returns a Future that completes to a fully-authorized [_Client]. +Future<_Client> _authorize() async { + var grant = _AuthorizationCodeGrant( _identifier, _authorizationEndpoint, tokenEndpoint, secret: _secret, // Google's OAuth2 API doesn't support basic auth. @@ -227,7 +232,7 @@ Future _authorize() async { // Spin up a one-shot HTTP server to receive the authorization code from the // Google OAuth2 server via redirect. This server will close itself as soon as // the code is received. - var completer = Completer(); + var completer = Completer<_Client>(); var server = await bindServer('localhost', 0); shelf_io.serveRequests(server, (request) { if (request.url.path.isNotEmpty) { @@ -273,3 +278,1065 @@ Future fetchOidcDiscoveryDocument() async { }); return parseJsonResponse(discoveryResponse); } + +// The following code originates in package:oauth2. +// TODO(sigurdm): simplify to only do what we need. + +/// A class for obtaining credentials via an [authorization code grant][]. +/// +/// This method of authorization involves sending the resource owner to the +/// authorization server where they will authorize the client. They're then +/// redirected back to your server, along with an authorization code. This is +/// used to obtain [Credentials] and create a fully-authorized [_Client]. +/// +/// To use this class, you must first call [getAuthorizationUrl] to get the URL +/// to which to redirect the resource owner. Then once they've been redirected +/// back to your application, call [handleAuthorizationResponse] or +/// [handleAuthorizationCode] to process the authorization server's response and +/// construct a [_Client]. +/// +/// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1 +class _AuthorizationCodeGrant { + /// The function used to parse parameters from a host's response. + final _GetParameters _getParameters; + + /// The client identifier for this client. + /// + /// The authorization server will issue each client a separate client + /// identifier and secret, which allows the server to tell which client is + /// accessing it. Some servers may also have an anonymous identifier/secret + /// pair that any client may use. + /// + /// This is usually global to the program using this library. + final String identifier; + + /// The client secret for this client. + /// + /// The authorization server will issue each client a separate client + /// identifier and secret, which allows the server to tell which client is + /// accessing it. Some servers may also have an anonymous identifier/secret + /// pair that any client may use. + /// + /// This is usually global to the program using this library. + /// + /// Note that clients whose source code or binary executable is readily + /// available may not be able to make sure the client secret is kept a secret. + /// This is fine; OAuth2 servers generally won't rely on knowing with + /// certainty that a client is who it claims to be. + final String? secret; + + /// A URL provided by the authorization server that serves as the base for the + /// URL that the resource owner will be redirected to to authorize this + /// client. + /// + /// This will usually be listed in the authorization server's OAuth2 API + /// documentation. + final Uri authorizationEndpoint; + + /// A URL provided by the authorization server that this library uses to + /// obtain long-lasting credentials. + /// + /// This will usually be listed in the authorization server's OAuth2 API + /// documentation. + final Uri tokenEndpoint; + + /// Callback to be invoked whenever the credentials are refreshed. + /// + /// This will be passed as-is to the constructed [_Client]. + final _CredentialsRefreshedCallback? _onCredentialsRefreshed; + + /// Whether to use HTTP Basic authentication for authorizing the client. + final bool _basicAuth; + + /// A [String] used to separate scopes; defaults to `" "`. + final String _delimiter; + + /// The HTTP client used to make HTTP requests. + http.Client? _httpClient; + + /// The URL to which the resource owner will be redirected after they + /// authorize this client with the authorization server. + Uri? _redirectEndpoint; + + /// The scopes that the client is requesting access to. + List? _scopes; + + /// An opaque string that users of this library may specify that will be + /// included in the response query parameters. + String? _stateString; + + /// The current state of the grant object. + _State _state = _State.initial; + + /// Allowed characters for generating the _codeVerifier + static const String _charset = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + + /// The PKCE code verifier. Will be generated if one is not provided in the + /// constructor. + final String _codeVerifier; + + /// Creates a new grant. + /// + /// If [basicAuth] is `true` (the default), the client credentials are sent to + /// the server using using HTTP Basic authentication as defined in [RFC 2617]. + /// Otherwise, they're included in the request body. Note that the latter form + /// is not recommended by the OAuth 2.0 spec, and should only be used if the + /// server doesn't support Basic authentication. + /// + /// [RFC 2617]: https://tools.ietf.org/html/rfc2617 + /// + /// [httpClient] is used for all HTTP requests made by this grant, as well as + /// those of the [_Client] is constructs. + /// + /// [onCredentialsRefreshed] will be called by the constructed [_Client] + /// whenever the credentials are refreshed. + /// + /// [codeVerifier] String to be used as PKCE code verifier. If none is + /// provided a random codeVerifier will be generated. + /// The codeVerifier must meet requirements specified in [RFC 7636]. + /// + /// [RFC 7636]: https://tools.ietf.org/html/rfc7636#section-4.1 + /// + /// The scope strings will be separated by the provided [delimiter]. This + /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) + /// use non-standard delimiters. + /// + /// By default, this follows the OAuth2 spec and requires the server's + /// responses to be in JSON format. However, some servers return non-standard + /// response formats, which can be parsed using the [getParameters] function. + /// + /// This function is passed the `Content-Type` header of the response as well + /// as its body as a UTF-8-decoded string. It should return a map in the same + /// format as the [standard JSON response][]. + /// + /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 + _AuthorizationCodeGrant( + this.identifier, + this.authorizationEndpoint, + this.tokenEndpoint, { + this.secret, + String? delimiter, + bool basicAuth = true, + http.Client? httpClient, + _CredentialsRefreshedCallback? onCredentialsRefreshed, + Map Function(MediaType? contentType, String body)? + getParameters, + String? codeVerifier, + }) : _basicAuth = basicAuth, + _httpClient = httpClient ?? http.Client(), + _delimiter = delimiter ?? ' ', + _getParameters = getParameters ?? parseJsonParameters, + _onCredentialsRefreshed = onCredentialsRefreshed, + _codeVerifier = codeVerifier ?? _createCodeVerifier(); + + /// Returns the URL to which the resource owner should be redirected to + /// authorize this client. + /// + /// The resource owner will then be redirected to [redirect], which should + /// point to a server controlled by the client. This redirect will have + /// additional query parameters that should be passed to + /// [handleAuthorizationResponse]. + /// + /// The specific permissions being requested from the authorization server may + /// be specified via [scopes]. The scope strings are specific to the + /// authorization server and may be found in its documentation. Note that you + /// may not be granted access to every scope you request; you may check the + /// [Credentials.scopes] field of [_Client.credentials] to see which scopes you + /// were granted. + /// + /// An opaque [state] string may also be passed that will be present in the + /// query parameters provided to the redirect URL. + /// + /// It is a [StateError] to call this more than once. + Uri getAuthorizationUrl( + Uri redirect, { + Iterable? scopes, + String? state, + }) { + if (_state != _State.initial) { + throw StateError('The authorization URL has already been generated.'); + } + _state = _State.awaitingResponse; + + var scopeList = scopes?.toList() ?? []; + var codeChallenge = base64Url + .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes) + .replaceAll('=', ''); + + _redirectEndpoint = redirect; + _scopes = scopeList; + _stateString = state; + var parameters = { + 'response_type': 'code', + 'client_id': identifier, + 'redirect_uri': redirect.toString(), + 'code_challenge': codeChallenge, + 'code_challenge_method': 'S256', + }; + + if (state != null) parameters['state'] = state; + if (scopeList.isNotEmpty) parameters['scope'] = scopeList.join(_delimiter); + + return _addQueryParameters(authorizationEndpoint, parameters); + } + + /// Processes the query parameters added to a redirect from the authorization + /// server. + /// + /// Note that this "response" is not an HTTP response, but rather the data + /// passed to a server controlled by the client as query parameters on the + /// redirect URL. + /// + /// It is a [StateError] to call this more than once, to call it before + /// [getAuthorizationUrl] is called, or to call it after + /// [handleAuthorizationCode] is called. + /// + /// Throws [FormatException] if [parameters] is invalid according to the + /// OAuth2 spec or if the authorization server otherwise provides invalid + /// responses. If `state` was passed to [getAuthorizationUrl], this will throw + /// a [FormatException] if the `state` parameter doesn't match the original + /// value. + /// + /// Throws [_AuthorizationException] if the authorization fails. + Future<_Client> handleAuthorizationResponse( + Map parameters, + ) async { + if (_state == _State.initial) { + throw StateError('The authorization URL has not yet been generated.'); + } else if (_state == _State.finished) { + throw StateError('The authorization code has already been received.'); + } + _state = _State.finished; + + if (_stateString != null) { + if (!parameters.containsKey('state')) { + throw FormatException('Invalid OAuth response for ' + '"$authorizationEndpoint": parameter "state" expected to be ' + '"$_stateString", was missing.'); + } else if (parameters['state'] != _stateString) { + throw FormatException('Invalid OAuth response for ' + '"$authorizationEndpoint": parameter "state" expected to be ' + '"$_stateString", was "${parameters['state']}".'); + } + } + + if (parameters.containsKey('error')) { + var description = parameters['error_description']; + var uriString = parameters['error_uri']; + var uri = uriString == null ? null : Uri.parse(uriString); + throw _AuthorizationException(parameters['error']!, description, uri); + } else if (!parameters.containsKey('code')) { + throw FormatException('Invalid OAuth response for ' + '"$authorizationEndpoint": did not contain required parameter ' + '"code".'); + } + + return _handleAuthorizationCode(parameters['code']); + } + + /// Processes an authorization code directly. + /// + /// Usually [handleAuthorizationResponse] is preferable to this method, since + /// it validates all of the query parameters. However, some authorization + /// servers allow the user to copy and paste an authorization code into a + /// command-line application, in which case this method must be used. + /// + /// It is a [StateError] to call this more than once, to call it before + /// [getAuthorizationUrl] is called, or to call it after + /// [handleAuthorizationCode] is called. + /// + /// Throws [FormatException] if the authorization server provides invalid + /// responses while retrieving credentials. + /// + /// Throws [_AuthorizationException] if the authorization fails. + Future<_Client> handleAuthorizationCode(String authorizationCode) async { + if (_state == _State.initial) { + throw StateError('The authorization URL has not yet been generated.'); + } else if (_state == _State.finished) { + throw StateError('The authorization code has already been received.'); + } + _state = _State.finished; + + return _handleAuthorizationCode(authorizationCode); + } + + /// This works just like [handleAuthorizationCode], except it doesn't validate + /// the state beforehand. + Future<_Client> _handleAuthorizationCode(String? authorizationCode) async { + var startTime = DateTime.now(); + + var headers = {}; + + var body = { + 'grant_type': 'authorization_code', + 'code': authorizationCode, + 'redirect_uri': _redirectEndpoint.toString(), + 'code_verifier': _codeVerifier, + }; + + var secret = this.secret; + if (_basicAuth && secret != null) { + headers['Authorization'] = _basicAuthHeader(identifier, secret); + } else { + // The ID is required for this request any time basic auth isn't being + // used, even if there's no actual client authentication to be done. + body['client_id'] = identifier; + if (secret != null) body['client_secret'] = secret; + } + + var response = + await _httpClient!.post(tokenEndpoint, headers: headers, body: body); + + var credentials = _handleAccessTokenResponse( + response, + tokenEndpoint, + startTime, + _scopes, + _delimiter, + getParameters: _getParameters, + ); + return _Client( + credentials, + identifier: identifier, + secret: secret, + basicAuth: _basicAuth, + httpClient: _httpClient, + onCredentialsRefreshed: _onCredentialsRefreshed, + ); + } + + // Randomly generate a 128 character string to be used as the PKCE code + // verifier. + static String _createCodeVerifier() => List.generate( + 128, + (i) => _charset[Random.secure().nextInt(_charset.length)], + ).join(); + + /// Closes the grant and frees its resources. + /// + /// This will close the underlying HTTP client, which is shared by the + /// [_Client] created by this grant, so it's not safe to close the grant and + /// continue using the client. + void close() { + _httpClient?.close(); + _httpClient = null; + } +} + +/// States that [_AuthorizationCodeGrant] can be in. +enum _State { + initial('initial'), + awaitingResponse('awaiting response'), + finished('finished'); + + final String _name; + + const _State(this._name); + + @override + String toString() => _name; +} + +/// An exception raised when OAuth2 authorization fails. +class _AuthorizationException implements Exception { + /// The name of the error. + /// + /// Possible names are enumerated in [the spec][]. + /// + /// [the spec]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.2 + final String error; + + /// The description of the error, provided by the server. + /// + /// May be `null` if the server provided no description. + final String? description; + + /// A URL for a page that describes the error in more detail, provided by the + /// server. + /// + /// May be `null` if the server provided no URL. + final Uri? uri; + + /// Creates an AuthorizationException. + _AuthorizationException(this.error, this.description, this.uri); + + /// Provides a string description of the AuthorizationException. + @override + String toString() { + var header = 'OAuth authorization error ($error)'; + if (description != null) { + header = '$header: $description'; + } else if (uri != null) { + header = '$header: $uri'; + } + return '$header.'; + } +} + +/// An OAuth2 client. +/// +/// This acts as a drop-in replacement for an [http.Client], while sending +/// OAuth2 authorization credentials along with each request. +/// +/// The client also automatically refreshes its credentials if possible. When it +/// makes a request, if its credentials are expired, it will first refresh them. +/// This means that any request may throw an [_AuthorizationException] if the +/// refresh is not authorized for some reason, a [FormatException] if the +/// authorization server provides ill-formatted responses, or an +/// [_ExpirationException] if the credentials are expired and can't be refreshed. +/// +/// The client will also throw an [_AuthorizationException] if the resource +/// server returns a 401 response with a WWW-Authenticate header indicating that +/// the current credentials are invalid. +/// +/// If you already have a set of [Credentials], you can construct a [_Client] +/// directly. However, in order to first obtain the credentials, you must +/// authorize. At the time of writing, the only authorization method this +/// library supports is [_AuthorizationCodeGrant]. +class _Client extends http.BaseClient { + /// The client identifier for this client. + /// + /// The authorization server will issue each client a separate client + /// identifier and secret, which allows the server to tell which client is + /// accessing it. Some servers may also have an anonymous identifier/secret + /// pair that any client may use. + /// + /// This is usually global to the program using this library. + final String? identifier; + + /// The client secret for this client. + /// + /// The authorization server will issue each client a separate client + /// identifier and secret, which allows the server to tell which client is + /// accessing it. Some servers may also have an anonymous identifier/secret + /// pair that any client may use. + /// + /// This is usually global to the program using this library. + /// + /// Note that clients whose source code or binary executable is readily + /// available may not be able to make sure the client secret is kept a secret. + /// This is fine; OAuth2 servers generally won't rely on knowing with + /// certainty that a client is who it claims to be. + final String? secret; + + /// The credentials this client uses to prove to the resource server that it's + /// authorized. + /// + /// This may change from request to request as the credentials expire and the + /// client refreshes them automatically. + Credentials get credentials => _credentials; + Credentials _credentials; + + /// Callback to be invoked whenever the credentials refreshed. + final _CredentialsRefreshedCallback? _onCredentialsRefreshed; + + /// Whether to use HTTP Basic authentication for authorizing the client. + final bool _basicAuth; + + /// The underlying HTTP client. + http.Client? _httpClient; + + /// Creates a new client from a pre-existing set of credentials. + /// + /// When authorizing a client for the first time, you should use + /// [_AuthorizationCodeGrant] or [_resourceOwnerPasswordGrant] instead of + /// constructing a [_Client] directly. + /// + /// [httpClient] is the underlying client that this forwards requests to after + /// adding authorization credentials to them. + /// + /// Throws an [ArgumentError] if [secret] is passed without [identifier]. + _Client( + this._credentials, { + this.identifier, + this.secret, + _CredentialsRefreshedCallback? onCredentialsRefreshed, + bool basicAuth = true, + http.Client? httpClient, + }) : _basicAuth = basicAuth, + _onCredentialsRefreshed = onCredentialsRefreshed, + _httpClient = httpClient ?? http.Client() { + if (identifier == null && secret != null) { + throw ArgumentError('secret may not be passed without identifier.'); + } + } + + /// Sends an HTTP request with OAuth2 authorization credentials attached. + /// + /// This will also automatically refresh this client's [Credentials] before + /// sending the request if necessary. + @override + Future send(http.BaseRequest request) async { + if (credentials.isExpired) { + if (!credentials.canRefresh) throw _ExpirationException(credentials); + await refreshCredentials(); + } + + request.headers['authorization'] = 'Bearer ${credentials.accessToken}'; + var response = await _httpClient!.send(request); + + if (response.statusCode != 401) return response; + if (!response.headers.containsKey('www-authenticate')) return response; + + List challenges; + try { + challenges = AuthenticationChallenge.parseHeader( + response.headers['www-authenticate']!, + ); + } on FormatException { + return response; + } + + var challenge = challenges + .firstWhereOrNull((challenge) => challenge.scheme == 'bearer'); + if (challenge == null) return response; + + var params = challenge.parameters; + if (!params.containsKey('error')) return response; + + throw _AuthorizationException( + params['error']!, + params['error_description'], + params['error_uri'] == null ? null : Uri.parse(params['error_uri']!), + ); + } + + /// A [Future] used to track whether [refreshCredentials] is running. + Future? _refreshingFuture; + + /// Explicitly refreshes this client's credentials. Returns this client. + /// + /// This will throw a [StateError] if the [Credentials] can't be refreshed, an + /// [_AuthorizationException] if refreshing the credentials fails, or a + /// [FormatException] if the authorization server returns invalid responses. + /// + /// You may request different scopes than the default by passing in + /// [newScopes]. These must be a subset of the scopes in the + /// [Credentials.scopes] field of [_Client.credentials]. + Future<_Client> refreshCredentials([List? newScopes]) async { + if (!credentials.canRefresh) { + var prefix = 'OAuth credentials'; + if (credentials.isExpired) prefix = '$prefix have expired and'; + throw StateError("$prefix can't be refreshed."); + } + + // To make sure that only one refresh happens when credentials are expired + // we track it using the [_refreshingFuture]. And also make sure that the + // _onCredentialsRefreshed callback is only called once. + if (_refreshingFuture == null) { + try { + _refreshingFuture = credentials.refresh( + identifier: identifier, + secret: secret, + newScopes: newScopes, + basicAuth: _basicAuth, + httpClient: _httpClient, + ); + _credentials = await _refreshingFuture!; + _onCredentialsRefreshed?.call(_credentials); + } finally { + _refreshingFuture = null; + } + } else { + await _refreshingFuture; + } + + return this; + } + + /// Closes this client and its underlying HTTP client. + @override + void close() { + _httpClient?.close(); + _httpClient = null; + } +} + +/// Type of the callback when credentials are refreshed. +typedef _CredentialsRefreshedCallback = void Function(Credentials); + +/// Credentials that prove that a client is allowed to access a resource on the +/// resource owner's behalf. +/// +/// These credentials are long-lasting and can be safely persisted across +/// multiple runs of the program. +/// +/// Many authorization servers will attach an expiration date to a set of +/// credentials, along with a token that can be used to refresh the credentials +/// once they've expired. The [_Client] will automatically refresh its +/// credentials when necessary. It's also possible to explicitly refresh them +/// via [_Client.refreshCredentials] or [Credentials.refresh]. +/// +/// Note that a given set of credentials can only be refreshed once, so be sure +/// to save the refreshed credentials for future use. +class Credentials { + /// A [String] used to separate scopes; defaults to `" "`. + String _delimiter; + + /// The token that is sent to the resource server to prove the authorization + /// of a client. + final String accessToken; + + /// The token that is sent to the authorization server to refresh the + /// credentials. + /// + /// This may be `null`, indicating that the credentials can't be refreshed. + final String? refreshToken; + + /// The token that is received from the authorization server to enable + /// End-Users to be Authenticated, contains Claims, represented as a + /// JSON Web Token (JWT). + /// + /// This may be `null`, indicating that the 'openid' scope was not + /// requested (or not supported). + /// + /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken + final String? idToken; + + /// The URL of the authorization server endpoint that's used to refresh the + /// credentials. + /// + /// This may be `null`, indicating that the credentials can't be refreshed. + final Uri? tokenEndpoint; + + /// The specific permissions being requested from the authorization server. + /// + /// The scope strings are specific to the authorization server and may be + /// found in its documentation. + final List? scopes; + + /// The date at which these credentials will expire. + /// + /// This is likely to be a few seconds earlier than the server's idea of the + /// expiration date. + final DateTime? expiration; + + /// The function used to parse parameters from a host's response. + final _GetParameters _getParameters; + + /// Whether or not these credentials have expired. + /// + /// Note that it's possible the credentials will expire shortly after this is + /// called. However, since the client's expiration date is kept a few seconds + /// earlier than the server's, there should be enough leeway to rely on this. + bool get isExpired { + var expiration = this.expiration; + return expiration != null && DateTime.now().isAfter(expiration); + } + + /// Whether it's possible to refresh these credentials. + bool get canRefresh => refreshToken != null && tokenEndpoint != null; + + /// Creates a new set of credentials. + /// + /// This class is usually not constructed directly; rather, it's accessed via + /// [_Client.credentials] after a [_Client] is created by + /// [_AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized + /// form via [Credentials.fromJson]. + /// + /// The scope strings will be separated by the provided [delimiter]. This + /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) + /// use non-standard delimiters. + /// + /// By default, this follows the OAuth2 spec and requires the server's + /// responses to be in JSON format. However, some servers return non-standard + /// response formats, which can be parsed using the [getParameters] function. + /// + /// This function is passed the `Content-Type` header of the response as well + /// as its body as a UTF-8-decoded string. It should return a map in the same + /// format as the [standard JSON response][]. + /// + /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 + Credentials( + this.accessToken, { + this.refreshToken, + this.idToken, + this.tokenEndpoint, + Iterable? scopes, + this.expiration, + String? delimiter, + Map Function(MediaType? mediaType, String body)? + getParameters, + }) : scopes = UnmodifiableListView( + // Explicitly type-annotate the list literal to work around + // sdk#24202. + scopes == null ? [] : scopes.toList(), + ), + _delimiter = delimiter ?? ' ', + _getParameters = getParameters ?? parseJsonParameters; + + /// Loads a set of credentials from a JSON-serialized form. + /// + /// Throws a [FormatException] if the JSON is incorrectly formatted. + factory Credentials.fromJson(String json) { + void validate(bool condition, message) { + if (condition) return; + throw FormatException('Failed to load credentials: $message.\n\n$json'); + } + + dynamic parsed; + try { + parsed = jsonDecode(json); + } on FormatException { + validate(false, 'invalid JSON'); + } + + validate(parsed is Map, 'was not a JSON map'); + + parsed = parsed as Map; + validate( + parsed.containsKey('accessToken'), + 'did not contain required field "accessToken"', + ); + validate( + parsed['accessToken'] is String, + 'required field "accessToken" was not a string, was ' + '${parsed["accessToken"]}', + ); + + for (var stringField in ['refreshToken', 'idToken', 'tokenEndpoint']) { + var value = parsed[stringField]; + validate( + value == null || value is String, + 'field "$stringField" was not a string, was "$value"', + ); + } + + var scopes = parsed['scopes']; + validate( + scopes == null || scopes is List, + 'field "scopes" was not a list, was "$scopes"', + ); + + var tokenEndpoint = parsed['tokenEndpoint']; + Uri? tokenEndpointUri; + if (tokenEndpoint != null) { + tokenEndpointUri = Uri.parse(tokenEndpoint as String); + } + + var expiration = parsed['expiration']; + DateTime? expirationDateTime; + if (expiration != null) { + validate( + expiration is int, + 'field "expiration" was not an int, was "$expiration"', + ); + expiration = expiration as int; + expirationDateTime = DateTime.fromMillisecondsSinceEpoch(expiration); + } + + return Credentials( + parsed['accessToken'] as String, + refreshToken: parsed['refreshToken'] as String?, + idToken: parsed['idToken'] as String?, + tokenEndpoint: tokenEndpointUri, + scopes: (scopes as List).map((scope) => scope as String), + expiration: expirationDateTime, + ); + } + + /// Serializes a set of credentials to JSON. + /// + /// Nothing is guaranteed about the output except that it's valid JSON and + /// compatible with [Credentials.toJson]. + String toJson() => jsonEncode({ + 'accessToken': accessToken, + 'refreshToken': refreshToken, + 'idToken': idToken, + 'tokenEndpoint': tokenEndpoint?.toString(), + 'scopes': scopes, + 'expiration': expiration?.millisecondsSinceEpoch, + }); + + /// Returns a new set of refreshed credentials. + /// + /// See [_Client.identifier] and [_Client.secret] for explanations of those + /// parameters. + /// + /// You may request different scopes than the default by passing in + /// [newScopes]. These must be a subset of [scopes]. + /// + /// This throws an [ArgumentError] if [secret] is passed without [identifier], + /// a [StateError] if these credentials can't be refreshed, an + /// [_AuthorizationException] if refreshing the credentials fails, or a + /// [FormatException] if the authorization server returns invalid responses. + Future refresh({ + String? identifier, + String? secret, + Iterable? newScopes, + bool basicAuth = true, + http.Client? httpClient, + }) async { + var scopes = this.scopes; + if (newScopes != null) scopes = newScopes.toList(); + scopes ??= []; + httpClient ??= http.Client(); + + if (identifier == null && secret != null) { + throw ArgumentError('secret may not be passed without identifier.'); + } + + var startTime = DateTime.now(); + var tokenEndpoint = this.tokenEndpoint; + if (refreshToken == null) { + throw StateError("Can't refresh credentials without a refresh " + 'token.'); + } else if (tokenEndpoint == null) { + throw StateError("Can't refresh credentials without a token " + 'endpoint.'); + } + + var headers = {}; + + var body = {'grant_type': 'refresh_token', 'refresh_token': refreshToken}; + if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter); + + if (basicAuth && secret != null) { + headers['Authorization'] = _basicAuthHeader(identifier!, secret); + } else { + if (identifier != null) body['client_id'] = identifier; + if (secret != null) body['client_secret'] = secret; + } + + var response = + await httpClient.post(tokenEndpoint, headers: headers, body: body); + var credentials = _handleAccessTokenResponse( + response, + tokenEndpoint, + startTime, + scopes, + _delimiter, + getParameters: _getParameters, + ); + + // The authorization server may issue a new refresh token. If it doesn't, + // we should re-use the one we already have. + if (credentials.refreshToken != null) return credentials; + return Credentials( + credentials.accessToken, + refreshToken: refreshToken, + idToken: credentials.idToken, + tokenEndpoint: credentials.tokenEndpoint, + scopes: credentials.scopes, + expiration: credentials.expiration, + ); + } +} + +/// An exception raised when attempting to use expired OAuth2 credentials. +class _ExpirationException implements Exception { + /// The expired credentials. + final Credentials credentials; + + /// Creates an ExpirationException. + _ExpirationException(this.credentials); + + /// Provides a string description of the ExpirationException. + @override + String toString() => + "OAuth2 credentials have expired and can't be refreshed."; +} + +/// The amount of time to add as a "grace period" for credential expiration. +/// +/// This allows credential expiration checks to remain valid for a reasonable +/// amount of time. +const _expirationGrace = Duration(seconds: 10); + +/// Handles a response from the authorization server that contains an access +/// token. +/// +/// This response format is common across several different components of the +/// OAuth2 flow. +/// +/// By default, this follows the OAuth2 spec and requires the server's responses +/// to be in JSON format. However, some servers return non-standard response +/// formats, which can be parsed using the [getParameters] function. +/// +/// This function is passed the `Content-Type` header of the response as well as +/// its body as a UTF-8-decoded string. It should return a map in the same +/// format as the [standard JSON response][]. +/// +/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 +Credentials _handleAccessTokenResponse( + http.Response response, + Uri tokenEndpoint, + DateTime startTime, + List? scopes, + String delimiter, { + Map Function(MediaType? contentType, String body)? + getParameters, +}) { + getParameters ??= parseJsonParameters; + + try { + if (response.statusCode != 200) { + _handleErrorResponse(response, tokenEndpoint, getParameters); + } + + var contentTypeString = response.headers['content-type']; + if (contentTypeString == null) { + throw const FormatException('Missing Content-Type string.'); + } + + var parameters = + getParameters(MediaType.parse(contentTypeString), response.body); + + for (var requiredParameter in ['access_token', 'token_type']) { + if (!parameters.containsKey(requiredParameter)) { + throw FormatException( + 'did not contain required parameter "$requiredParameter"', + ); + } else if (parameters[requiredParameter] is! String) { + throw FormatException( + 'required parameter "$requiredParameter" was not a string, was ' + '"${parameters[requiredParameter]}"'); + } + } + + // TODO(nweiz): support the "mac" token type + // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01) + if ((parameters['token_type'] as String).toLowerCase() != 'bearer') { + throw FormatException( + '"$tokenEndpoint": unknown token type "${parameters['token_type']}"', + ); + } + + var expiresIn = parameters['expires_in']; + if (expiresIn != null) { + if (expiresIn is String) { + try { + expiresIn = double.parse(expiresIn).toInt(); + } on FormatException { + throw FormatException( + 'parameter "expires_in" could not be parsed as in, was: "$expiresIn"', + ); + } + } else if (expiresIn is! int) { + throw FormatException( + 'parameter "expires_in" was not an int, was: "$expiresIn"', + ); + } + } + + for (var name in ['refresh_token', 'id_token', 'scope']) { + var value = parameters[name]; + if (value != null && value is! String) { + throw FormatException( + 'parameter "$name" was not a string, was "$value"', + ); + } + } + + var scope = parameters['scope'] as String?; + if (scope != null) scopes = scope.split(delimiter); + + var expiration = expiresIn == null + ? null + : startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace); + + return Credentials( + parameters['access_token'] as String, + refreshToken: parameters['refresh_token'] as String?, + idToken: parameters['id_token'] as String?, + tokenEndpoint: tokenEndpoint, + scopes: scopes, + expiration: expiration, + ); + } on FormatException catch (e) { + throw FormatException('Invalid OAuth response for "$tokenEndpoint": ' + '${e.message}.\n\n${response.body}'); + } +} + +/// Throws the appropriate exception for an error response from the +/// authorization server. +void _handleErrorResponse( + http.Response response, + Uri tokenEndpoint, + _GetParameters getParameters, +) { + // OAuth2 mandates a 400 or 401 response code for access token error + // responses. If it's not a 400 reponse, the server is either broken or + // off-spec. + if (response.statusCode != 400 && response.statusCode != 401) { + var reason = ''; + var reasonPhrase = response.reasonPhrase; + if (reasonPhrase != null && reasonPhrase.isNotEmpty) { + reason = ' $reasonPhrase'; + } + throw FormatException('OAuth request for "$tokenEndpoint" failed ' + 'with status ${response.statusCode}$reason.\n\n${response.body}'); + } + + var contentTypeString = response.headers['content-type']; + var contentType = + contentTypeString == null ? null : MediaType.parse(contentTypeString); + + var parameters = getParameters(contentType, response.body); + + if (!parameters.containsKey('error')) { + throw const FormatException('did not contain required parameter "error"'); + } else if (parameters['error'] is! String) { + throw FormatException('required parameter "error" was not a string, was ' + '"${parameters["error"]}"'); + } + + for (var name in ['error_description', 'error_uri']) { + var value = parameters[name]; + + if (value != null && value is! String) { + throw FormatException('parameter "$name" was not a string, was "$value"'); + } + } + + var uriString = parameters['error_uri'] as String?; + var uri = uriString == null ? null : Uri.parse(uriString); + var description = parameters['error_description'] as String?; + throw _AuthorizationException( + parameters['error'] as String, + description, + uri, + ); +} + +/// The type of a callback that parses parameters from an HTTP response. +typedef _GetParameters = Map Function( + MediaType? contentType, + String body, +); + +/// Parses parameters from a response with a JSON body, as per the +/// [OAuth2 spec][]. +/// +/// [OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-5.1 +Map parseJsonParameters(MediaType? contentType, String body) { + // The spec requires a content-type of application/json, but some endpoints + // (e.g. Dropbox) serve it as text/javascript instead. + if (contentType == null || + (contentType.mimeType != 'application/json' && + contentType.mimeType != 'text/javascript')) { + throw FormatException( + 'Content-Type was "$contentType", expected "application/json"', + ); + } + + var untypedParameters = jsonDecode(body); + if (untypedParameters is Map) { + return untypedParameters; + } + + throw FormatException('Parameters must be a map, was "$untypedParameters"'); +} + +/// Adds additional query parameters to [url], overwriting the original +/// parameters if a name conflict occurs. +Uri _addQueryParameters(Uri url, Map parameters) => url.replace( + queryParameters: Map.from(url.queryParameters)..addAll(parameters), + ); + +String _basicAuthHeader(String identifier, String secret) { + var userPass = '${Uri.encodeFull(identifier)}:${Uri.encodeFull(secret)}'; + return 'Basic ${base64Encode(ascii.encode(userPass))}'; +} diff --git a/lib/src/package.dart b/lib/src/package.dart index fa7399ab4..8058dc3bb 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart @@ -116,13 +116,14 @@ class Package { /// Loads the package whose root directory is [packageDir]. /// - /// [expectedName] is the expected name of that package (e.g. the name given - /// in the dependency), or `null` if the package being loaded is the - /// entrypoint package. + /// [name] is the expected name of that package (e.g. the name given in the + /// dependency), or `null` if the package being loaded is the entrypoint + /// package. /// /// `pubspec_overrides.yaml` is only loaded if [withPubspecOverrides] is /// `true`. factory Package.load( + String? name, String dir, SourceRegistry sources, { bool withPubspecOverrides = false, @@ -130,6 +131,7 @@ class Package { final pubspec = Pubspec.load( dir, sources, + expectedName: name, allowOverridesFile: withPubspecOverrides, ); return Package._(dir, pubspec); @@ -202,8 +204,8 @@ class Package { /// For each directory a .pubignore takes precedence over a .gitignore. /// /// Note that the returned paths will be always be below [dir], and will - /// always start with [dir] (thus alway be relative to current working - /// directory or absolute id [dir] is absolute. + /// always start with [dir] (thus always be relative to the current working + /// directory) or absolute id [dir] is absolute. /// /// To convert them to paths relative to the package root, use [p.relative]. List listFiles({String? beneath, bool recursive = true}) { @@ -275,16 +277,16 @@ class Package { '$ignoreFile had invalid pattern $pattern. ${exception.message}', ); }, - // Ignore case on MacOs and Windows, because `git clone` and + // Ignore case on macOS and Windows, because `git clone` and // `git init` will set `core.ignoreCase = true` in the local // local `.git/config` file for the repository. // - // So on Windows and MacOS most users will have case-insensitive + // So on Windows and macOS most users will have case-insensitive // behavior with `.gitignore`, hence, it seems reasonable to do // the same when we interpret `.gitignore` and `.pubignore`. // // There are cases where a user may have case-sensitive behavior - // with `.gitignore` on Windows and MacOS: + // with `.gitignore` on Windows and macOS: // // (A) The user has manually overwritten the repository // configuration setting `core.ignoreCase = false`. @@ -303,7 +305,7 @@ class Package { // > if appropriate when the repository is created. // // In either case, it seems likely that users on Windows and - // MacOS will prefer case-insensitive matching. We specifically + // macOS will prefer case-insensitive matching. We specifically // know that some tooling will generate `.PDB` files instead of // `.pdb`, see: [#3003][2] // diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index f1ec4729d..f8bfe8ca5 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart @@ -207,11 +207,15 @@ class Pubspec extends PubspecBase { /// Loads the pubspec for a package located in [packageDir]. /// + /// If [expectedName] is passed and the pubspec doesn't have a matching name + /// field, this will throw a [SourceSpanApplicationException]. + /// /// If [allowOverridesFile] is `true` [pubspecOverridesFilename] is loaded and /// is allowed to override dependency_overrides from `pubspec.yaml`. factory Pubspec.load( String packageDir, SourceRegistry sources, { + String? expectedName, bool allowOverridesFile = false, }) { var pubspecPath = path.join(packageDir, pubspecYamlFilename); @@ -233,6 +237,7 @@ class Pubspec extends PubspecBase { return Pubspec.parse( readTextFile(pubspecPath), sources, + expectedName: expectedName, location: path.toUri(pubspecPath), overridesFileContents: overridesFileContents, overridesLocation: path.toUri(overridesPath), @@ -273,11 +278,15 @@ class Pubspec extends PubspecBase { /// Returns a Pubspec object for an already-parsed map representing its /// contents. /// + /// If [expectedName] is passed and the pubspec doesn't have a matching name + /// field, this will throw a [PubspecError]. + /// /// [location] is the location from which this pubspec was loaded. Pubspec.fromMap( Map fields, this._sources, { YamlMap? overridesFields, + String? expectedName, Uri? location, }) : _overridesFileFields = overridesFields, _includeDefaultSdkConstraint = true, @@ -288,7 +297,18 @@ class Pubspec extends PubspecBase { fields is YamlMap ? fields : YamlMap.wrap(fields, sourceUrl: location), - ); + ) { + // If [expectedName] is passed, ensure that the actual 'name' field exists + // and matches the expectation. + if (expectedName == null) return; + if (name == expectedName) return; + + throw SourceSpanApplicationException( + '"name" field doesn\'t match expected name ' + '"$expectedName".', + this.fields.nodes['name']!.span, + ); + } /// Parses the pubspec stored at [location] whose text is [contents]. /// @@ -297,6 +317,7 @@ class Pubspec extends PubspecBase { factory Pubspec.parse( String contents, SourceRegistry sources, { + String? expectedName, Uri? location, String? overridesFileContents, Uri? overridesLocation, @@ -318,6 +339,7 @@ class Pubspec extends PubspecBase { pubspecMap, sources, overridesFields: overridesFileMap, + expectedName: expectedName, location: location, ); } diff --git a/lib/src/pubspec_parse.dart b/lib/src/pubspec_parse.dart index d76a049b3..3ea5f88e0 100644 --- a/lib/src/pubspec_parse.dart +++ b/lib/src/pubspec_parse.dart @@ -182,7 +182,7 @@ abstract class PubspecBase { /// that will be placed on the user's PATH. The value is the name of the /// .dart script (without extension) in the package's `bin` directory that /// should be run for that command. Both key and value must be "simple" - /// strings: alphanumerics, underscores and hypens only. If a value is + /// strings: alphanumerics, underscores and hyphens only. If a value is /// omitted, it is inferred to use the same name as the key. Map get executables { if (_executables != null) return _executables!; diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index 6322fb4ce..fb8f2e584 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart @@ -4,9 +4,7 @@ import 'package:collection/collection.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml/yaml.dart'; -import '../exceptions.dart'; import '../http.dart'; import '../io.dart'; import '../lock_file.dart'; @@ -78,7 +76,6 @@ class SolveResult { return id; }), ); - await _validatePubspecNames(cache); // Invariant: the content-hashes in PUB_CACHE matches those provided by the // server. @@ -110,39 +107,6 @@ class SolveResult { ); } - /// Validate that all dependencies in [packages] refer to a package that has the - /// expected name. - /// - /// Throws an ApplicationException if there is a mismatch. - Future _validatePubspecNames( - SystemCache cache, - ) async { - for (final id in packages) { - final pubspec = pubspecs[id.name]!; - final validatedNames = {}; - for (final dependency in [ - ...pubspec.dependencies.keys, - if (id.isRoot) ...pubspec.devDependencies.keys, - ]) { - if (!validatedNames.add(dependency)) continue; - final dependencyPubspec = pubspecs[dependency]!; - - if (dependencyPubspec.name != dependency) { - // Find the span for the reference. - final key = pubspec.dependencies.containsKey(dependency) - ? 'dependencies' - : 'dev_dependencies'; - final dependencyNode = (pubspec.fields.nodes[key] as YamlMap) - .nodes[dependency] as YamlNode; - throw SourceSpanApplicationException( - 'Expected to find package "${log.bold(dependency)}", found package "${log.bold(dependencyPubspec.name)}".', - dependencyNode.span, - ); - } - } - } - } - final LockFile _previousLockFile; /// Returns the names of all packages that were changed. @@ -215,7 +179,7 @@ class SolveResult { category: 'pub-get', ); log.fine( - 'Sending analytics timing "pub-get" took ${resolutionTime.inMilliseconds} miliseconds', + 'Sending analytics timing "pub-get" took ${resolutionTime.inMilliseconds} milliseconds', ); } diff --git a/lib/src/solver/solve_suggestions.dart b/lib/src/solver/solve_suggestions.dart index 68a6d4306..2431b7525 100644 --- a/lib/src/solver/solve_suggestions.dart +++ b/lib/src/solver/solve_suggestions.dart @@ -17,8 +17,8 @@ import '../system_cache.dart'; import 'incompatibility.dart'; import 'incompatibility_cause.dart'; -/// Looks through the root-[incompability] of a solve-failure and tries to see if -/// the conflict could resolved by any of the following suggestions: +/// Looks through the root-[incompatibility] of a solve-failure and tries to see +/// if the conflict could resolved by any of the following suggestions: /// * An update of the current SDK. /// * Any single change to a package constraint. /// * Removing the bounds on all constraints, changing less than 5 dependencies. @@ -144,7 +144,7 @@ class _ResolutionContext { final constraint = cause.constraint; if (constraint == null) return null; - /// Find the most relevant Flutter release fullfilling the constraint. + // Find the most relevant Flutter release fulfilling the constraint. final bestRelease = await inferBestFlutterRelease({cause.sdk.identifier: constraint}); if (bestRelease == null) return null; diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart index b57f3b530..933b08c9a 100644 --- a/lib/src/source/cached.dart +++ b/lib/src/source/cached.dart @@ -29,7 +29,7 @@ abstract class CachedSource extends Source { Future doDescribe(PackageId id, SystemCache cache) async { var packageDir = getDirectoryInCache(id, cache); if (fileExists(path.join(packageDir, 'pubspec.yaml'))) { - return Pubspec.load(packageDir, cache.sources); + return Pubspec.load(packageDir, cache.sources, expectedName: id.name); } return await describeUncached(id, cache); diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart index bc5470bc1..367f7a92d 100644 --- a/lib/src/source/git.dart +++ b/lib/src/source/git.dart @@ -336,6 +336,7 @@ class GitSource extends CachedSource { cache, ), cache.sources, + expectedName: ref.name, ); } @@ -427,7 +428,7 @@ class GitSource extends CachedSource { var packageDir = p.join(revisionCachePath, relative); try { - return Package.load(packageDir, cache.sources); + return Package.load(null, packageDir, cache.sources); } catch (error, stackTrace) { log.error('Failed to load package', error, stackTrace); var name = p.basename(revisionCachePath).split('-').first; diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index f0c230d39..e99ee523a 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -396,6 +396,7 @@ class HostedSource extends CachedSource { var pubspec = Pubspec.fromMap( pubspecData, cache.sources, + expectedName: ref.name, location: location, ); final archiveSha256 = map['archive_sha256']; @@ -944,8 +945,9 @@ class HostedSource extends CachedSource { url = _directoryToUrl(directory); } on FormatException { log.error('Unable to detect hosted url from directory: $directory'); - // If _directoryToUrl can't intepret a directory name, we just silently - // ignore it and hope it's because it comes from a newer version of pub. + // If _directoryToUrl can't interpret a directory name, + // we just silently ignore it and hope it's because it + // comes from a newer version of pub. // // This is most likely because someone manually modified PUB_CACHE. return []; @@ -955,7 +957,7 @@ class HostedSource extends CachedSource { var packages = []; for (var entry in listDir(serverDir)) { try { - packages.add(Package.load(entry, cache.sources)); + packages.add(Package.load(null, entry, cache.sources)); } catch (error, stackTrace) { log.error('Failed to load package', error, stackTrace); final id = _idForBasename( @@ -1062,7 +1064,7 @@ class HostedSource extends CachedSource { .where(_looksLikePackageDir) .map((entry) { try { - return Package.load(entry, cache.sources); + return Package.load(null, entry, cache.sources); } catch (error, stackTrace) { log.fine('Failed to load package from $entry:\n' '$error\n' diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart index e2b3ef993..f881b8e9e 100644 --- a/lib/src/source/path.dart +++ b/lib/src/source/path.dart @@ -168,7 +168,7 @@ class PathSource extends Source { throw ArgumentError('Wrong source'); } var dir = _validatePath(ref.name, description); - return Pubspec.load(dir, cache.sources); + return Pubspec.load(dir, cache.sources, expectedName: ref.name); } @override diff --git a/lib/src/source/sdk.dart b/lib/src/source/sdk.dart index 3b391f68d..d37c13ef9 100644 --- a/lib/src/source/sdk.dart +++ b/lib/src/source/sdk.dart @@ -89,8 +89,11 @@ class SdkSource extends Source { /// /// Throws a [PackageNotFoundException] if [ref]'s SDK is unavailable or /// doesn't contain the package. - Pubspec _loadPubspec(PackageRef ref, SystemCache cache) => - Pubspec.load(_verifiedPackagePath(ref), cache.sources); + Pubspec _loadPubspec(PackageRef ref, SystemCache cache) => Pubspec.load( + _verifiedPackagePath(ref), + cache.sources, + expectedName: ref.name, + ); /// Returns the path for the given [ref]. /// diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart index 10145febf..ff2b53537 100644 --- a/lib/src/system_cache.dart +++ b/lib/src/system_cache.dart @@ -112,13 +112,17 @@ Consider setting the `PUB_CACHE` variable manually. /// /// Throws an [ArgumentError] if [id] has an invalid source. Package load(PackageId id) { - return Package.load(getDirectory(id), sources); + return Package.load(id.name, getDirectory(id), sources); } Package loadCached(PackageId id) { final source = id.description.description.source; if (source is CachedSource) { - return Package.load(source.getDirectoryInCache(id, this), sources); + return Package.load( + id.name, + source.getDirectoryInCache(id, this), + sources, + ); } else { throw ArgumentError('Call only on Cached ids.'); } diff --git a/lib/src/third_party/oauth2/CHANGELOG.md b/lib/src/third_party/oauth2/CHANGELOG.md deleted file mode 100644 index 0c0deb205..000000000 --- a/lib/src/third_party/oauth2/CHANGELOG.md +++ /dev/null @@ -1,123 +0,0 @@ -# 2.0.1 - -* Handle `expires_in` when encoded as string. -* Populate the pubspec `repository` field. -* Increase the minimum Dart SDK to `2.17.0`. - -# 2.0.0 - -* Migrate to null safety. - -# 1.6.3 - -* Added optional `codeVerifier` parameter to `AuthorizationCodeGrant` constructor. - -# 1.6.1 - -* Added fix to make sure that credentials are only refreshed once when multiple calls are made. - -# 1.6.0 - -* Added PKCE support to `AuthorizationCodeGrant`. - -# 1.5.0 - -* Added support for `clientCredentialsGrant`. - -# 1.4.0 - -* OpenID's id_token treated. - -# 1.3.0 - -* Added `onCredentialsRefreshed` option when creating `Client` objects. - -# 1.2.3 - -* Support the latest `package:http` release. - -# 1.2.2 - -* Allow the stable 2.0 SDK. - -# 1.2.1 - -* Updated SDK version to 2.0.0-dev.17.0 - -# 1.2.0 - -* Add a `getParameter()` parameter to `new AuthorizationCodeGrant()`, `new - Credentials()`, and `resourceOwnerPasswordGrant()`. This controls how the - authorization server's response is parsed for servers that don't provide the - standard JSON response. - -# 1.1.1 - -* `resourceOwnerPasswordGrant()` now properly uses its HTTP client for requests - made by the OAuth2 client it returns. - -# 1.1.0 - -* Add a `delimiter` parameter to `new AuthorizationCodeGrant()`, `new - Credentials()`, and `resourceOwnerPasswordGrant()`. This controls the - delimiter between scopes, which some authorization servers require to be - different values than the specified `' '`. - -# 1.0.2 - -* Fix all strong-mode warnings. - -* Support `crypto` 1.0.0. - -* Support `http_parser` 3.0.0. - -# 1.0.1 - -* Support `http_parser` 2.0.0. - -# 1.0.0 - -## Breaking changes - -* Requests that use client authentication, such as the - `AuthorizationCodeGrant`'s access token request and `Credentials`' refresh - request, now use HTTP Basic authentication by default. This form of - authentication is strongly recommended by the OAuth 2.0 spec. The new - `basicAuth` parameter may be set to `false` to force form-based authentication - for servers that require it. - -* `new AuthorizationCodeGrant()` now takes `secret` as an optional named - argument rather than a required argument. This matches the OAuth 2.0 spec, - which says that a client secret is only required for confidential clients. - -* `new Client()` and `Credentials.refresh()` now take both `identifier` and - `secret` as optional named arguments rather than required arguments. This - matches the OAuth 2.0 spec, which says that the server may choose not to - require client authentication for some flows. - -* `new Credentials()` now takes named arguments rather than optional positional - arguments. - -## Non-breaking changes - -* Added a `resourceOwnerPasswordGrant` method. - -* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` and - `new Credentials()` and the `newScopes` argument to `Credentials.refresh` now - take an `Iterable` rather than just a `List`. - -* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` now - defaults to `null` rather than `const []`. - -# 0.9.3 - -* Update the `http` dependency. - -* Since `http` 0.11.0 now works in non-`dart:io` contexts, `oauth2` does as - well. - -# 0.9.2 - -* Expand the dependency on the HTTP package to include 0.10.x. - -* Add a README file. diff --git a/lib/src/third_party/oauth2/LICENSE b/lib/src/third_party/oauth2/LICENSE deleted file mode 100644 index 162572a44..000000000 --- a/lib/src/third_party/oauth2/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright 2014, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/src/third_party/oauth2/README.md b/lib/src/third_party/oauth2/README.md deleted file mode 100644 index 196b9f789..000000000 --- a/lib/src/third_party/oauth2/README.md +++ /dev/null @@ -1,260 +0,0 @@ -[![Dart CI](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml) -[![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2) -[![package publisher](https://img.shields.io/pub/publisher/oauth2.svg)](https://pub.dev/packages/oauth2/publisher) - -A client library for authenticating with a remote service via OAuth2 on behalf -of a user, and making authorized HTTP requests with the user's OAuth2 -credentials. - -## About OAuth2 - -OAuth2 allows a client (the program using this library) to access and manipulate -a resource that's owned by a resource owner (the end user) and lives on a remote -server. The client directs the resource owner to an authorization server -(usually but not always the same as the server that hosts the resource), where -the resource owner tells the authorization server to give the client an access -token. This token serves as proof that the client has permission to access -resources on behalf of the resource owner. - -OAuth2 provides several different methods for the client to obtain -authorization. At the time of writing, this library only supports the -[Authorization Code Grant][authorizationCodeGrantSection], -[Client Credentials Grant][clientCredentialsGrantSection] and -[Resource Owner Password Grant][resourceOwnerPasswordGrantSection] flows, but -more may be added in the future. - -## Authorization Code Grant - -**Resources:** [Class summary][authorizationCodeGrantMethod], -[OAuth documentation][authorizationCodeGrantDocs] - -```dart -import 'dart:io'; - -import 'package:oauth2/oauth2.dart' as oauth2; - -// These URLs are endpoints that are provided by the authorization -// server. They're usually included in the server's documentation of its -// OAuth2 API. -final authorizationEndpoint = - Uri.parse('http://example.com/oauth2/authorization'); -final tokenEndpoint = Uri.parse('http://example.com/oauth2/token'); - -// The authorization server will issue each client a separate client -// identifier and secret, which allows the server to tell which client -// is accessing it. Some servers may also have an anonymous -// identifier/secret pair that any client may use. -// -// Note that clients whose source code or binary executable is readily -// available may not be able to make sure the client secret is kept a -// secret. This is fine; OAuth2 servers generally won't rely on knowing -// with certainty that a client is who it claims to be. -final identifier = 'my client identifier'; -final secret = 'my client secret'; - -// This is a URL on your application's server. The authorization server -// will redirect the resource owner here once they've authorized the -// client. The redirection will include the authorization code in the -// query parameters. -final redirectUrl = Uri.parse('http://my-site.com/oauth2-redirect'); - -/// A file in which the users credentials are stored persistently. If the server -/// issues a refresh token allowing the client to refresh outdated credentials, -/// these may be valid indefinitely, meaning the user never has to -/// re-authenticate. -final credentialsFile = File('~/.myapp/credentials.json'); - -/// Either load an OAuth2 client from saved credentials or authenticate a new -/// one. -Future createClient() async { - var exists = await credentialsFile.exists(); - - // If the OAuth2 credentials have already been saved from a previous run, we - // just want to reload them. - if (exists) { - var credentials = - oauth2.Credentials.fromJson(await credentialsFile.readAsString()); - return oauth2.Client(credentials, identifier: identifier, secret: secret); - } - - // If we don't have OAuth2 credentials yet, we need to get the resource owner - // to authorize us. We're assuming here that we're a command-line application. - var grant = oauth2.AuthorizationCodeGrant( - identifier, authorizationEndpoint, tokenEndpoint, - secret: secret); - - // A URL on the authorization server (authorizationEndpoint with some additional - // query parameters). Scopes and state can optionally be passed into this method. - var authorizationUrl = grant.getAuthorizationUrl(redirectUrl); - - // Redirect the resource owner to the authorization URL. Once the resource - // owner has authorized, they'll be redirected to `redirectUrl` with an - // authorization code. The `redirect` should cause the browser to redirect to - // another URL which should also have a listener. - // - // `redirect` and `listen` are not shown implemented here. See below for the - // details. - await redirect(authorizationUrl); - var responseUrl = await listen(redirectUrl); - - // Once the user is redirected to `redirectUrl`, pass the query parameters to - // the AuthorizationCodeGrant. It will validate them and extract the - // authorization code to create a new Client. - return await grant.handleAuthorizationResponse(responseUrl.queryParameters); -} - -void main() async { - var client = await createClient(); - - // Once you have a Client, you can use it just like any other HTTP client. - print(await client.read('http://example.com/protected-resources.txt')); - - // Once we're done with the client, save the credentials file. This ensures - // that if the credentials were automatically refreshed while using the - // client, the new credentials are available for the next run of the - // program. - await credentialsFile.writeAsString(client.credentials.toJson()); -} -``` - -
- Click here to learn how to implement `redirect` and `listen`. - --------------------------------------------------------------------------------- - -There is not a universal example for implementing `redirect` and `listen`, -because different options exist for each platform. - -For Flutter apps, there's two popular approaches: - -1. Launch a browser using [url_launcher][] and listen for a redirect using - [uni_links][]. - - ```dart - if (await canLaunch(authorizationUrl.toString())) { - await launch(authorizationUrl.toString()); } - - // ------- 8< ------- - - final linksStream = getLinksStream().listen((Uri uri) async { - if (uri.toString().startsWith(redirectUrl)) { - responseUrl = uri; - } - }); - ``` - -1. Launch a WebView inside the app and listen for a redirect using - [webview_flutter][]. - - ```dart - WebView( - javascriptMode: JavascriptMode.unrestricted, - initialUrl: authorizationUrl.toString(), - navigationDelegate: (navReq) { - if (navReq.url.startsWith(redirectUrl)) { - responseUrl = Uri.parse(navReq.url); - return NavigationDecision.prevent; - } - return NavigationDecision.navigate; - }, - // ------- 8< ------- - ); - ``` - -For Dart apps, the best approach depends on the available options for accessing -a browser. In general, you'll need to launch the authorization URL through the -client's browser and listen for the redirect URL. -
- -## Client Credentials Grant - -**Resources:** [Method summary][clientCredentialsGrantMethod], -[OAuth documentation][clientCredentialsGrantDocs] - -```dart -// This URL is an endpoint that's provided by the authorization server. It's -// usually included in the server's documentation of its OAuth2 API. -final authorizationEndpoint = - Uri.parse('http://example.com/oauth2/authorization'); - -// The OAuth2 specification expects a client's identifier and secret -// to be sent when using the client credentials grant. -// -// Because the client credentials grant is not inherently associated with a user, -// it is up to the server in question whether the returned token allows limited -// API access. -// -// Either way, you must provide both a client identifier and a client secret: -final identifier = 'my client identifier'; -final secret = 'my client secret'; - -// Calling the top-level `clientCredentialsGrant` function will return a -// [Client] instead. -var client = await oauth2.clientCredentialsGrant( - authorizationEndpoint, identifier, secret); - -// With an authenticated client, you can make requests, and the `Bearer` token -// returned by the server during the client credentials grant will be attached -// to any request you make. -var response = - await client.read('https://example.com/api/some_resource.json'); - -// You can save the client's credentials, which consists of an access token, and -// potentially a refresh token and expiry date, to a file. This way, subsequent runs -// do not need to reauthenticate, and you can avoid saving the client identifier and -// secret. -await credentialsFile.writeAsString(client.credentials.toJson()); -``` - -## Resource Owner Password Grant - -**Resources:** [Method summary][resourceOwnerPasswordGrantMethod], -[OAuth documentation][resourceOwnerPasswordGrantDocs] - -```dart -// This URL is an endpoint that's provided by the authorization server. It's -// usually included in the server's documentation of its OAuth2 API. -final authorizationEndpoint = - Uri.parse('http://example.com/oauth2/authorization'); - -// The user should supply their own username and password. -final username = 'example user'; -final password = 'example password'; - -// The authorization server may issue each client a separate client -// identifier and secret, which allows the server to tell which client -// is accessing it. Some servers may also have an anonymous -// identifier/secret pair that any client may use. -// -// Some servers don't require the client to authenticate itself, in which case -// these should be omitted. -final identifier = 'my client identifier'; -final secret = 'my client secret'; - -// Make a request to the authorization endpoint that will produce the fully -// authenticated Client. -var client = await oauth2.resourceOwnerPasswordGrant( - authorizationEndpoint, username, password, - identifier: identifier, secret: secret); - -// Once you have the client, you can use it just like any other HTTP client. -var result = await client.read('http://example.com/protected-resources.txt'); - -// Once we're done with the client, save the credentials file. This will allow -// us to re-use the credentials and avoid storing the username and password -// directly. -File('~/.myapp/credentials.json').writeAsString(client.credentials.toJson()); -``` - -[authorizationCodeGrantDocs]: https://oauth.net/2/grant-types/authorization-code/ -[authorizationCodeGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/AuthorizationCodeGrant-class.html -[authorizationCodeGrantSection]: #authorization-code-grant -[clientCredentialsGrantDocs]: https://oauth.net/2/grant-types/client-credentials/ -[clientCredentialsGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/clientCredentialsGrant.html -[clientCredentialsGrantSection]: #client-credentials-grant -[resourceOwnerPasswordGrantDocs]: https://oauth.net/2/grant-types/password/ -[resourceOwnerPasswordGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/resourceOwnerPasswordGrant.html -[resourceOwnerPasswordGrantSection]: #resource-owner-password-grant -[uni_links]: https://pub.dev/packages/uni_links -[url_launcher]: https://pub.dev/packages/url_launcher -[webview_flutter]: https://pub.dev/packages/webview_flutter diff --git a/lib/src/third_party/oauth2/analysis_options.yaml b/lib/src/third_party/oauth2/analysis_options.yaml deleted file mode 100644 index c8bc59c2c..000000000 --- a/lib/src/third_party/oauth2/analysis_options.yaml +++ /dev/null @@ -1,40 +0,0 @@ -include: package:lints/recommended.yaml - -analyzer: - language: - strict-casts: true - strict-raw-types: true - -linter: - rules: - - always_declare_return_types - - avoid_catching_errors - - avoid_dynamic_calls - - avoid_private_typedef_functions - - avoid_unused_constructor_parameters - - avoid_void_async - - cancel_subscriptions - - directives_ordering - - literal_only_boolean_expressions - - no_adjacent_strings_in_list - - no_runtimeType_toString - - omit_local_variable_types - - only_throw_errors - - package_api_docs - - prefer_asserts_in_initializer_lists - - prefer_const_constructors - - prefer_const_declarations - - prefer_relative_imports - - prefer_single_quotes - - sort_pub_dependencies - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - unawaited_futures - - unnecessary_await_in_return - - unnecessary_lambdas - - unnecessary_parenthesis - - unnecessary_statements - - use_is_even_rather_than_modulo - - use_string_buffers - - use_super_parameters diff --git a/lib/src/third_party/oauth2/lib/oauth2.dart b/lib/src/third_party/oauth2/lib/oauth2.dart deleted file mode 100644 index 45efc5c1b..000000000 --- a/lib/src/third_party/oauth2/lib/oauth2.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -export 'src/authorization_code_grant.dart'; -export 'src/authorization_exception.dart'; -export 'src/client.dart'; -export 'src/client_credentials_grant.dart'; -export 'src/credentials.dart'; -export 'src/expiration_exception.dart'; -export 'src/resource_owner_password_grant.dart'; diff --git a/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart b/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart deleted file mode 100644 index fac56ba0d..000000000 --- a/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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:convert'; -import 'dart:math'; - -import 'package:crypto/crypto.dart'; -import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'authorization_exception.dart'; -import 'client.dart'; -import 'credentials.dart'; -import 'handle_access_token_response.dart'; -import 'parameters.dart'; -import 'utils.dart'; - -/// A class for obtaining credentials via an [authorization code grant][]. -/// -/// This method of authorization involves sending the resource owner to the -/// authorization server where they will authorize the client. They're then -/// redirected back to your server, along with an authorization code. This is -/// used to obtain [Credentials] and create a fully-authorized [Client]. -/// -/// To use this class, you must first call [getAuthorizationUrl] to get the URL -/// to which to redirect the resource owner. Then once they've been redirected -/// back to your application, call [handleAuthorizationResponse] or -/// [handleAuthorizationCode] to process the authorization server's response and -/// construct a [Client]. -/// -/// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1 -class AuthorizationCodeGrant { - /// The function used to parse parameters from a host's response. - final GetParameters _getParameters; - - /// The client identifier for this client. - /// - /// The authorization server will issue each client a separate client - /// identifier and secret, which allows the server to tell which client is - /// accessing it. Some servers may also have an anonymous identifier/secret - /// pair that any client may use. - /// - /// This is usually global to the program using this library. - final String identifier; - - /// The client secret for this client. - /// - /// The authorization server will issue each client a separate client - /// identifier and secret, which allows the server to tell which client is - /// accessing it. Some servers may also have an anonymous identifier/secret - /// pair that any client may use. - /// - /// This is usually global to the program using this library. - /// - /// Note that clients whose source code or binary executable is readily - /// available may not be able to make sure the client secret is kept a secret. - /// This is fine; OAuth2 servers generally won't rely on knowing with - /// certainty that a client is who it claims to be. - final String? secret; - - /// A URL provided by the authorization server that serves as the base for the - /// URL that the resource owner will be redirected to to authorize this - /// client. - /// - /// This will usually be listed in the authorization server's OAuth2 API - /// documentation. - final Uri authorizationEndpoint; - - /// A URL provided by the authorization server that this library uses to - /// obtain long-lasting credentials. - /// - /// This will usually be listed in the authorization server's OAuth2 API - /// documentation. - final Uri tokenEndpoint; - - /// Callback to be invoked whenever the credentials are refreshed. - /// - /// This will be passed as-is to the constructed [Client]. - final CredentialsRefreshedCallback? _onCredentialsRefreshed; - - /// Whether to use HTTP Basic authentication for authorizing the client. - final bool _basicAuth; - - /// A [String] used to separate scopes; defaults to `" "`. - final String _delimiter; - - /// The HTTP client used to make HTTP requests. - http.Client? _httpClient; - - /// The URL to which the resource owner will be redirected after they - /// authorize this client with the authorization server. - Uri? _redirectEndpoint; - - /// The scopes that the client is requesting access to. - List? _scopes; - - /// An opaque string that users of this library may specify that will be - /// included in the response query parameters. - String? _stateString; - - /// The current state of the grant object. - _State _state = _State.initial; - - /// Allowed characters for generating the _codeVerifier - static const String _charset = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; - - /// The PKCE code verifier. Will be generated if one is not provided in the - /// constructor. - final String _codeVerifier; - - /// Creates a new grant. - /// - /// If [basicAuth] is `true` (the default), the client credentials are sent to - /// the server using using HTTP Basic authentication as defined in [RFC 2617]. - /// Otherwise, they're included in the request body. Note that the latter form - /// is not recommended by the OAuth 2.0 spec, and should only be used if the - /// server doesn't support Basic authentication. - /// - /// [RFC 2617]: https://tools.ietf.org/html/rfc2617 - /// - /// [httpClient] is used for all HTTP requests made by this grant, as well as - /// those of the [Client] is constructs. - /// - /// [onCredentialsRefreshed] will be called by the constructed [Client] - /// whenever the credentials are refreshed. - /// - /// [codeVerifier] String to be used as PKCE code verifier. If none is - /// provided a random codeVerifier will be generated. - /// The codeVerifier must meet requirements specified in [RFC 7636]. - /// - /// [RFC 7636]: https://tools.ietf.org/html/rfc7636#section-4.1 - /// - /// The scope strings will be separated by the provided [delimiter]. This - /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) - /// use non-standard delimiters. - /// - /// By default, this follows the OAuth2 spec and requires the server's - /// responses to be in JSON format. However, some servers return non-standard - /// response formats, which can be parsed using the [getParameters] function. - /// - /// This function is passed the `Content-Type` header of the response as well - /// as its body as a UTF-8-decoded string. It should return a map in the same - /// format as the [standard JSON response][]. - /// - /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 - AuthorizationCodeGrant( - this.identifier, this.authorizationEndpoint, this.tokenEndpoint, - {this.secret, - String? delimiter, - bool basicAuth = true, - http.Client? httpClient, - CredentialsRefreshedCallback? onCredentialsRefreshed, - Map Function(MediaType? contentType, String body)? - getParameters, - String? codeVerifier}) - : _basicAuth = basicAuth, - _httpClient = httpClient ?? http.Client(), - _delimiter = delimiter ?? ' ', - _getParameters = getParameters ?? parseJsonParameters, - _onCredentialsRefreshed = onCredentialsRefreshed, - _codeVerifier = codeVerifier ?? _createCodeVerifier(); - - /// Returns the URL to which the resource owner should be redirected to - /// authorize this client. - /// - /// The resource owner will then be redirected to [redirect], which should - /// point to a server controlled by the client. This redirect will have - /// additional query parameters that should be passed to - /// [handleAuthorizationResponse]. - /// - /// The specific permissions being requested from the authorization server may - /// be specified via [scopes]. The scope strings are specific to the - /// authorization server and may be found in its documentation. Note that you - /// may not be granted access to every scope you request; you may check the - /// [Credentials.scopes] field of [Client.credentials] to see which scopes you - /// were granted. - /// - /// An opaque [state] string may also be passed that will be present in the - /// query parameters provided to the redirect URL. - /// - /// It is a [StateError] to call this more than once. - Uri getAuthorizationUrl(Uri redirect, - {Iterable? scopes, String? state}) { - if (_state != _State.initial) { - throw StateError('The authorization URL has already been generated.'); - } - _state = _State.awaitingResponse; - - var scopeList = scopes?.toList() ?? []; - var codeChallenge = base64Url - .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes) - .replaceAll('=', ''); - - _redirectEndpoint = redirect; - _scopes = scopeList; - _stateString = state; - var parameters = { - 'response_type': 'code', - 'client_id': identifier, - 'redirect_uri': redirect.toString(), - 'code_challenge': codeChallenge, - 'code_challenge_method': 'S256' - }; - - if (state != null) parameters['state'] = state; - if (scopeList.isNotEmpty) parameters['scope'] = scopeList.join(_delimiter); - - return addQueryParameters(authorizationEndpoint, parameters); - } - - /// Processes the query parameters added to a redirect from the authorization - /// server. - /// - /// Note that this "response" is not an HTTP response, but rather the data - /// passed to a server controlled by the client as query parameters on the - /// redirect URL. - /// - /// It is a [StateError] to call this more than once, to call it before - /// [getAuthorizationUrl] is called, or to call it after - /// [handleAuthorizationCode] is called. - /// - /// Throws [FormatException] if [parameters] is invalid according to the - /// OAuth2 spec or if the authorization server otherwise provides invalid - /// responses. If `state` was passed to [getAuthorizationUrl], this will throw - /// a [FormatException] if the `state` parameter doesn't match the original - /// value. - /// - /// Throws [AuthorizationException] if the authorization fails. - Future handleAuthorizationResponse( - Map parameters) async { - if (_state == _State.initial) { - throw StateError('The authorization URL has not yet been generated.'); - } else if (_state == _State.finished) { - throw StateError('The authorization code has already been received.'); - } - _state = _State.finished; - - if (_stateString != null) { - if (!parameters.containsKey('state')) { - throw FormatException('Invalid OAuth response for ' - '"$authorizationEndpoint": parameter "state" expected to be ' - '"$_stateString", was missing.'); - } else if (parameters['state'] != _stateString) { - throw FormatException('Invalid OAuth response for ' - '"$authorizationEndpoint": parameter "state" expected to be ' - '"$_stateString", was "${parameters['state']}".'); - } - } - - if (parameters.containsKey('error')) { - var description = parameters['error_description']; - var uriString = parameters['error_uri']; - var uri = uriString == null ? null : Uri.parse(uriString); - throw AuthorizationException(parameters['error']!, description, uri); - } else if (!parameters.containsKey('code')) { - throw FormatException('Invalid OAuth response for ' - '"$authorizationEndpoint": did not contain required parameter ' - '"code".'); - } - - return _handleAuthorizationCode(parameters['code']); - } - - /// Processes an authorization code directly. - /// - /// Usually [handleAuthorizationResponse] is preferable to this method, since - /// it validates all of the query parameters. However, some authorization - /// servers allow the user to copy and paste an authorization code into a - /// command-line application, in which case this method must be used. - /// - /// It is a [StateError] to call this more than once, to call it before - /// [getAuthorizationUrl] is called, or to call it after - /// [handleAuthorizationCode] is called. - /// - /// Throws [FormatException] if the authorization server provides invalid - /// responses while retrieving credentials. - /// - /// Throws [AuthorizationException] if the authorization fails. - Future handleAuthorizationCode(String authorizationCode) async { - if (_state == _State.initial) { - throw StateError('The authorization URL has not yet been generated.'); - } else if (_state == _State.finished) { - throw StateError('The authorization code has already been received.'); - } - _state = _State.finished; - - return _handleAuthorizationCode(authorizationCode); - } - - /// This works just like [handleAuthorizationCode], except it doesn't validate - /// the state beforehand. - Future _handleAuthorizationCode(String? authorizationCode) async { - var startTime = DateTime.now(); - - var headers = {}; - - var body = { - 'grant_type': 'authorization_code', - 'code': authorizationCode, - 'redirect_uri': _redirectEndpoint.toString(), - 'code_verifier': _codeVerifier - }; - - var secret = this.secret; - if (_basicAuth && secret != null) { - headers['Authorization'] = basicAuthHeader(identifier, secret); - } else { - // The ID is required for this request any time basic auth isn't being - // used, even if there's no actual client authentication to be done. - body['client_id'] = identifier; - if (secret != null) body['client_secret'] = secret; - } - - var response = - await _httpClient!.post(tokenEndpoint, headers: headers, body: body); - - var credentials = handleAccessTokenResponse( - response, tokenEndpoint, startTime, _scopes, _delimiter, - getParameters: _getParameters); - return Client(credentials, - identifier: identifier, - secret: secret, - basicAuth: _basicAuth, - httpClient: _httpClient, - onCredentialsRefreshed: _onCredentialsRefreshed); - } - - // Randomly generate a 128 character string to be used as the PKCE code - // verifier. - static String _createCodeVerifier() => List.generate( - 128, - (i) => _charset[Random.secure().nextInt(_charset.length)], - ).join(); - - /// Closes the grant and frees its resources. - /// - /// This will close the underlying HTTP client, which is shared by the - /// [Client] created by this grant, so it's not safe to close the grant and - /// continue using the client. - void close() { - _httpClient?.close(); - _httpClient = null; - } -} - -/// States that [AuthorizationCodeGrant] can be in. -class _State { - /// [AuthorizationCodeGrant.getAuthorizationUrl] has not yet been called for - /// this grant. - static const initial = _State('initial'); - - // [AuthorizationCodeGrant.getAuthorizationUrl] has been called but neither - // [AuthorizationCodeGrant.handleAuthorizationResponse] nor - // [AuthorizationCodeGrant.handleAuthorizationCode] has been called. - static const awaitingResponse = _State('awaiting response'); - - // [AuthorizationCodeGrant.getAuthorizationUrl] and either - // [AuthorizationCodeGrant.handleAuthorizationResponse] or - // [AuthorizationCodeGrant.handleAuthorizationCode] have been called. - static const finished = _State('finished'); - - final String _name; - - const _State(this._name); - - @override - String toString() => _name; -} diff --git a/lib/src/third_party/oauth2/lib/src/authorization_exception.dart b/lib/src/third_party/oauth2/lib/src/authorization_exception.dart deleted file mode 100644 index 14a5a3c00..000000000 --- a/lib/src/third_party/oauth2/lib/src/authorization_exception.dart +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -/// An exception raised when OAuth2 authorization fails. -class AuthorizationException implements Exception { - /// The name of the error. - /// - /// Possible names are enumerated in [the spec][]. - /// - /// [the spec]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.2 - final String error; - - /// The description of the error, provided by the server. - /// - /// May be `null` if the server provided no description. - final String? description; - - /// A URL for a page that describes the error in more detail, provided by the - /// server. - /// - /// May be `null` if the server provided no URL. - final Uri? uri; - - /// Creates an AuthorizationException. - AuthorizationException(this.error, this.description, this.uri); - - /// Provides a string description of the AuthorizationException. - @override - String toString() { - var header = 'OAuth authorization error ($error)'; - if (description != null) { - header = '$header: $description'; - } else if (uri != null) { - header = '$header: $uri'; - } - return '$header.'; - } -} diff --git a/lib/src/third_party/oauth2/lib/src/client.dart b/lib/src/third_party/oauth2/lib/src/client.dart deleted file mode 100644 index 1dd2282fb..000000000 --- a/lib/src/third_party/oauth2/lib/src/client.dart +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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 'package:collection/collection.dart'; -import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'authorization_exception.dart'; -import 'credentials.dart'; -import 'expiration_exception.dart'; - -/// An OAuth2 client. -/// -/// This acts as a drop-in replacement for an [http.Client], while sending -/// OAuth2 authorization credentials along with each request. -/// -/// The client also automatically refreshes its credentials if possible. When it -/// makes a request, if its credentials are expired, it will first refresh them. -/// This means that any request may throw an [AuthorizationException] if the -/// refresh is not authorized for some reason, a [FormatException] if the -/// authorization server provides ill-formatted responses, or an -/// [ExpirationException] if the credentials are expired and can't be refreshed. -/// -/// The client will also throw an [AuthorizationException] if the resource -/// server returns a 401 response with a WWW-Authenticate header indicating that -/// the current credentials are invalid. -/// -/// If you already have a set of [Credentials], you can construct a [Client] -/// directly. However, in order to first obtain the credentials, you must -/// authorize. At the time of writing, the only authorization method this -/// library supports is [AuthorizationCodeGrant]. -class Client extends http.BaseClient { - /// The client identifier for this client. - /// - /// The authorization server will issue each client a separate client - /// identifier and secret, which allows the server to tell which client is - /// accessing it. Some servers may also have an anonymous identifier/secret - /// pair that any client may use. - /// - /// This is usually global to the program using this library. - final String? identifier; - - /// The client secret for this client. - /// - /// The authorization server will issue each client a separate client - /// identifier and secret, which allows the server to tell which client is - /// accessing it. Some servers may also have an anonymous identifier/secret - /// pair that any client may use. - /// - /// This is usually global to the program using this library. - /// - /// Note that clients whose source code or binary executable is readily - /// available may not be able to make sure the client secret is kept a secret. - /// This is fine; OAuth2 servers generally won't rely on knowing with - /// certainty that a client is who it claims to be. - final String? secret; - - /// The credentials this client uses to prove to the resource server that it's - /// authorized. - /// - /// This may change from request to request as the credentials expire and the - /// client refreshes them automatically. - Credentials get credentials => _credentials; - Credentials _credentials; - - /// Callback to be invoked whenever the credentials refreshed. - final CredentialsRefreshedCallback? _onCredentialsRefreshed; - - /// Whether to use HTTP Basic authentication for authorizing the client. - final bool _basicAuth; - - /// The underlying HTTP client. - http.Client? _httpClient; - - /// Creates a new client from a pre-existing set of credentials. - /// - /// When authorizing a client for the first time, you should use - /// [AuthorizationCodeGrant] or [resourceOwnerPasswordGrant] instead of - /// constructing a [Client] directly. - /// - /// [httpClient] is the underlying client that this forwards requests to after - /// adding authorization credentials to them. - /// - /// Throws an [ArgumentError] if [secret] is passed without [identifier]. - Client(this._credentials, - {this.identifier, - this.secret, - CredentialsRefreshedCallback? onCredentialsRefreshed, - bool basicAuth = true, - http.Client? httpClient}) - : _basicAuth = basicAuth, - _onCredentialsRefreshed = onCredentialsRefreshed, - _httpClient = httpClient ?? http.Client() { - if (identifier == null && secret != null) { - throw ArgumentError('secret may not be passed without identifier.'); - } - } - - /// Sends an HTTP request with OAuth2 authorization credentials attached. - /// - /// This will also automatically refresh this client's [Credentials] before - /// sending the request if necessary. - @override - Future send(http.BaseRequest request) async { - if (credentials.isExpired) { - if (!credentials.canRefresh) throw ExpirationException(credentials); - await refreshCredentials(); - } - - request.headers['authorization'] = 'Bearer ${credentials.accessToken}'; - var response = await _httpClient!.send(request); - - if (response.statusCode != 401) return response; - if (!response.headers.containsKey('www-authenticate')) return response; - - List challenges; - try { - challenges = AuthenticationChallenge.parseHeader( - response.headers['www-authenticate']!); - } on FormatException { - return response; - } - - var challenge = challenges - .firstWhereOrNull((challenge) => challenge.scheme == 'bearer'); - if (challenge == null) return response; - - var params = challenge.parameters; - if (!params.containsKey('error')) return response; - - throw AuthorizationException(params['error']!, params['error_description'], - params['error_uri'] == null ? null : Uri.parse(params['error_uri']!)); - } - - /// A [Future] used to track whether [refreshCredentials] is running. - Future? _refreshingFuture; - - /// Explicitly refreshes this client's credentials. Returns this client. - /// - /// This will throw a [StateError] if the [Credentials] can't be refreshed, an - /// [AuthorizationException] if refreshing the credentials fails, or a - /// [FormatException] if the authorization server returns invalid responses. - /// - /// You may request different scopes than the default by passing in - /// [newScopes]. These must be a subset of the scopes in the - /// [Credentials.scopes] field of [Client.credentials]. - Future refreshCredentials([List? newScopes]) async { - if (!credentials.canRefresh) { - var prefix = 'OAuth credentials'; - if (credentials.isExpired) prefix = '$prefix have expired and'; - throw StateError("$prefix can't be refreshed."); - } - - // To make sure that only one refresh happens when credentials are expired - // we track it using the [_refreshingFuture]. And also make sure that the - // _onCredentialsRefreshed callback is only called once. - if (_refreshingFuture == null) { - try { - _refreshingFuture = credentials.refresh( - identifier: identifier, - secret: secret, - newScopes: newScopes, - basicAuth: _basicAuth, - httpClient: _httpClient, - ); - _credentials = await _refreshingFuture!; - _onCredentialsRefreshed?.call(_credentials); - } finally { - _refreshingFuture = null; - } - } else { - await _refreshingFuture; - } - - return this; - } - - /// Closes this client and its underlying HTTP client. - @override - void close() { - _httpClient?.close(); - _httpClient = null; - } -} diff --git a/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart b/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart deleted file mode 100644 index 045d1a086..000000000 --- a/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'client.dart'; -import 'handle_access_token_response.dart'; -import 'utils.dart'; - -/// Obtains credentials using a [client credentials grant](https://tools.ietf.org/html/rfc6749#section-1.3.4). -/// -/// This mode of authorization uses the client's [identifier] and [secret] -/// to obtain an authorization token from the authorization server, instead -/// of sending a user through a dedicated flow. -/// -/// The client [identifier] and [secret] are required, and are -/// used to identify and authenticate your specific OAuth2 client. These are -/// usually global to the program using this library. -/// -/// The specific permissions being requested from the authorization server may -/// be specified via [scopes]. The scope strings are specific to the -/// authorization server and may be found in its documentation. Note that you -/// may not be granted access to every scope you request; you may check the -/// [Credentials.scopes] field of [Client.credentials] to see which scopes you -/// were granted. -/// -/// The scope strings will be separated by the provided [delimiter]. This -/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) -/// use non-standard delimiters. -/// -/// By default, this follows the OAuth2 spec and requires the server's responses -/// to be in JSON format. However, some servers return non-standard response -/// formats, which can be parsed using the [getParameters] function. -/// -/// This function is passed the `Content-Type` header of the response as well as -/// its body as a UTF-8-decoded string. It should return a map in the same -/// format as the [standard JSON response](https://tools.ietf.org/html/rfc6749#section-5.1) -Future clientCredentialsGrant( - Uri authorizationEndpoint, String? identifier, String? secret, - {Iterable? scopes, - bool basicAuth = true, - http.Client? httpClient, - String? delimiter, - Map Function(MediaType? contentType, String body)? - getParameters}) async { - delimiter ??= ' '; - var startTime = DateTime.now(); - - var body = {'grant_type': 'client_credentials'}; - - var headers = {}; - - if (identifier != null) { - if (basicAuth) { - headers['Authorization'] = basicAuthHeader(identifier, secret!); - } else { - body['client_id'] = identifier; - if (secret != null) body['client_secret'] = secret; - } - } - - if (scopes != null && scopes.isNotEmpty) { - body['scope'] = scopes.join(delimiter); - } - - httpClient ??= http.Client(); - var response = await httpClient.post(authorizationEndpoint, - headers: headers, body: body); - - var credentials = handleAccessTokenResponse(response, authorizationEndpoint, - startTime, scopes?.toList() ?? [], delimiter, - getParameters: getParameters); - return Client(credentials, - identifier: identifier, secret: secret, httpClient: httpClient); -} diff --git a/lib/src/third_party/oauth2/lib/src/credentials.dart b/lib/src/third_party/oauth2/lib/src/credentials.dart deleted file mode 100644 index 459e63e5c..000000000 --- a/lib/src/third_party/oauth2/lib/src/credentials.dart +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'handle_access_token_response.dart'; -import 'parameters.dart'; -import 'utils.dart'; - -/// Type of the callback when credentials are refreshed. -typedef CredentialsRefreshedCallback = void Function(Credentials); - -/// Credentials that prove that a client is allowed to access a resource on the -/// resource owner's behalf. -/// -/// These credentials are long-lasting and can be safely persisted across -/// multiple runs of the program. -/// -/// Many authorization servers will attach an expiration date to a set of -/// credentials, along with a token that can be used to refresh the credentials -/// once they've expired. The [Client] will automatically refresh its -/// credentials when necessary. It's also possible to explicitly refresh them -/// via [Client.refreshCredentials] or [Credentials.refresh]. -/// -/// Note that a given set of credentials can only be refreshed once, so be sure -/// to save the refreshed credentials for future use. -class Credentials { - /// A [String] used to separate scopes; defaults to `" "`. - String _delimiter; - - /// The token that is sent to the resource server to prove the authorization - /// of a client. - final String accessToken; - - /// The token that is sent to the authorization server to refresh the - /// credentials. - /// - /// This may be `null`, indicating that the credentials can't be refreshed. - final String? refreshToken; - - /// The token that is received from the authorization server to enable - /// End-Users to be Authenticated, contains Claims, represented as a - /// JSON Web Token (JWT). - /// - /// This may be `null`, indicating that the 'openid' scope was not - /// requested (or not supported). - /// - /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken - final String? idToken; - - /// The URL of the authorization server endpoint that's used to refresh the - /// credentials. - /// - /// This may be `null`, indicating that the credentials can't be refreshed. - final Uri? tokenEndpoint; - - /// The specific permissions being requested from the authorization server. - /// - /// The scope strings are specific to the authorization server and may be - /// found in its documentation. - final List? scopes; - - /// The date at which these credentials will expire. - /// - /// This is likely to be a few seconds earlier than the server's idea of the - /// expiration date. - final DateTime? expiration; - - /// The function used to parse parameters from a host's response. - final GetParameters _getParameters; - - /// Whether or not these credentials have expired. - /// - /// Note that it's possible the credentials will expire shortly after this is - /// called. However, since the client's expiration date is kept a few seconds - /// earlier than the server's, there should be enough leeway to rely on this. - bool get isExpired { - var expiration = this.expiration; - return expiration != null && DateTime.now().isAfter(expiration); - } - - /// Whether it's possible to refresh these credentials. - bool get canRefresh => refreshToken != null && tokenEndpoint != null; - - /// Creates a new set of credentials. - /// - /// This class is usually not constructed directly; rather, it's accessed via - /// [Client.credentials] after a [Client] is created by - /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized - /// form via [Credentials.fromJson]. - /// - /// The scope strings will be separated by the provided [delimiter]. This - /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) - /// use non-standard delimiters. - /// - /// By default, this follows the OAuth2 spec and requires the server's - /// responses to be in JSON format. However, some servers return non-standard - /// response formats, which can be parsed using the [getParameters] function. - /// - /// This function is passed the `Content-Type` header of the response as well - /// as its body as a UTF-8-decoded string. It should return a map in the same - /// format as the [standard JSON response][]. - /// - /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 - Credentials(this.accessToken, - {this.refreshToken, - this.idToken, - this.tokenEndpoint, - Iterable? scopes, - this.expiration, - String? delimiter, - Map Function(MediaType? mediaType, String body)? - getParameters}) - : scopes = UnmodifiableListView( - // Explicitly type-annotate the list literal to work around - // sdk#24202. - scopes == null ? [] : scopes.toList()), - _delimiter = delimiter ?? ' ', - _getParameters = getParameters ?? parseJsonParameters; - - /// Loads a set of credentials from a JSON-serialized form. - /// - /// Throws a [FormatException] if the JSON is incorrectly formatted. - factory Credentials.fromJson(String json) { - void validate(bool condition, message) { - if (condition) return; - throw FormatException('Failed to load credentials: $message.\n\n$json'); - } - - dynamic parsed; - try { - parsed = jsonDecode(json); - } on FormatException { - validate(false, 'invalid JSON'); - } - - validate(parsed is Map, 'was not a JSON map'); - - parsed = parsed as Map; - validate(parsed.containsKey('accessToken'), - 'did not contain required field "accessToken"'); - validate( - parsed['accessToken'] is String, - 'required field "accessToken" was not a string, was ' - '${parsed["accessToken"]}', - ); - - for (var stringField in ['refreshToken', 'idToken', 'tokenEndpoint']) { - var value = parsed[stringField]; - validate(value == null || value is String, - 'field "$stringField" was not a string, was "$value"'); - } - - var scopes = parsed['scopes']; - validate(scopes == null || scopes is List, - 'field "scopes" was not a list, was "$scopes"'); - - var tokenEndpoint = parsed['tokenEndpoint']; - Uri? tokenEndpointUri; - if (tokenEndpoint != null) { - tokenEndpointUri = Uri.parse(tokenEndpoint as String); - } - - var expiration = parsed['expiration']; - DateTime? expirationDateTime; - if (expiration != null) { - validate(expiration is int, - 'field "expiration" was not an int, was "$expiration"'); - expiration = expiration as int; - expirationDateTime = DateTime.fromMillisecondsSinceEpoch(expiration); - } - - return Credentials( - parsed['accessToken'] as String, - refreshToken: parsed['refreshToken'] as String?, - idToken: parsed['idToken'] as String?, - tokenEndpoint: tokenEndpointUri, - scopes: (scopes as List).map((scope) => scope as String), - expiration: expirationDateTime, - ); - } - - /// Serializes a set of credentials to JSON. - /// - /// Nothing is guaranteed about the output except that it's valid JSON and - /// compatible with [Credentials.toJson]. - String toJson() => jsonEncode({ - 'accessToken': accessToken, - 'refreshToken': refreshToken, - 'idToken': idToken, - 'tokenEndpoint': tokenEndpoint?.toString(), - 'scopes': scopes, - 'expiration': expiration?.millisecondsSinceEpoch - }); - - /// Returns a new set of refreshed credentials. - /// - /// See [Client.identifier] and [Client.secret] for explanations of those - /// parameters. - /// - /// You may request different scopes than the default by passing in - /// [newScopes]. These must be a subset of [scopes]. - /// - /// This throws an [ArgumentError] if [secret] is passed without [identifier], - /// a [StateError] if these credentials can't be refreshed, an - /// [AuthorizationException] if refreshing the credentials fails, or a - /// [FormatException] if the authorization server returns invalid responses. - Future refresh( - {String? identifier, - String? secret, - Iterable? newScopes, - bool basicAuth = true, - http.Client? httpClient}) async { - var scopes = this.scopes; - if (newScopes != null) scopes = newScopes.toList(); - scopes ??= []; - httpClient ??= http.Client(); - - if (identifier == null && secret != null) { - throw ArgumentError('secret may not be passed without identifier.'); - } - - var startTime = DateTime.now(); - var tokenEndpoint = this.tokenEndpoint; - if (refreshToken == null) { - throw StateError("Can't refresh credentials without a refresh " - 'token.'); - } else if (tokenEndpoint == null) { - throw StateError("Can't refresh credentials without a token " - 'endpoint.'); - } - - var headers = {}; - - var body = {'grant_type': 'refresh_token', 'refresh_token': refreshToken}; - if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter); - - if (basicAuth && secret != null) { - headers['Authorization'] = basicAuthHeader(identifier!, secret); - } else { - if (identifier != null) body['client_id'] = identifier; - if (secret != null) body['client_secret'] = secret; - } - - var response = - await httpClient.post(tokenEndpoint, headers: headers, body: body); - var credentials = handleAccessTokenResponse( - response, tokenEndpoint, startTime, scopes, _delimiter, - getParameters: _getParameters); - - // The authorization server may issue a new refresh token. If it doesn't, - // we should re-use the one we already have. - if (credentials.refreshToken != null) return credentials; - return Credentials(credentials.accessToken, - refreshToken: refreshToken, - idToken: credentials.idToken, - tokenEndpoint: credentials.tokenEndpoint, - scopes: credentials.scopes, - expiration: credentials.expiration); - } -} diff --git a/lib/src/third_party/oauth2/lib/src/expiration_exception.dart b/lib/src/third_party/oauth2/lib/src/expiration_exception.dart deleted file mode 100644 index d72fcf64c..000000000 --- a/lib/src/third_party/oauth2/lib/src/expiration_exception.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'credentials.dart'; - -/// An exception raised when attempting to use expired OAuth2 credentials. -class ExpirationException implements Exception { - /// The expired credentials. - final Credentials credentials; - - /// Creates an ExpirationException. - ExpirationException(this.credentials); - - /// Provides a string description of the ExpirationException. - @override - String toString() => - "OAuth2 credentials have expired and can't be refreshed."; -} diff --git a/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart b/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart deleted file mode 100644 index 931ae9dd7..000000000 --- a/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'authorization_exception.dart'; -import 'credentials.dart'; -import 'parameters.dart'; - -/// The amount of time to add as a "grace period" for credential expiration. -/// -/// This allows credential expiration checks to remain valid for a reasonable -/// amount of time. -const _expirationGrace = Duration(seconds: 10); - -/// Handles a response from the authorization server that contains an access -/// token. -/// -/// This response format is common across several different components of the -/// OAuth2 flow. -/// -/// By default, this follows the OAuth2 spec and requires the server's responses -/// to be in JSON format. However, some servers return non-standard response -/// formats, which can be parsed using the [getParameters] function. -/// -/// This function is passed the `Content-Type` header of the response as well as -/// its body as a UTF-8-decoded string. It should return a map in the same -/// format as the [standard JSON response][]. -/// -/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 -Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint, - DateTime startTime, List? scopes, String delimiter, - {Map Function(MediaType? contentType, String body)? - getParameters}) { - getParameters ??= parseJsonParameters; - - try { - if (response.statusCode != 200) { - _handleErrorResponse(response, tokenEndpoint, getParameters); - } - - var contentTypeString = response.headers['content-type']; - if (contentTypeString == null) { - throw const FormatException('Missing Content-Type string.'); - } - - var parameters = - getParameters(MediaType.parse(contentTypeString), response.body); - - for (var requiredParameter in ['access_token', 'token_type']) { - if (!parameters.containsKey(requiredParameter)) { - throw FormatException( - 'did not contain required parameter "$requiredParameter"'); - } else if (parameters[requiredParameter] is! String) { - throw FormatException( - 'required parameter "$requiredParameter" was not a string, was ' - '"${parameters[requiredParameter]}"'); - } - } - - // TODO(nweiz): support the "mac" token type - // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01) - if ((parameters['token_type'] as String).toLowerCase() != 'bearer') { - throw FormatException( - '"$tokenEndpoint": unknown token type "${parameters['token_type']}"'); - } - - var expiresIn = parameters['expires_in']; - if (expiresIn != null) { - if (expiresIn is String) { - try { - expiresIn = double.parse(expiresIn).toInt(); - } on FormatException { - throw FormatException( - 'parameter "expires_in" could not be parsed as in, was: "$expiresIn"'); - } - } else if (expiresIn is! int) { - throw FormatException( - 'parameter "expires_in" was not an int, was: "$expiresIn"'); - } - } - - for (var name in ['refresh_token', 'id_token', 'scope']) { - var value = parameters[name]; - if (value != null && value is! String) { - throw FormatException( - 'parameter "$name" was not a string, was "$value"'); - } - } - - var scope = parameters['scope'] as String?; - if (scope != null) scopes = scope.split(delimiter); - - var expiration = expiresIn == null - ? null - : startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace); - - return Credentials( - parameters['access_token'] as String, - refreshToken: parameters['refresh_token'] as String?, - idToken: parameters['id_token'] as String?, - tokenEndpoint: tokenEndpoint, - scopes: scopes, - expiration: expiration, - ); - } on FormatException catch (e) { - throw FormatException('Invalid OAuth response for "$tokenEndpoint": ' - '${e.message}.\n\n${response.body}'); - } -} - -/// Throws the appropriate exception for an error response from the -/// authorization server. -void _handleErrorResponse( - http.Response response, Uri tokenEndpoint, GetParameters getParameters) { - // OAuth2 mandates a 400 or 401 response code for access token error - // responses. If it's not a 400 reponse, the server is either broken or - // off-spec. - if (response.statusCode != 400 && response.statusCode != 401) { - var reason = ''; - var reasonPhrase = response.reasonPhrase; - if (reasonPhrase != null && reasonPhrase.isNotEmpty) { - reason = ' $reasonPhrase'; - } - throw FormatException('OAuth request for "$tokenEndpoint" failed ' - 'with status ${response.statusCode}$reason.\n\n${response.body}'); - } - - var contentTypeString = response.headers['content-type']; - var contentType = - contentTypeString == null ? null : MediaType.parse(contentTypeString); - - var parameters = getParameters(contentType, response.body); - - if (!parameters.containsKey('error')) { - throw const FormatException('did not contain required parameter "error"'); - } else if (parameters['error'] is! String) { - throw FormatException('required parameter "error" was not a string, was ' - '"${parameters["error"]}"'); - } - - for (var name in ['error_description', 'error_uri']) { - var value = parameters[name]; - - if (value != null && value is! String) { - throw FormatException('parameter "$name" was not a string, was "$value"'); - } - } - - var uriString = parameters['error_uri'] as String?; - var uri = uriString == null ? null : Uri.parse(uriString); - var description = parameters['error_description'] as String?; - throw AuthorizationException(parameters['error'] as String, description, uri); -} diff --git a/lib/src/third_party/oauth2/lib/src/parameters.dart b/lib/src/third_party/oauth2/lib/src/parameters.dart deleted file mode 100644 index ecc655978..000000000 --- a/lib/src/third_party/oauth2/lib/src/parameters.dart +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file -// for details. 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:convert'; - -import 'package:http_parser/http_parser.dart'; - -/// The type of a callback that parses parameters from an HTTP response. -typedef GetParameters = Map Function( - MediaType? contentType, String body); - -/// Parses parameters from a response with a JSON body, as per the -/// [OAuth2 spec][]. -/// -/// [OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-5.1 -Map parseJsonParameters(MediaType? contentType, String body) { - // The spec requires a content-type of application/json, but some endpoints - // (e.g. Dropbox) serve it as text/javascript instead. - if (contentType == null || - (contentType.mimeType != 'application/json' && - contentType.mimeType != 'text/javascript')) { - throw FormatException( - 'Content-Type was "$contentType", expected "application/json"'); - } - - var untypedParameters = jsonDecode(body); - if (untypedParameters is Map) { - return untypedParameters; - } - - throw FormatException('Parameters must be a map, was "$untypedParameters"'); -} diff --git a/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart b/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart deleted file mode 100644 index 96fb5037b..000000000 --- a/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; - -import 'client.dart'; -import 'credentials.dart'; -import 'handle_access_token_response.dart'; -import 'utils.dart'; - -/// Obtains credentials using a [resource owner password grant](https://tools.ietf.org/html/rfc6749#section-1.3.3). -/// -/// This mode of authorization uses the user's username and password to obtain -/// an authentication token, which can then be stored. This is safer than -/// storing the username and password directly, but it should be avoided if any -/// other authorization method is available, since it requires the user to -/// provide their username and password to a third party (you). -/// -/// The client [identifier] and [secret] may be issued by the server, and are -/// used to identify and authenticate your specific OAuth2 client. These are -/// usually global to the program using this library. -/// -/// The specific permissions being requested from the authorization server may -/// be specified via [scopes]. The scope strings are specific to the -/// authorization server and may be found in its documentation. Note that you -/// may not be granted access to every scope you request; you may check the -/// [Credentials.scopes] field of [Client.credentials] to see which scopes you -/// were granted. -/// -/// The scope strings will be separated by the provided [delimiter]. This -/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's) -/// use non-standard delimiters. -/// -/// By default, this follows the OAuth2 spec and requires the server's responses -/// to be in JSON format. However, some servers return non-standard response -/// formats, which can be parsed using the [getParameters] function. -/// -/// This function is passed the `Content-Type` header of the response as well as -/// its body as a UTF-8-decoded string. It should return a map in the same -/// format as the [standard JSON response][]. -/// -/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1 -Future resourceOwnerPasswordGrant( - Uri authorizationEndpoint, String username, String password, - {String? identifier, - String? secret, - Iterable? scopes, - bool basicAuth = true, - CredentialsRefreshedCallback? onCredentialsRefreshed, - http.Client? httpClient, - String? delimiter, - Map Function(MediaType? contentType, String body)? - getParameters}) async { - delimiter ??= ' '; - var startTime = DateTime.now(); - - var body = { - 'grant_type': 'password', - 'username': username, - 'password': password - }; - - var headers = {}; - - if (identifier != null) { - if (basicAuth) { - headers['Authorization'] = basicAuthHeader(identifier, secret!); - } else { - body['client_id'] = identifier; - if (secret != null) body['client_secret'] = secret; - } - } - - if (scopes != null && scopes.isNotEmpty) { - body['scope'] = scopes.join(delimiter); - } - - httpClient ??= http.Client(); - var response = await httpClient.post(authorizationEndpoint, - headers: headers, body: body); - - var credentials = handleAccessTokenResponse( - response, authorizationEndpoint, startTime, scopes?.toList(), delimiter, - getParameters: getParameters); - return Client(credentials, - identifier: identifier, - secret: secret, - httpClient: httpClient, - onCredentialsRefreshed: onCredentialsRefreshed); -} diff --git a/lib/src/third_party/oauth2/lib/src/utils.dart b/lib/src/third_party/oauth2/lib/src/utils.dart deleted file mode 100644 index 2a22b9fa5..000000000 --- a/lib/src/third_party/oauth2/lib/src/utils.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file -// for details. 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:convert'; - -/// Adds additional query parameters to [url], overwriting the original -/// parameters if a name conflict occurs. -Uri addQueryParameters(Uri url, Map parameters) => url.replace( - queryParameters: Map.from(url.queryParameters)..addAll(parameters)); - -String basicAuthHeader(String identifier, String secret) { - var userPass = '${Uri.encodeFull(identifier)}:${Uri.encodeFull(secret)}'; - return 'Basic ${base64Encode(ascii.encode(userPass))}'; -} diff --git a/lib/src/third_party/oauth2/vendored-pubspec.yaml b/lib/src/third_party/oauth2/vendored-pubspec.yaml deleted file mode 100644 index bb43bb7d3..000000000 --- a/lib/src/third_party/oauth2/vendored-pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: oauth2 -version: 2.0.1 -description: >- - A client library for authenticating with a remote service via OAuth2 on - behalf of a user, and making authorized HTTP requests with the user's - OAuth2 credentials. -repository: https://github.com/dart-lang/oauth2 - -environment: - sdk: '>=2.17.0 <3.0.0' - -dependencies: - collection: ^1.15.0 - crypto: ^3.0.0 - http: ^0.13.0 - http_parser: ^4.0.0 - -dev_dependencies: - lints: ^2.0.0 - test: ^1.16.0 diff --git a/lib/src/third_party/tar/CHANGELOG.md b/lib/src/third_party/tar/CHANGELOG.md deleted file mode 100644 index 590a70db0..000000000 --- a/lib/src/third_party/tar/CHANGELOG.md +++ /dev/null @@ -1,94 +0,0 @@ -## 1.0.1 - -- Fix an incompatibility with Dart 3.1. - -## 1.0.0 - -- __Breaking__ Add class modifiers where applicable. - -## 0.5.6 - -- Allow cancelling a `TarEntry.contents` subscription before reading more files. - -## 0.5.5+1 - -- No user-visible changes. - -## 0.5.5 - -- Fix a crash when pausing a subscription to `TarEntry.contents` right before - it ends. - -## 0.5.4 - -- Fix generating corrupt tar files when adding lots of entries at very high - speeds [(#20)](https://github.com/simolus3/tar/issues/20). -- Allow tar files with invalid utf8 content in PAX header values if those - values aren't used for anything important. - -## 0.5.3 - -- Improve error messages when reading a tar entry after, or during, a call to - `moveNext()`. - -## 0.5.2 - -- This package now supports being compiled to JavaScript. - -## 0.5.1 - -- Improve performance when reading large archives - -## 0.5.0 - -- Support sync encoding with `tarConverter`. - -## 0.4.0 - -- Support generating tar files with GNU-style long link names - - Add `format` parameter to `tarWritingSink` and `tarWriterWith` - -## 0.3.3 - -- Drop `chunked_stream` dependency in favor of `package:async`. - -## 0.3.2 - -- Allow arbitrarily many zero bytes at the end of an archive when - `disallowTrailingData` is enabled. - -## 0.3.1 - -- Add `disallowTrailingData` parameter to `TarReader`. When the option is set, - `readNext` will ensure that the input stream does not emit further data after - the tar archive has been read fully. - -## 0.3.0 - -- Remove outdated references in the documentation - -## 0.3.0-nullsafety.0 - -- Remove `TarReader.contents` and `TarReader.header`. Use `current.contents` and `current.header`, respectively. -- Fix some minor implementation details - -## 0.2.0-nullsafety - -Most of the tar package has been rewritten, it's now based on the -implementation written by [Garett Tok Ern Liang](https://github.com/walnutdust) -in the GSoC 2020. - -- Added `tar` prefix to exported symbols. -- Remove `MemoryEntry`. Use `TarEntry.data` to create a tar entry from bytes. -- Make `WritingSink` private. Use `tarWritingSink` to create a general `StreamSink`. -- `TarReader` is now a [`StreamIterator`](https://api.dart.dev/stable/2.10.4/dart-async/StreamIterator-class.html), - the transformer had some design flaws. - -## 0.1.0-nullsafety.1 - -- Support writing user and group names -- Better support for PAX-headers and large files - -## 0.1.0-nullsafety.0 - -- Initial version diff --git a/lib/src/third_party/tar/LICENSE b/lib/src/third_party/tar/LICENSE deleted file mode 100644 index ed92ded99..000000000 --- a/lib/src/third_party/tar/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Simon Binder - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/lib/src/third_party/tar/README.md b/lib/src/third_party/tar/README.md deleted file mode 100644 index 8d5a33440..000000000 --- a/lib/src/third_party/tar/README.md +++ /dev/null @@ -1,214 +0,0 @@ -# tar - -![Build status](https://github.com/simolus3/tar/workflows/build/badge.svg) - -This package provides stream-based readers and writers for tar files. - -When working with large tar files, this library consumes considerably less memory -than [package:archive](https://pub.dev/packages/archive), although it is slightly slower due to the async overhead. - -## Reading - -To read entries from a tar file, use a `TarReader` with a `Stream` emitting bytes (as `List`): - -```dart -import 'dart:convert'; -import 'dart:io'; -import 'package:tar/tar.dart'; - -Future main() async { - final reader = TarReader(File('file.tar').openRead()); - - while (await reader.moveNext()) { - final entry = reader.current; - // Use reader.header to see the header of the current tar entry - print(entry.header.name); - // And reader.contents to read the content of the current entry as a stream - print(await entry.contents.transform(utf8.decoder).first); - } - // Note that the reader will automatically close if moveNext() returns false or - // throws. If you want to close a tar stream before that happens, use - // reader.cancel(); -} -``` - -To read `.tar.gz` files, transform the stream with `gzip.decoder` before -passing it to the `TarReader`. - -To easily go through all entries in a tar file, use `TarReader.forEach`: - -```dart -Future main() async { - final inputStream = File('file.tar').openRead(); - - await TarReader.forEach(inputStream, (entry) { - print(header.name); - print(await entry.contents.transform(utf8.decoder).first); - }); -} -``` - -__Warning__: Since the reader is backed by a single stream, concurrent calls to -`read` are not allowed! Similarly, if you're reading from an entry's `contents`, -make sure to fully drain the stream before calling `read()` again. -_Not_ subscribing to `contents` before calling `moveNext()` is acceptable too. -In this case, the reader will implicitly drain the stream. -The reader detects concurrency misuses and will throw an error when they occur, -there's no risk of reading faulty data. - -## Writing - -When writing archives, `package:tar` expects a `Stream` of tar entries to include in -the archive. -This stream can then be converted into a stream of byte-array chunks forming the -encoded tar archive. - -To write a tar stream into a `StreamSink>`, such as an `IOSink` returned by -`File.openWrite`, use `tarWritingSink`: - -```dart -import 'dart:convert'; -import 'dart:io'; -import 'package:tar/tar.dart'; - -Future main() async { - final output = File('test.tar').openWrite(); - final tarEntries = Stream.value( - TarEntry.data( - TarHeader( - name: 'hello.txt', - mode: int.parse('644', radix: 8), - ), - utf8.encode('Hello world'), - ), - ); - - await tarEntries.pipe(tarWritingSink(output)); -} -``` - -For more complex stream transformations, `tarWriter` can be used as a stream -transformer converting a stream of tar entries into archive bytes. - -Together with the `gzip.encoder` transformer from `dart:io`, this can be used -to write a `.tar.gz` file: - -```dart -import 'dart:io'; -import 'package:tar/tar.dart'; - -Future write(Stream entries) { - return entries - .transform(tarWriter) // convert entries into a .tar stream - .transform(gzip.encoder) // convert the .tar stream into a .tar.gz stream - .pipe(File('output.tar.gz').openWrite()); -} -``` - -A more complex example for writing files can be found in [`example/archive_self.dart`](example/archive_self.dart). - -### Encoding options - -By default, tar files are written in the pax format defined by the -POSIX.1-2001 specification (`--format=posix` in GNU tar). -When all entries have file names shorter than 100 chars and a size smaller -than 8 GB, this is equivalent to the `ustar` format. This library won't write -PAX headers when there is no reason to do so. -If you prefer writing GNU-style long filenames instead, you can use the -`format` option: - -```dart -Future write(Stream entries) { - return entries - .pipe( - tarWritingSink( - File('output.tar').openWrite(), - format: OutputFormat.gnuLongName, - )); -} -``` - -To change the output format on the `tarWriter` transformer, use -`tarWriterWith`. - -### Synchronous writing - -As the content of tar entries is defined as an asynchronous stream, the tar encoder is asynchronous too. -The more specific `SynchronousTarEntry` class stores tar content as a list of bytes, meaning that it can be -written synchronously. - -To synchronously write tar files, use `tarConverter` (or `tarConverterWith` for options): - -```dart -List createTarArchive(Iterable entries) { - late List result; - final sink = ByteConversionSink.withCallback((data) => result = data); - - final output = tarConverter.startChunkedConversion(sink); - entries.forEach(output.add); - output.close(); - - return result; -} -``` - -## Features - -- Supports v7, ustar, pax, gnu and star archives -- Supports extended pax headers for long file or link names -- Supports long file and link names generated by GNU-tar -- Hardened against denial-of-service attacks with invalid tar files -- Supports being compiled to JavaScript, tested on Node.js - -## Security considerations - -Internally, this package contains checks to guard against some invalid tar files. -In particular, - -- The reader doesn't allocate memory based on values in a tar file (so there's - a guard against DoS attacks with tar files containing huge headers). -- When encountering malformed tar files, the reader will throw a `TarException`. - Any other exception thrown indicates a bug in `package:tar` or how it's used. - The reader should never crash. -- Reading a tar file can be cancelled mid-stream without leaking resources. - -However, the tar reader __does not__ throw exceptions for wellformed archives -with suspicious contents, such as - -- File names beginning with `../`, `/` or names pointing out of the archive by - other means. -- Link references to files outside of the archive. -- Paths not using forward slashes. -- Gzip + tar bombs. -- Invalid permission bits in entries. -- ... - -When reading or extracting untrusted tar files, it is your responsibility to -detect and handle these cases. -For instance, this naive extraction function is susceptible to invalid tar -files containing paths outside of the target directory: - -```dart -Future extractTarGz(File tarGz, Directory target) async { - final input = tarGz.openRead().transform(gzip.decoder); - - await TarReader.forEach(input, (entry) async { - final destination = - // DON'T DO THIS! If `entry.name` contained `../`, this may escape the - // target directory. - path.joinAll([target.path, ...path.posix.split(entry.name)]); - - final f = File(destination); - await f.create(recursive: true); - await entry.contents.pipe(f.openWrite()); - }); -} -``` - -For an idea on how to guard against this, see the [extraction logic](https://github.com/dart-lang/pub/blob/3082796f8ba9b3f509265ac3a223312fb5033988/lib/src/io.dart#L904-L991) -used by the pub client. - ------ - -Big thanks to [Garett Tok Ern Liang](https://github.com/walnutdust) for writing the initial -Dart tar reader that this library is based on. diff --git a/lib/src/third_party/tar/analysis_options.yaml b/lib/src/third_party/tar/analysis_options.yaml deleted file mode 100644 index 1dd563f70..000000000 --- a/lib/src/third_party/tar/analysis_options.yaml +++ /dev/null @@ -1,18 +0,0 @@ -include: package:extra_pedantic/analysis_options.4.0.0.yaml - -analyzer: - language: - strict-inference: true - strict-raw-types: true - -linter: - rules: - close_sinks: false # This rule has just too many false-positives... - comment_references: true - package_api_docs: true - literal_only_boolean_expressions: false # Nothing wrong with a little while(true) - parameter_assignments: false - unnecessary_await_in_return: false - no_default_cases: false - prefer_asserts_with_message: false # We only use asserts for library-internal invariants - prefer_final_parameters: false # Too much noise diff --git a/lib/src/third_party/tar/lib/src/charcodes.dart b/lib/src/third_party/tar/lib/src/charcodes.dart deleted file mode 100644 index b25bb369a..000000000 --- a/lib/src/third_party/tar/lib/src/charcodes.dart +++ /dev/null @@ -1,76 +0,0 @@ -@internal -library; - -import 'package:meta/meta.dart'; - -/// "Line feed" control character. -const int $lf = 0x0a; - -/// Space character. -const int $space = 0x20; - -/// Character `0`. -const int $0 = 0x30; - -/// Character `1`. -const int $1 = 0x31; - -/// Character `2`. -const int $2 = 0x32; - -/// Character `3`. -const int $3 = 0x33; - -/// Character `4`. -const int $4 = 0x34; - -/// Character `5`. -const int $5 = 0x35; - -/// Character `6`. -const int $6 = 0x36; - -/// Character `7`. -const int $7 = 0x37; - -/// Character `8`. -const int $8 = 0x38; - -/// Character `9`. -const int $9 = 0x39; - -/// Character `<`. -const int $equal = 0x3d; - -/// Character `A`. -const int $A = 0x41; - -/// Character `K`. -const int $K = 0x4b; - -/// Character `L`. -const int $L = 0x4c; - -/// Character `S`. -const int $S = 0x53; - -/// Character `a`. -const int $a = 0x61; - -/// Character `g`. -const int $g = 0x67; - -/// Character `r`. -const int $r = 0x72; - -/// Character `s`. -const int $s = 0x73; - -/// Character `t`. -const int $t = 0x74; - -/// Character `u`. -const int $u = 0x75; - -/// Character `x`. -const int $x = 0x78; diff --git a/lib/src/third_party/tar/lib/src/constants.dart b/lib/src/third_party/tar/lib/src/constants.dart deleted file mode 100644 index d74a2b00f..000000000 --- a/lib/src/third_party/tar/lib/src/constants.dart +++ /dev/null @@ -1,188 +0,0 @@ -@internal -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; - -import 'charcodes.dart'; -import 'exception.dart'; -import 'header.dart'; - -// Magic values to help us identify the TAR header type. -const magicGnu = [$u, $s, $t, $a, $r, $space]; // 'ustar ' -const versionGnu = [$space, 0]; // ' \x00' -const magicUstar = [$u, $s, $t, $a, $r, 0]; // 'ustar\x00' -const versionUstar = [$0, $0]; // '00' -const trailerStar = [$t, $a, $r, 0]; // 'tar\x00' - -/// Generates the corresponding [TypeFlag] associated with [byte]. -TypeFlag typeflagFromByte(int byte) { - switch (byte) { - case $0: - return TypeFlag.reg; - case 0: - return TypeFlag.regA; - case $1: - return TypeFlag.link; - case $2: - return TypeFlag.symlink; - case $3: - return TypeFlag.char; - case $4: - return TypeFlag.block; - case $5: - return TypeFlag.dir; - case $6: - return TypeFlag.fifo; - case $7: - return TypeFlag.reserved; - case $x: - return TypeFlag.xHeader; - case $g: - return TypeFlag.xGlobalHeader; - case $S: - return TypeFlag.gnuSparse; - case $L: - return TypeFlag.gnuLongName; - case $K: - return TypeFlag.gnuLongLink; - default: - if (64 < byte && byte < 91) { - return TypeFlag.vendor; - } - throw TarException.header('Invalid typeflag value $byte'); - } -} - -int typeflagToByte(TypeFlag flag) { - switch (flag) { - case TypeFlag.reg: - case TypeFlag.regA: - return $0; - case TypeFlag.link: - return $1; - case TypeFlag.symlink: - return $2; - case TypeFlag.char: - return $3; - case TypeFlag.block: - return $4; - case TypeFlag.dir: - return $5; - case TypeFlag.fifo: - return $6; - case TypeFlag.reserved: - return $7; - case TypeFlag.xHeader: - return $x; - case TypeFlag.xGlobalHeader: - return $g; - case TypeFlag.gnuSparse: - return $S; - case TypeFlag.gnuLongName: - return $L; - case TypeFlag.gnuLongLink: - return $K; - case TypeFlag.vendor: - throw ArgumentError("Can't write vendor-specific type-flags"); - } -} - -/// Keywords for PAX extended header records. -const paxPath = 'path'; -const paxLinkpath = 'linkpath'; -const paxSize = 'size'; -const paxUid = 'uid'; -const paxGid = 'gid'; -const paxUname = 'uname'; -const paxGname = 'gname'; -const paxMtime = 'mtime'; -const paxAtime = 'atime'; -const paxCtime = - 'ctime'; // Removed from later revision of PAX spec, but was valid -const paxComment = 'comment'; -const paxSchilyXattr = 'SCHILY.xattr.'; - -/// Keywords for GNU sparse files in a PAX extended header. -const paxGNUSparse = 'GNU.sparse.'; -const paxGNUSparseNumBlocks = 'GNU.sparse.numblocks'; -const paxGNUSparseOffset = 'GNU.sparse.offset'; -const paxGNUSparseNumBytes = 'GNU.sparse.numbytes'; -const paxGNUSparseMap = 'GNU.sparse.map'; -const paxGNUSparseName = 'GNU.sparse.name'; -const paxGNUSparseMajor = 'GNU.sparse.major'; -const paxGNUSparseMinor = 'GNU.sparse.minor'; -const paxGNUSparseSize = 'GNU.sparse.size'; -const paxGNUSparseRealSize = 'GNU.sparse.realsize'; - -/// A set of pax header keys supported by this library. -/// -/// The reader will ignore pax headers not listed in this map. -const supportedPaxHeaders = { - paxPath, - paxLinkpath, - paxSize, - paxUid, - paxGid, - paxUname, - paxGname, - paxMtime, - paxAtime, - paxCtime, - paxComment, - paxSchilyXattr, - paxGNUSparse, - paxGNUSparseNumBlocks, - paxGNUSparseOffset, - paxGNUSparseNumBytes, - paxGNUSparseMap, - paxGNUSparseName, - paxGNUSparseMajor, - paxGNUSparseMinor, - paxGNUSparseSize, - paxGNUSparseRealSize -}; - -/// User ID bit -const c_ISUID = 2048; - -/// Group ID bit -const c_ISGID = 1024; - -/// Sticky bit -const c_ISVTX = 512; - -/// Constants to determine file modes. -const modeType = 2401763328; -const modeSymLink = 134217728; -const modeDevice = 67108864; -const modeCharDevice = 2097152; -const modeNamedPipe = 33554432; -const modeSocket = 1677216; -const modeSetUid = 8388608; -const modeSetGid = 4194304; -const modeSticky = 1048576; -const modeDirectory = 2147483648; - -/// The offset of the checksum in the header -const checksumOffset = 148; -const checksumLength = 8; -const magicOffset = 257; -const versionOffset = 263; -const starTrailerOffset = 508; - -/// Size constants from various TAR specifications. -/// Size of each block in a TAR stream. -const blockSize = 512; -const blockSizeLog2 = 9; -const maxIntFor12CharOct = 0x1ffffffff; // 777 7777 7777 in oct - -const defaultSpecialLength = 4 * blockSize; - -/// Max length of the name field in USTAR format. -const nameSize = 100; - -/// Max length of the prefix field in USTAR format. -const prefixSize = 155; - -/// A full TAR block of zeros. -final zeroBlock = Uint8List(blockSize); diff --git a/lib/src/third_party/tar/lib/src/entry.dart b/lib/src/third_party/tar/lib/src/entry.dart deleted file mode 100644 index 7645bbb22..000000000 --- a/lib/src/third_party/tar/lib/src/entry.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'dart:async'; - -import 'header.dart'; - -/// An entry in a tar file. -/// -/// Usually, tar entries are read from a stream, and they're bound to the stream -/// from which they've been read. This means that they can only be read once, -/// and that only one [TarEntry] is active at a time. -final class TarEntry { - /// The parsed [TarHeader] of this tar entry. - final TarHeader header; - - /// The content stream of the active tar entry. - /// - /// For tar entries read through the reader provided by this library, - /// [contents] is a single-subscription streamed backed by the original stream - /// used to create the reader. - /// When listening on [contents], the stream needs to be fully drained before - /// the next call to [StreamIterator.moveNext]. It's acceptable to not listen - /// to [contents] at all before calling [StreamIterator.moveNext] again. - /// In that case, this library will take care of draining the stream to get to - /// the next entry. - final Stream> contents; - - /// The name of this entry, as indicated in the header or a previous pax - /// entry. - String get name => header.name; - - /// The type of tar entry (file, directory, etc.). - TypeFlag get type => header.typeFlag; - - /// The content size of this entry, in bytes. - int get size => header.size; - - /// Time of the last modification of this file, as indicated in the [header]. - DateTime get modified => header.modified; - - /// Creates a tar entry from a [header] and the [contents] stream. - /// - /// If the total length of [contents] is known, consider setting the - /// [header]'s [TarHeader.size] property to the appropriate value. - /// Otherwise, the tar writer needs to buffer contents to determine the right - /// size. - // factory so that this class can't be extended - factory TarEntry(TarHeader header, Stream> contents) = TarEntry._; - - TarEntry._(this.header, this.contents); - - /// Creates an in-memory tar entry from the [header] and the [data] to store. - static SynchronousTarEntry data(TarHeader header, List data) { - (header as HeaderImpl).size = data.length; - return SynchronousTarEntry._(header, data); - } -} - -/// A tar entry stored in memory. -final class SynchronousTarEntry extends TarEntry { - /// The contents of this tar entry as a byte array. - final List data; - - SynchronousTarEntry._(TarHeader header, this.data) - : super._(header, Stream.value(data)); -} diff --git a/lib/src/third_party/tar/lib/src/exception.dart b/lib/src/third_party/tar/lib/src/exception.dart deleted file mode 100644 index fa1fc924e..000000000 --- a/lib/src/third_party/tar/lib/src/exception.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:meta/meta.dart'; - -/// An exception indicating that there was an issue parsing a `.tar` file. -/// -/// The [message] contains reported from this exception contains details on the -/// location of the parsing error. -/// -/// This is the only exception that should be thrown by the `tar` package. Other -/// exceptions are either a bug in this package or errors thrown as a response -/// to API misuse. -final class TarException extends FormatException { - @internal - TarException(String message) : super(message); - - @internal - factory TarException.header(String message) { - return TarException('Invalid header: $message'); - } -} diff --git a/lib/src/third_party/tar/lib/src/format.dart b/lib/src/third_party/tar/lib/src/format.dart deleted file mode 100644 index b584f5c05..000000000 --- a/lib/src/third_party/tar/lib/src/format.dart +++ /dev/null @@ -1,322 +0,0 @@ -/// Holds the possible TAR formats that a file could take. -/// -/// This library supports the V7, USTAR, PAX, GNU, and STAR formats. The -/// [MaybeTarFormat] class generally describes any combination of those formats -/// and represents that we don't know the exact format yet. As soon as we do -/// know, the [TarFormat] enum represents the exact format of a header. -sealed class MaybeTarFormat { - /// The TAR formats are encoded in powers of two in [_value], such that we - /// can refine our guess via bit operations as we discover more information - /// about the TAR file. - /// A value of 0 means that the format is invalid. - int get _value; - - factory MaybeTarFormat._(int value) { - return switch (value) { - 1 => TarFormat.v7, - 2 => TarFormat.ustar, - 4 => TarFormat.pax, - 8 => TarFormat.gnu, - 16 => TarFormat.star, - final other => _MaybeTarFormat(other), - }; - } - - /// Returns a new [MaybeTarFormat] that signifies that it can be either - /// `this` or [other]'s format. - /// - /// **Example:** - /// ```dart - /// TarFormat format = TarFormat.ustar | TarFormat.pax; - /// ``` - /// - /// The above code would signify that we have limited `format` to either - /// the USTAR or PAX format, but need further information to refine the guess. - MaybeTarFormat operator |(TarFormat other); - - /// Returns if [other] is a possible resolution of `this`. - /// - /// For example, a [TarFormat] with a value of 6 means that we do not have - /// enough information to determine if it is [TarFormat.ustar] or - /// [TarFormat.pax], so either of them could be possible resolutions of - /// `this`. - bool has(MaybeTarFormat other); - - /// Returns a new [TarFormat] that signifies that it can only be [other]'s - /// format. - /// - /// **Example:** - /// ```dart - /// TarFormat format = TarFormat.PAX | TarFormat.USTAR; - /// ... - /// format = format.mayOnlyBe(TarFormat.USTAR); - /// ``` - /// - /// In the above example, we found that `format` could either be PAX or USTAR, - /// but later learnt that it can only be the USTAR format. - /// - /// If `has(other) == false`, [mayOnlyBe] will result in an unknown - /// [TarFormat]. - MaybeTarFormat mayOnlyBe(MaybeTarFormat other); - - /// Returns if this format might be valid. - /// - /// This returns true as well even if we have yet to fully determine what the - /// format is. - bool get valid; -} - -/// A fully resolved tar format. -/// -/// When we know that a tar entry must use a specific format, this is represented -/// with a value from this [TarFormat] enum. -enum TarFormat implements MaybeTarFormat { - /// Original Unix Version 7 (V7) AT&T tar tool prior to standardization. - /// - /// The structure of the V7 Header consists of the following: - /// - /// Start | End | Field - /// ========================================================================= - /// 0 | 100 | Path name, stored as null-terminated string. - /// 100 | 108 | File mode, stored as an octal number in ASCII. - /// 108 | 116 | User id of owner, as octal number in ASCII. - /// 116 | 124 | Group id of owner, as octal number in ASCII. - /// 124 | 136 | Size of file, as octal number in ASCII. - /// 136 | 148 | Modification time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 148 | 156 | Header checksum, stored as an octal number in ASCII. - /// 156 | 157 | Link flag, determines the kind of header. - /// 157 | 257 | Link name, stored as a string. - /// 257 | 512 | NUL pad. - /// - /// Unused bytes are set to NUL ('\x00')s - /// - /// Reference: - /// https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&format=html - /// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188 - /// http://cdrtools.sourceforge.net/private/man/star/star.4.html - v7(1, 'V7'), - - /// USTAR (Unix Standard TAR) header format defined in POSIX.1-1988. - /// - /// The structure of the USTAR Header consists of the following: - /// - /// Start | End | Field - /// ========================================================================= - /// 0 | 100 | Path name, stored as null-terminated string. - /// 100 | 108 | File mode, stored as an octal number in ASCII. - /// 108 | 116 | User id of owner, as octal number in ASCII. - /// 116 | 124 | Group id of owner, as octal number in ASCII. - /// 124 | 136 | Size of file, as octal number in ASCII. - /// 136 | 148 | Modification time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 148 | 156 | Header checksum, stored as an octal number in ASCII. - /// 156 | 157 | Type flag, determines the kind of header. - /// Note that the meaning of the size field depends on the type. - /// 157 | 257 | Link name, stored as a string. - /// 257 | 263 | Contains the magic value "ustar\x00" to indicate that this is - /// the USTAR format. Full compliance requires user name and - /// group name fields to be set. - /// 263 | 265 | Version. "00" for POSIX standard archives. - /// 265 | 297 | User name, as null-terminated ASCII string. - /// 297 | 329 | Group name, as null-terminated ASCII string. - /// 329 | 337 | Major number for character or block device entry. - /// 337 | 345 | Minor number for character or block device entry. - /// 345 | 500 | Prefix. If the pathname is too long to fit in the 100 bytes - /// provided at the start, it can be split at any / character - /// with the first portion going here. - /// 500 | 512 | NUL pad. - /// - /// Unused bytes are set to NUL ('\x00')s - /// - /// User and group names should be used in preference to uid/gid values when - /// they are set and the corresponding names exist on the system. - /// - /// While this format is compatible with most tar readers, the format has - /// several limitations making it unsuitable for some usages. Most notably, it - /// cannot support sparse files, files larger than 8GiB, filenames larger than - /// 256 characters, and non-ASCII filenames. - /// - /// Reference: - /// https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&format=html - /// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188 - /// http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06 - ustar(2, 'USTAR'), - - /// PAX header format defined in POSIX.1-2001. - /// - /// PAX extends USTAR by writing a special file with either the `x` or `g` - /// type flags to allow for attributes that are not conveniently stored in a - /// POSIX ustar archive to be held. - /// - /// Some newer formats add their own extensions to PAX by defining their - /// own keys and assigning certain semantic meaning to the associated values. - /// For example, sparse file support in PAX is implemented using keys - /// defined by the GNU manual (e.g., "GNU.sparse.map"). - /// - /// Reference: - /// https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&format=html - /// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188 - /// http://cdrtools.sourceforge.net/private/man/star/star.4.html - /// http://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html - pax(4, 'PAX'), - - /// GNU header format. - /// - /// The GNU header format is older than the USTAR and PAX standards and - /// is not compatible with them. The GNU format supports - /// arbitrary file sizes, filenames of arbitrary encoding and length, - /// sparse files, and other features. - /// - /// Start | End | Field - /// ========================================================================= - /// 0 | 100 | Path name, stored as null-terminated string. - /// 100 | 108 | File mode, stored as an octal number in ASCII. - /// 108 | 116 | User id of owner, as octal number in ASCII. - /// 116 | 124 | Group id of owner, as octal number in ASCII. - /// 124 | 136 | Size of file, as octal number in ASCII. - /// 136 | 148 | Modification time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 148 | 156 | Header checksum, stored as an octal number in ASCII. - /// 156 | 157 | Type flag, determines the kind of header. - /// Note that the meaning of the size field depends on the type. - /// 157 | 257 | Link name, stored as a string. - /// 257 | 263 | Contains the magic value "ustar " to indicate that this is - /// the GNU format. - /// 263 | 265 | Version. " \x00" for POSIX standard archives. - /// 265 | 297 | User name, as null-terminated ASCII string. - /// 297 | 329 | Group name, as null-terminated ASCII string. - /// 329 | 337 | Major number for character or block device entry. - /// 337 | 345 | Minor number for character or block device entry. - /// 345 | 357 | Last Access time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 357 | 369 | Last Changed time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 369 | 381 | Offset - not used. - /// 381 | 385 | Longnames - deprecated - /// 385 | 386 | Unused. - /// 386 | 482 | Sparse data - 4 sets of (offset, numbytes) stored as - /// octal numbers in ASCII. - /// 482 | 483 | isExtended - if this field is non-zero, this header is - /// followed by additional sparse records, which are in the - /// same format as above. - /// 483 | 495 | Binary representation of the file's complete size, inclusive - /// of the sparse data. - /// 495 | 512 | NUL pad. - /// - /// It is recommended that PAX be chosen over GNU unless the target - /// application can only parse GNU formatted archives. - /// - /// Reference: - /// https://www.gnu.org/software/tar/manual/html_node/Standard.html - gnu(8, 'GNU'), - - /// Schily's TAR format, which is incompatible with USTAR. - /// This does not cover STAR extensions to the PAX format; these fall under - /// the PAX format. - /// - /// Start | End | Field - /// ========================================================================= - /// 0 | 100 | Path name, stored as null-terminated string. - /// 100 | 108 | File mode, stored as an octal number in ASCII. - /// 108 | 116 | User id of owner, as octal number in ASCII. - /// 116 | 124 | Group id of owner, as octal number in ASCII. - /// 124 | 136 | Size of file, as octal number in ASCII. - /// 136 | 148 | Modification time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 148 | 156 | Header checksum, stored as an octal number in ASCII. - /// 156 | 157 | Type flag, determines the kind of header. - /// Note that the meaning of the size field depends on the type. - /// 157 | 257 | Link name, stored as a string. - /// 257 | 263 | Contains the magic value "ustar\x00" to indicate that this is - /// the GNU format. - /// 263 | 265 | Version. "00" for STAR archives. - /// 265 | 297 | User name, as null-terminated ASCII string. - /// 297 | 329 | Group name, as null-terminated ASCII string. - /// 329 | 337 | Major number for character or block device entry. - /// 337 | 345 | Minor number for character or block device entry. - /// 345 | 476 | Prefix. If the pathname is too long to fit in the 100 bytes - /// provided at the start, it can be split at any / character - /// with the first portion going here. - /// 476 | 488 | Last Access time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 488 | 500 | Last Changed time of file, number of seconds from epoch, - /// stored as an octal number in ASCII. - /// 500 | 508 | NUL pad. - /// 508 | 512 | Trailer - "tar\x00". - /// - /// Reference: - /// http://cdrtools.sourceforge.net/private/man/star/star.4.html - star(16, 'STAR'), - ; - - @override - final int _value; - - final String _name; - - const TarFormat(this._value, this._name); - - @override - bool get valid => true; - - @override - MaybeTarFormat operator |(TarFormat other) { - return other == this ? this : _MaybeTarFormat(_value | other._value); - } - - @override - bool has(MaybeTarFormat other) { - return other == this; - } - - @override - MaybeTarFormat mayOnlyBe(MaybeTarFormat other) { - return MaybeTarFormat._(_value & other._value); - } - - @override - String toString() => _name; -} - -final class _MaybeTarFormat implements MaybeTarFormat { - // Note: We never represent a single tar format in a _MaybeTarFormat, these - // are represented in the TarFormat enum. - @override - final int _value; - - const _MaybeTarFormat(this._value); - - @override - int get hashCode => _value; - - @override - bool operator ==(Object other) { - if (other is! TarFormat) return false; - - return _value == other._value; - } - - @override - String toString() { - if (!valid) return 'Invalid'; - - return TarFormat.values.where(has).map((e) => e._name).join(' or '); - } - - @override - bool has(MaybeTarFormat other) => _value & other._value != 0; - - @override - bool get valid => _value != 0; - - @override - MaybeTarFormat mayOnlyBe(MaybeTarFormat other) { - return MaybeTarFormat._(_value & other._value); - } - - @override - MaybeTarFormat operator |(TarFormat other) { - return MaybeTarFormat._(_value | other._value); - } -} diff --git a/lib/src/third_party/tar/lib/src/header.dart b/lib/src/third_party/tar/lib/src/header.dart deleted file mode 100644 index e18cddf57..000000000 --- a/lib/src/third_party/tar/lib/src/header.dart +++ /dev/null @@ -1,409 +0,0 @@ -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; - -import 'constants.dart'; -import 'exception.dart'; -import 'format.dart'; -import 'utils.dart'; - -/// Type flags for [TarHeader]. -/// -/// The type flag of a header indicates the kind of file associated with the -/// entry. This enum contains the various type flags over the different TAR -/// formats, and users should be careful that the type flag corresponds to the -/// TAR format they are working with. -enum TypeFlag { - /// [reg] indicates regular files. - /// - /// Old tar implementations have a seperate `TypeRegA` value. This library - /// will transparently read those as [regA]. - reg, - - /// Legacy-version of [reg] in old tar implementations. - /// - /// This is only used internally. - regA, - - /// Hard link - header-only, may not have a data body - link, - - /// Symbolic link - header-only, may not have a data body - symlink, - - /// Character device node - header-only, may not have a data body - char, - - /// Block device node - header-only, may not have a data body - block, - - /// Directory - header-only, may not have a data body - dir, - - /// FIFO node - header-only, may not have a data body - fifo, - - /// Currently does not have any meaning, but is reserved for the future. - reserved, - - /// Used by the PAX format to store key-value records that are only relevant - /// to the next file. - /// - /// This package transparently handles these types. - xHeader, - - /// Used by the PAX format to store key-value records that are relevant to all - /// subsequent files. - /// - /// This package only supports parsing and composing such headers, - /// but does not currently support persisting the global state across files. - xGlobalHeader, - - /// Indiates a sparse file in the GNU format - gnuSparse, - - /// Used by the GNU format for a meta file to store the path or link name for - /// the next file. - /// This package transparently handles these types. - gnuLongName, - gnuLongLink, - - /// Vendor specific typeflag, as defined in POSIX.1-1998. Seen as outdated but - /// may still exist on old files. - /// - /// This library uses a single enum to catch them all. - vendor -} - -/// Header of a tar entry -/// -/// A tar header stores meta-information about the matching tar entry, such as -/// its name. -sealed class TarHeader { - /// Type of header entry. In the V7 TAR format, this field was known as the - /// link flag. - TypeFlag get typeFlag; - - /// Name of file or directory entry. - String get name; - - /// Target name of link (valid for hard links or symbolic links). - String? get linkName; - - /// Permission and mode bits. - int get mode; - - /// User ID of owner. - int get userId; - - /// Group ID of owner. - int get groupId; - - /// User name of owner. - String? get userName; - - /// Group name of owner. - String? get groupName; - - /// Logical file size in bytes. - int get size; - - /// The time of the last change to the data of the TAR file. - DateTime get modified; - - /// The time of the last access to the data of the TAR file. - DateTime? get accessed; - - /// The time of the last change to the data or metadata of the TAR file. - DateTime? get changed; - - /// Major device number - int get devMajor; - - /// Minor device number - int get devMinor; - - /// The TAR format of the header. - /// - /// When this library is sure it knows the format of the tar entry, this will - /// be a [TarFormat] enum value. In other cases, a [MaybeTarFormat] could - /// represent multiple possible formats. - MaybeTarFormat get format; - - /// Checks if this header indicates that the file will have content. - bool get hasContent { - switch (typeFlag) { - case TypeFlag.link: - case TypeFlag.symlink: - case TypeFlag.block: - case TypeFlag.dir: - case TypeFlag.char: - case TypeFlag.fifo: - return false; - default: - return true; - } - } - - /// Creates a tar header from the individual field. - factory TarHeader({ - required String name, - TarFormat? format, - TypeFlag? typeFlag, - DateTime? modified, - String? linkName, - int mode = 0, - int size = -1, - String? userName, - int userId = 0, - int groupId = 0, - String? groupName, - DateTime? accessed, - DateTime? changed, - int devMajor = 0, - int devMinor = 0, - }) { - return HeaderImpl.internal( - name: name, - modified: modified ?? DateTime.fromMillisecondsSinceEpoch(0), - format: format ?? TarFormat.pax, - typeFlag: typeFlag ?? TypeFlag.reg, - linkName: linkName, - mode: mode, - size: size, - userName: userName, - userId: userId, - groupId: groupId, - groupName: groupName, - accessed: accessed, - changed: changed, - devMajor: devMajor, - devMinor: devMinor, - ); - } - - TarHeader._(); -} - -@internal -class HeaderImpl extends TarHeader { - TypeFlag internalTypeFlag; - - @override - String name; - - @override - String? linkName; - - @override - int mode; - - @override - int userId; - - @override - int groupId; - - @override - String? userName; - - @override - String? groupName; - - @override - int size; - - @override - DateTime modified; - - @override - DateTime? accessed; - - @override - DateTime? changed; - - @override - int devMajor; - - @override - int devMinor; - - @override - MaybeTarFormat format; - - @override - TypeFlag get typeFlag { - return switch (internalTypeFlag) { - TypeFlag.regA => TypeFlag.reg, // normalize - final other => other, - }; - } - - /// This constructor is meant to help us deal with header-only headers (i.e. - /// meta-headers that only describe the next file instead of being a header - /// to files themselves) - HeaderImpl.internal({ - required this.name, - required this.modified, - required this.format, - required TypeFlag typeFlag, - this.linkName, - this.mode = 0, - this.size = -1, - this.userName, - this.userId = 0, - this.groupId = 0, - this.groupName, - this.accessed, - this.changed, - this.devMajor = 0, - this.devMinor = 0, - }) : internalTypeFlag = typeFlag, - super._(); - - factory HeaderImpl.parseBlock(Uint8List headerBlock, - {Map paxHeaders = const {}}) { - assert(headerBlock.length == 512); - - final format = _getFormat(headerBlock); - final size = paxHeaders.size ?? headerBlock.readOctal(124, 12); - - // Start by reading data available in every format. - final header = HeaderImpl.internal( - format: format, - name: headerBlock.readString(0, 100), - mode: headerBlock.readOctal(100, 8), - // These should be octal, but some weird tar implementations ignore that?! - // Encountered with package:RAL, version 1.28.0 on pub - userId: headerBlock.readNumeric(108, 8), - groupId: headerBlock.readNumeric(116, 8), - size: size, - modified: secondsSinceEpoch(headerBlock.readOctal(136, 12)), - typeFlag: typeflagFromByte(headerBlock[156]), - linkName: headerBlock.readStringOrNullIfEmpty(157, 100), - ); - - if (header.hasContent && size < 0) { - throw TarException.header('Indicates an invalid size of $size'); - } - - if (format.valid && format != TarFormat.v7) { - // If it's a valid header that is not of the v7 format, it will have the - // USTAR fields - header - ..userName ??= headerBlock.readStringOrNullIfEmpty(265, 32) - ..groupName ??= headerBlock.readStringOrNullIfEmpty(297, 32) - ..devMajor = headerBlock.readNumeric(329, 8) - ..devMinor = headerBlock.readNumeric(337, 8); - - // Prefix to the file name - var prefix = ''; - if (format.has(TarFormat.ustar) || format.has(TarFormat.pax)) { - prefix = headerBlock.readString(345, 155); - - if (headerBlock.any(isNotAscii)) { - header.format = format.mayOnlyBe(TarFormat.pax); - } - } else if (format.has(TarFormat.star)) { - prefix = headerBlock.readString(345, 131); - header - ..accessed = secondsSinceEpoch(headerBlock.readNumeric(476, 12)) - ..changed = secondsSinceEpoch(headerBlock.readNumeric(488, 12)); - } else if (format.has(TarFormat.gnu)) { - header.format = TarFormat.gnu; - - if (headerBlock[345] != 0) { - header.accessed = secondsSinceEpoch(headerBlock.readNumeric(345, 12)); - } - - if (headerBlock[357] != 0) { - header.changed = secondsSinceEpoch(headerBlock.readNumeric(357, 12)); - } - } - - if (prefix.isNotEmpty) { - header.name = '$prefix/${header.name}'; - } - } - - return header.._applyPaxHeaders(paxHeaders); - } - - void _applyPaxHeaders(Map headers) { - for (final entry in headers.entries) { - if (entry.value == '') { - continue; // Keep the original USTAR value - } - - switch (entry.key) { - case paxPath: - name = entry.value; - break; - case paxLinkpath: - linkName = entry.value; - break; - case paxUname: - userName = entry.value; - break; - case paxGname: - groupName = entry.value; - break; - case paxUid: - userId = parseInt(entry.value); - break; - case paxGid: - groupId = parseInt(entry.value); - break; - case paxAtime: - accessed = parsePaxTime(entry.value); - break; - case paxMtime: - modified = parsePaxTime(entry.value); - break; - case paxCtime: - changed = parsePaxTime(entry.value); - break; - case paxSize: - size = parseInt(entry.value); - break; - default: - break; - } - } - } -} - -/// Checks that [rawHeader] represents a valid tar header based on the -/// checksum, and then attempts to guess the specific format based -/// on magic values. If the checksum fails, then an error is thrown. -MaybeTarFormat _getFormat(Uint8List rawHeader) { - final checksum = rawHeader.readOctal(checksumOffset, checksumLength); - - // Modern TAR archives use the unsigned checksum, but we check the signed - // checksum as well for compatibility. - if (checksum != rawHeader.computeUnsignedHeaderChecksum() && - checksum != rawHeader.computeSignedHeaderChecksum()) { - throw TarException.header('Checksum does not match'); - } - - final hasUstarMagic = rawHeader.matchesHeader(magicUstar); - if (hasUstarMagic) { - return rawHeader.matchesHeader(trailerStar, offset: starTrailerOffset) - ? TarFormat.star - : TarFormat.ustar | TarFormat.pax; - } - - if (rawHeader.matchesHeader(magicGnu) && - rawHeader.matchesHeader(versionGnu, offset: versionOffset)) { - return TarFormat.gnu; - } - - return TarFormat.v7; -} - -extension _ReadPaxHeaders on Map { - int? get size { - final sizeStr = this[paxSize]; - return sizeStr == null ? null : int.tryParse(sizeStr); - } -} diff --git a/lib/src/third_party/tar/lib/src/reader.dart b/lib/src/third_party/tar/lib/src/reader.dart deleted file mode 100644 index c16715159..000000000 --- a/lib/src/third_party/tar/lib/src/reader.dart +++ /dev/null @@ -1,1012 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; -import 'package:typed_data/typed_data.dart'; - -import 'charcodes.dart'; -import 'constants.dart'; -import 'entry.dart'; -import 'exception.dart'; -import 'format.dart'; -import 'header.dart'; -import 'sparse.dart'; -import 'utils.dart'; - -/// [TarReader] provides sequential access to the TAR files in a TAR archive. -/// It is designed to read from a stream and to spit out substreams for -/// individual file contents in order to minimize the amount of memory needed -/// to read each archive where possible. -final class TarReader implements StreamIterator { - final BlockReader _reader; - final PaxHeaders _paxHeaders = PaxHeaders(); - final int _maxSpecialFileSize; - - TarEntry? _current; - _CurrentEntryStream? _currentStream; - - /// Whether we're in the process of reading tar headers. - bool _isReadingHeaders = false; - - /// Whether this tar reader is terminally done. - /// - /// That is the case if: - /// - [cancel] was called - /// - [moveNext] completed to `false` once. - /// - [moveNext] completed to an error - /// - an error was emitted through a tar entry's content stream - bool _isDone = false; - - /// Whether we should ensure that the stream emits no further data after the - /// end of the tar file was reached. - final bool _checkNoTrailingData; - - /// Creates a tar reader reading from the raw [tarStream]. - /// - /// The [disallowTrailingData] parameter can be enabled to assert that the - /// [tarStream] contains exactly one tar archive before ending. - /// When [disallowTrailingData] is disabled (which is the default), the reader - /// will automatically cancel its stream subscription when [moveNext] returns - /// `false`. - /// When it is enabled and a marker indicating the end of an archive is - /// encountered, [moveNext] will wait for further events on the stream. If - /// further data is received, a [TarException] will be thrown and the - /// subscription will be cancelled. Otherwise, [moveNext] effectively waits - /// for a done event, making a cancellation unecessary. - /// Depending on the input stream, cancellations may cause unintended - /// side-effects. In that case, [disallowTrailingData] can be used to ensure - /// that the stream is only cancelled if it emits an invalid tar file. - /// - /// The [maxSpecialFileSize] parameter can be used to limit the maximum length - /// of hidden entries in the tar stream. These entries include extended PAX - /// headers or long names in GNU tar. The content of those entries has to be - /// buffered in the parser to properly read the following tar entries. To - /// avoid memory-based denial-of-service attacks, this library limits their - /// maximum length. Changing the default of 2 KiB is rarely necessary. - TarReader(Stream> tarStream, - {int maxSpecialFileSize = defaultSpecialLength, - bool disallowTrailingData = false}) - : _reader = BlockReader(tarStream), - _checkNoTrailingData = disallowTrailingData, - _maxSpecialFileSize = maxSpecialFileSize; - - @override - TarEntry get current { - final current = _current; - - if (current == null) { - throw StateError('Invalid call to TarReader.current. \n' - 'Did you call and await next() and checked that it returned true?'); - } - - return current; - } - - /// Reads the tar stream up until the beginning of the next logical file. - /// - /// If such file exists, the returned future will complete with `true`. After - /// the future completes, the next tar entry will be evailable in [current]. - /// - /// If no such file exists, the future will complete with `false`. - /// The future might complete with an [TarException] if the tar stream is - /// malformed or ends unexpectedly. - /// If the future completes with `false` or an exception, the reader will - /// [cancel] itself and release associated resources. Thus, it is invalid to - /// call [moveNext] again in that case. - @override - Future moveNext() async { - await _prepareToReadHeaders(); - try { - return await _moveNextInternal(); - } on Object { - await cancel(); - rethrow; - } - } - - /// Consumes the stream up to the contents of the next logical tar entry. - /// Will cancel the underlying subscription when returning false, but not when - /// it throws. - Future _moveNextInternal() async { - // We're reading a new logical file, so clear the local pax headers - _paxHeaders.clearLocals(); - - var gnuLongName = ''; - var gnuLongLink = ''; - var eofAcceptable = true; - - var format = TarFormat.ustar | - TarFormat.pax | - TarFormat.gnu | - TarFormat.v7 | - TarFormat.star; - - HeaderImpl? nextHeader; - - // Externally, [moveNext] iterates through the tar archive as if it is a - // series of files. Internally, the tar format often uses fake "files" to - // add meta data that describes the next file. These meta data "files" - // should not normally be visible to the outside. As such, this loop - // iterates through one or more "header files" until it finds a - // "normal file". - while (true) { - final rawHeader = await _readFullBlock(allowEmpty: eofAcceptable); - - nextHeader = await _readHeader(rawHeader); - if (nextHeader == null) { - if (eofAcceptable) { - await _handleExpectedEof(); - return false; - } else { - _unexpectedEof(); - } - } - - // We're beginning to read a file, if the tar file ends now something is - // wrong - eofAcceptable = false; - format = format.mayOnlyBe(nextHeader.format); - - // Check for PAX/GNU special headers and files. - if (nextHeader.typeFlag == TypeFlag.xHeader || - nextHeader.typeFlag == TypeFlag.xGlobalHeader) { - format = format.mayOnlyBe(TarFormat.pax); - final paxHeaderSize = _checkSpecialSize(nextHeader.size); - - final rawPaxHeaders = - (await _readFullBlock(amount: numBlocks(paxHeaderSize))) - .sublistView(0, paxHeaderSize); - - _paxHeaders.readPaxHeaders( - rawPaxHeaders, nextHeader.typeFlag == TypeFlag.xGlobalHeader); - - // This is a meta header affecting the next header. - continue; - } else if (nextHeader.typeFlag == TypeFlag.gnuLongLink || - nextHeader.typeFlag == TypeFlag.gnuLongName) { - format = format.mayOnlyBe(TarFormat.gnu); - final size = _checkSpecialSize(nextHeader.size); - final realName = await _readFullBlock(amount: numBlocks(size)); - - final readName = realName.readString(0, realName.length); - if (nextHeader.typeFlag == TypeFlag.gnuLongName) { - gnuLongName = readName; - } else { - gnuLongLink = readName; - } - - // This is a meta header affecting the next header. - continue; - } else { - // The old GNU sparse format is handled here since it is technically - // just a regular file with additional attributes. - - if (gnuLongName.isNotEmpty) nextHeader.name = gnuLongName; - if (gnuLongLink.isNotEmpty) nextHeader.linkName = gnuLongLink; - - if (nextHeader.internalTypeFlag == TypeFlag.regA) { - /// Legacy archives use trailing slash for directories - if (nextHeader.name.endsWith('/')) { - nextHeader.internalTypeFlag = TypeFlag.dir; - } else { - nextHeader.internalTypeFlag = TypeFlag.reg; - } - } - - final content = await _handleFile(nextHeader, rawHeader); - - // Set the final guess at the format - if (format.has(TarFormat.ustar) && format.has(TarFormat.pax)) { - format = format.mayOnlyBe(TarFormat.ustar); - } - nextHeader.format = format; - - _current = TarEntry(nextHeader, content); - final currentStreams = _currentStream; - assert(currentStreams == null || - currentStreams.state == _EntryStreamState.preListen); - _isReadingHeaders = false; - return true; - } - } - } - - @override - Future cancel() async { - if (_isDone) return; - - _isDone = true; - _current = null; - _currentStream = null; - _isReadingHeaders = false; - - // Note: Calling cancel is safe when the stream has already been completed. - // It's a noop in that case, which is what we want. - return _reader.close(); - } - - /// Utility function for quickly iterating through all entries in [tarStream]. - static Future forEach(Stream> tarStream, - FutureOr Function(TarEntry entry) action) async { - final reader = TarReader(tarStream); - try { - while (await reader.moveNext()) { - await action(reader.current); - } - } finally { - await reader.cancel(); - } - } - - /// Ensures that this reader can safely read headers now. - /// - /// This methods prevents: - /// * concurrent calls to [moveNext] - /// * a call to [moveNext] while a stream is active: - /// * if [TarEntry.contents] has never been listened to, or if it has a - /// cancelled subscription, we drain the stream. - /// * otherwise, throws a [StateError] - Future _prepareToReadHeaders() async { - if (_isDone) { - throw StateError('Tried to call TarReader.moveNext() on a canceled ' - 'reader. \n' - 'Note that a reader is canceled when moveNext() throws or returns ' - 'false.'); - } - - if (_isReadingHeaders) { - throw StateError('Concurrent call to TarReader.moveNext() detected. \n' - 'Please await all calls to Reader.moveNext().'); - } - _isReadingHeaders = true; - - final underlyingStream = _currentStream; - if (underlyingStream != null) { - switch (underlyingStream.state) { - case _EntryStreamState.preListen: - await underlyingStream.drain(); - // The stream should reset when drained (we do this in _publishStream) - assert(_currentStream == null); - - break; - case _EntryStreamState.subscriptionActive: - throw StateError( - 'Illegal call to TarReader.moveNext() while a previous stream was ' - 'active.\n' - 'When listening to tar contents, make sure the stream is ' - 'complete or cancelled before calling TarReader.moveNext() again.', - ); - case _EntryStreamState.cancelled: - // ignore: cancel_subscriptions - final subscription = underlyingStream._sourceSubscription!; - - // Re-purpose the existing subscription to drain the stream - assert(subscription.isPaused); - - subscription - ..onData(null) - ..resume(); - - try { - await subscription.asFuture(); - } on Object { - await cancel(); - rethrow; - } finally { - // This also draines the stream - _currentStream = null; - } - - break; - case _EntryStreamState.done: - assert( - false, - 'Unreachable: There should not be a currentStream in a done state, ' - 'as the stream is no longer current at that point.', - ); - break; - } - } - } - - int _checkSpecialSize(int size) { - if (size > _maxSpecialFileSize) { - throw TarException( - 'TAR file contains hidden entry with an invalid size of $size.'); - } - - return size; - } - - /// Ater we detected the end of a tar file, optionally check for trailing - /// data. - Future _handleExpectedEof() async { - if (_checkNoTrailingData) { - // Trailing zeroes are okay, but don't allow any more data here. - Uint8List block; - - do { - block = await _reader.nextBlock(); - if (!block.isAllZeroes) { - throw TarException( - 'Illegal content after the end of the tar archive.'); - } - } while (block.length == blockSize); - // The stream is done when we couldn't read the full block. - } - - await cancel(); - } - - Never _unexpectedEof() { - throw TarException.header('Unexpected end of file'); - } - - /// Reads [amount] blocks from the input stream, or throws an exception if - /// the stream ends prematurely. - Future _readFullBlock({bool allowEmpty = false, int amount = 1}) { - final blocks = Uint8List(amount * blockSize); - var offset = 0; - - return _reader.nextBlocks(amount).forEach((chunk) { - blocks.setAll(offset, chunk); - offset += chunk.length; - }).then((void _) { - if (allowEmpty && offset == 0) { - return Uint8List(0); - } else if (offset < blocks.length) { - _unexpectedEof(); - } else { - return blocks; - } - }); - } - - /// Reads the next block header and assumes that the underlying reader - /// is already aligned to a block boundary. It returns the raw block of the - /// header in case further processing is required. - /// - /// EOF is hit when one of the following occurs: - /// * Exactly 0 bytes are read and EOF is hit. - /// * Exactly 1 block of zeros is read and EOF is hit. - /// * At least 2 blocks of zeros are read. - Future _readHeader(Uint8List rawHeader) async { - // Exactly 0 bytes are read and EOF is hit. - if (rawHeader.isEmpty) return null; - - if (rawHeader.isAllZeroes) { - rawHeader = await _reader.nextBlock(); - - // Exactly 1 block of zeroes is read and EOF is hit. - if (rawHeader.isEmpty) return null; - - if (rawHeader.isAllZeroes) { - // Two blocks of zeros are read - Normal EOF. - return null; - } - - throw TarException('Encountered a non-zero block after a zero block'); - } - - return HeaderImpl.parseBlock(rawHeader, paxHeaders: _paxHeaders); - } - - /// Creates a stream of the next entry's content - Future>> _handleFile( - HeaderImpl header, Uint8List rawHeader) async { - List? sparseData; - if (header.typeFlag == TypeFlag.gnuSparse) { - sparseData = await _readOldGNUSparseMap(header, rawHeader); - } else { - sparseData = await _readGNUSparsePAXHeaders(header); - } - - if (sparseData != null) { - if (header.hasContent && - !validateSparseEntries(sparseData, header.size)) { - throw TarException.header('Invalid sparse file header.'); - } - - final sparseHoles = invertSparseEntries(sparseData, header.size); - final sparseDataLength = - sparseData.fold(0, (value, element) => value + element.length); - - final streamBlockCount = numBlocks(sparseDataLength); - final safeStream = _publishStream( - _reader.nextBlocks(streamBlockCount), streamBlockCount * blockSize); - return sparseStream(safeStream, sparseHoles, header.size); - } else { - var size = header.size; - if (!header.hasContent) size = 0; - - if (size < 0) { - throw TarException.header('Invalid size ($size) detected!'); - } - - if (size == 0) { - return _publishStream(const Stream.empty(), 0); - } else { - final blockCount = numBlocks(header.size); - return _publishStream(_reader.nextBlocks(blockCount), header.size); - } - } - } - - /// Publishes an library-internal stream for users. - /// - /// This adds a check to ensure that the stream we're exposing has the - /// expected length. It also sets the [_currentStream] field and resets it - /// when it's done. - Stream> _publishStream(Stream stream, int length) { - // There can only be one content stream at a time. This precondition is - // checked by _prepareToReadHeaders. - assert(_currentStream == null); - - return _currentStream = _CurrentEntryStream(this, stream, length); - } - - /// Checks the PAX headers for GNU sparse headers. - /// If they are found, then this function reads the sparse map and returns it. - /// This assumes that 0.0 headers have already been converted to 0.1 headers - /// by the PAX header parsing logic. - Future?> _readGNUSparsePAXHeaders(HeaderImpl header) async { - /// Identify the version of GNU headers. - var isVersion1 = false; - final major = _paxHeaders[paxGNUSparseMajor]; - final minor = _paxHeaders[paxGNUSparseMinor]; - - final sparseMapHeader = _paxHeaders[paxGNUSparseMap]; - if (major == '0' && (minor == '0' || minor == '1') || - // assume 0.0 or 0.1 if no version header is set - sparseMapHeader != null && sparseMapHeader.isNotEmpty) { - isVersion1 = false; - } else if (major == '1' && minor == '0') { - isVersion1 = true; - } else { - // Unknown version that we don't support - return null; - } - - header.format |= TarFormat.pax; - - /// Update [header] from GNU sparse PAX headers. - final possibleName = _paxHeaders[paxGNUSparseName] ?? ''; - if (possibleName.isNotEmpty) { - header.name = possibleName; - } - - final possibleSize = - _paxHeaders[paxGNUSparseSize] ?? _paxHeaders[paxGNUSparseRealSize]; - - if (possibleSize != null && possibleSize.isNotEmpty) { - final size = int.tryParse(possibleSize, radix: 10); - if (size == null) { - throw TarException.header('Invalid PAX size ($possibleSize) detected'); - } - - header.size = size; - } - - // Read the sparse map according to the appropriate format. - if (isVersion1) { - return await _readGNUSparseMap1x0(); - } - - return _readGNUSparseMap0x1(header); - } - - /// Reads the sparse map as stored in GNU's PAX sparse format version 1.0. - /// The format of the sparse map consists of a series of newline-terminated - /// numeric fields. The first field is the number of entries and is always - /// present. Following this are the entries, consisting of two fields - /// (offset, length). This function must stop reading at the end boundary of - /// the block containing the last newline. - /// - /// Note that the GNU manual says that numeric values should be encoded in - /// octal format. However, the GNU tar utility itself outputs these values in - /// decimal. As such, this library treats values as being encoded in decimal. - Future> _readGNUSparseMap1x0() async { - var newLineCount = 0; - final block = Uint8Queue(); - - /// Ensures that [block] h as at least [n] tokens. - Future feedTokens(int n) async { - while (newLineCount < n) { - final newBlock = await _readFullBlock(); - if (newBlock.length < blockSize) { - throw TarException.header( - 'GNU Sparse Map does not have enough lines!'); - } - - block.addAll(newBlock); - newLineCount += newBlock.where((byte) => byte == $lf).length; - } - } - - /// Get the next token delimited by a newline. This assumes that - /// at least one newline exists in the buffer. - String nextToken() { - newLineCount--; - final nextNewLineIndex = block.indexOf($lf); - final result = block.sublist(0, nextNewLineIndex); - block.removeRange(0, nextNewLineIndex + 1); - return result.readString(0, nextNewLineIndex); - } - - await feedTokens(1); - - // Parse for the number of entries. - // Use integer overflow resistant math to check this. - final numEntriesString = nextToken(); - final numEntries = int.tryParse(numEntriesString); - if (numEntries == null || numEntries < 0 || 2 * numEntries < numEntries) { - throw TarException.header( - 'Invalid sparse map number of entries: $numEntriesString!'); - } - - // Parse for all member entries. - // [numEntries] is trusted after this since a potential attacker must have - // committed resources proportional to what this library used. - await feedTokens(2 * numEntries); - - final sparseData = []; - - for (var i = 0; i < numEntries; i++) { - final offsetToken = nextToken(); - final lengthToken = nextToken(); - - final offset = int.tryParse(offsetToken); - final length = int.tryParse(lengthToken); - - if (offset == null || length == null) { - throw TarException.header( - 'Failed to read a GNU sparse map entry. Encountered ' - 'offset: $offsetToken, length: $lengthToken'); - } - - sparseData.add(SparseEntry(offset, length)); - } - return sparseData; - } - - /// Reads the sparse map as stored in GNU's PAX sparse format version 0.1. - /// The sparse map is stored in the PAX headers and is stored like this: - /// `offset₀,size₀,offset₁,size₁...` - List _readGNUSparseMap0x1(TarHeader header) { - // Get number of entries, check for integer overflows - final numEntriesString = _paxHeaders[paxGNUSparseNumBlocks]; - final numEntries = - numEntriesString != null ? int.tryParse(numEntriesString) : null; - - if (numEntries == null || numEntries < 0 || 2 * numEntries < numEntries) { - throw TarException.header('Invalid GNU version 0.1 map'); - } - - // There should be two numbers in [sparseMap] for each entry. - final sparseMap = _paxHeaders[paxGNUSparseMap]?.split(','); - if (sparseMap == null) { - throw TarException.header('Invalid GNU version 0.1 map'); - } - - if (sparseMap.length != 2 * numEntries) { - throw TarException.header( - 'Detected sparse map length ${sparseMap.length} ' - 'that is not twice the number of entries $numEntries'); - } - - /// Loop through sparse map entries. - /// [numEntries] is now trusted. - final sparseData = []; - for (var i = 0; i < sparseMap.length; i += 2) { - final offset = int.tryParse(sparseMap[i]); - final length = int.tryParse(sparseMap[i + 1]); - - if (offset == null || length == null) { - throw TarException.header( - 'Failed to read a GNU sparse map entry. Encountered ' - 'offset: $offset, length: $length'); - } - - sparseData.add(SparseEntry(offset, length)); - } - - return sparseData; - } - - /// Reads the sparse map from the old GNU sparse format. - /// The sparse map is stored in the tar header if it's small enough. - /// If it's larger than four entries, then one or more extension headers are - /// used to store the rest of the sparse map. - /// - /// [TarHeader.size] does not reflect the size of any extended headers used. - /// Thus, this function will read from the chunked stream iterator to fetch - /// extra headers. - /// - /// See also: https://www.gnu.org/software/tar/manual/html_section/tar_94.html#SEC191 - Future> _readOldGNUSparseMap( - HeaderImpl header, Uint8List rawHeader) async { - // Make sure that the input format is GNU. - // Unfortunately, the STAR format also has a sparse header format that uses - // the same type flag but has a completely different layout. - if (header.format != TarFormat.gnu) { - throw TarException.header('Tried to read sparse map of non-GNU header'); - } - - // Read the real size of the file when sparse holes are expanded. - header.size = rawHeader.readNumeric(483, 12); - final sparseEntries = []; - - bool readEntry(Uint8List source, int offset) { - // If a sparse header starts with a null byte, it marks the end of the - // sparse structures. - if (rawHeader[offset] == 0) return false; - - final fileOffset = source.readNumeric(offset, 12); - final length = source.readNumeric(offset + 12, 12); - - sparseEntries.add(SparseEntry(fileOffset, length)); - return true; - } - - // The first four sparse headers are stored in the tar header itself - for (var i = 0; i < 4; i++) { - final offset = 386 + 24 * i; - if (!readEntry(rawHeader, offset)) break; - } - - var isExtended = rawHeader[482] != 0; - - while (isExtended) { - // Ok, we have a new block of sparse headers to process - final block = await _readFullBlock(); - - // A full block of sparse data contains up to 21 entries - for (var i = 0; i < 21; i++) { - if (!readEntry(block, i * 24)) break; - } - - // The last bytes indicates whether another sparse header block follows. - isExtended = block[504] != 0; - } - - return sparseEntries; - } -} - -@internal -final class PaxHeaders extends UnmodifiableMapBase { - final Map _globalHeaders = {}; - Map _localHeaders = {}; - - /// Applies new global PAX-headers from the map. - /// - /// The [headers] will replace global headers with the same key, but leave - /// others intact. - void newGlobals(Map headers) { - _globalHeaders.addAll(headers); - } - - /// Applies new local PAX-headers from the map. - /// - /// This replaces all currently active local headers. - void newLocals(Map headers) { - _localHeaders = headers; - } - - /// Clears local headers. - /// - /// This is used by the reader after a file has ended, as local headers only - /// apply to the next entry. - void clearLocals() { - _localHeaders = {}; - } - - @override - String? operator [](Object? key) { - return _localHeaders[key] ?? _globalHeaders[key]; - } - - @override - Iterable get keys => {..._globalHeaders.keys, ..._localHeaders.keys}; - - /// Decodes the content of an extended pax header entry. - /// - /// Semantically, a [PAX Header][posix pax] is a map with string keys and - /// values, where both keys and values are encodes with utf8. - /// - /// However, [old GNU Versions][gnu sparse00] used to repeat keys to store - /// sparse file information in sparse headers. This method will transparently - /// rewrite the PAX format of version 0.0 to version 0.1. - /// - /// [posix pax]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_03 - /// [gnu sparse00]: https://www.gnu.org/software/tar/manual/html_section/tar_94.html#SEC192 - void readPaxHeaders(List data, bool isGlobal, - {bool ignoreUnknown = true}) { - var offset = 0; - final map = {}; - final sparseMap = []; - - Never error() => throw TarException.header('Invalid PAX record'); - - while (offset < data.length) { - // At the start of an entry, expect its length which is terminated by a - // space char. - final space = data.indexOf($space, offset); - if (space == -1) break; - - var length = 0; - var currentChar = data[offset]; - var charsInLength = 0; - while (currentChar >= $0 && currentChar <= $9) { - length = length * 10 + currentChar - $0; - charsInLength++; - currentChar = data[++offset]; - } - - if (length == 0) { - error(); - } - - // Skip the whitespace - if (currentChar != $space) { - error(); - } - offset++; - - // Length also includes the length description and a space we just read - final endOfEntry = offset + length - 1 - charsInLength; - // checking against endOfEntry - 1 because the trailing whitespace is - // optional for the last entry - if (endOfEntry < offset || endOfEntry - 1 > data.length) { - error(); - } - - // Read the key - final nextEquals = data.indexOf($equal, offset); - if (nextEquals == -1 || nextEquals >= endOfEntry) { - error(); - } - - final key = utf8.decoder.convert(data, offset, nextEquals); - // Skip over the equals sign - offset = nextEquals + 1; - - // Subtract one for trailing newline for value - final endOfValue = endOfEntry - 1; - - if (!_isValidPaxKey(key)) { - error(); - } - - // If we're seeing weird PAX Version 0.0 sparse keys, expect alternating - // GNU.sparse.offset and GNU.sparse.numbytes headers. - if (key == paxGNUSparseNumBytes || key == paxGNUSparseOffset) { - final value = utf8.decoder.convert(data, offset, endOfValue); - - if (!_isValidPaxRecord(key, value) || - (sparseMap.length.isEven && key != paxGNUSparseOffset) || - (sparseMap.length.isOdd && key != paxGNUSparseNumBytes) || - value.contains(',')) { - error(); - } - - sparseMap.add(value); - } else if (!ignoreUnknown || supportedPaxHeaders.contains(key)) { - // Ignore unrecognized headers to avoid unbounded growth of the global - // header map. - final value = unsafeUtf8Decoder.convert(data, offset, endOfValue); - - if (!_isValidPaxRecord(key, value)) { - error(); - } - - map[key] = value; - } - - // Skip over value - offset = endOfValue; - // and the trailing newline - final hasNewline = offset < data.length; - if (hasNewline && data[offset] != $lf) { - throw TarException('Invalid PAX Record (missing trailing newline)'); - } - offset++; - } - - if (sparseMap.isNotEmpty) { - map[paxGNUSparseMap] = sparseMap.join(','); - } - - if (isGlobal) { - newGlobals(map); - } else { - newLocals(map); - } - } - - // NB: Some Tar files have malformed UTF-8 data in the headers, we should - // decode them anyways even if they're broken - static const unsafeUtf8Decoder = Utf8Decoder(allowMalformed: true); - - static bool _isValidPaxKey(String key) { - // These limitations are documented in the PAX standard. - return key.isNotEmpty && !key.contains('=') & !key.codeUnits.contains(0); - } - - /// Checks whether [key], [value] is a valid entry in a pax header. - /// - /// This is adopted from the Golang tar reader (`validPAXRecord`), which says - /// that "Keys and values should be UTF-8, but the number of bad writers out - /// there forces us to be a more liberal." - static bool _isValidPaxRecord(String key, String value) { - // These aren't documented in any standard, but Golangs's tar has them and - // got away with it. - switch (key) { - case paxPath: - case paxLinkpath: - case paxUname: - case paxGname: - return !value.codeUnits.contains(0); - default: - return true; - } - } -} - -enum _EntryStreamState { - preListen, - subscriptionActive, - cancelled, - done, -} - -/// The underlying content stream for the [TarReader._current] entry. Draining -/// this stream will move the tar reader to the beginning of the next file. -/// -/// This is not the same as `_current.stream` for sparse files, which are -/// reported as expanded through [TarEntry.contents]. -/// For that reason, we prefer to drain this stream when skipping a tar entry. -/// When we know we're skipping data, there's no point expanding sparse holes. -/// -/// Draining this stream will set the [TarReader._currentStream] field back to -/// null. There can only be one content stream at the time. -final class _CurrentEntryStream extends Stream> { - _EntryStreamState state = _EntryStreamState.preListen; - - final TarReader _reader; - final Stream _source; - - final StreamController> _listener = StreamController(sync: true); - // ignore: cancel_subscriptions - StreamSubscription>? _sourceSubscription; - - int _remainingContentSize; - int _remainingPaddingSize; - bool _hadError = false; - bool _isInContent = true; - - _CurrentEntryStream(this._reader, this._source, this._remainingContentSize) - : _remainingPaddingSize = _paddingFor(_remainingContentSize); - - @override - StreamSubscription> listen(void Function(List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - // Make sure that this entry is still the current one: If users store the - // contents of a tar entry, then read more tar entries, and finally try to - // read the stream of the old contents, they'd get an exception about the - // stream already being listened to. - // This can be a bit confusing, so this check enables a better error UX. - if (_reader._currentStream != this) { - throw StateError( - 'Tried listening to an outdated tar entry. \n' - 'As all tar entries found by a reader are backed by a single source ' - 'stream, only the latest tar entry can be read. It looks like you ' - 'stored the results of `tarEntry.contents` somewhere, called ' - '`reader.moveNext()` and then read the contents of the previous ' - 'entry.\n' - 'For more details, including a discussion of workarounds, see ' - 'https://github.com/simolus3/tar/issues/18', - ); - } else if (state != _EntryStreamState.preListen) { - throw StateError( - 'A tar entry has been listened to multiple times. \n' - 'As all tar entries are read from what\'s likely a single-' - 'subscription stream, this is unsupported. If you didn\'t read a tar ' - 'entry multiple times yourself, perhaps you\'ve called `moveNext()` ' - 'before reading contents?', - ); - } - - // Now we have a listener, so - state = _EntryStreamState.subscriptionActive; - // ignore: cancel_subscriptions - final sub = _sourceSubscription = _source.listen( - _forwardData, - onError: _forwardError, - onDone: _forwardDone, - ); - - _listener - ..onPause = sub.pause - ..onResume = sub.resume - ..onCancel = () { - // Pause the source subscription. When reading the next entry, the tar - // reader will drain the remaining source stream. - sub.pause(); - state = _EntryStreamState.cancelled; - }; - - return _listener.stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - static int _paddingFor(int contentSize) { - final offsetInLastBlock = contentSize.toUnsigned(blockSizeLog2); - if (offsetInLastBlock != 0) { - return blockSize - offsetInLastBlock; - } - return 0; - } - - void _assertInStateForForwarding() { - assert(state == _EntryStreamState.subscriptionActive && - _listener.hasListener && - !_listener.isPaused); - } - - void _forwardData(Uint8List event) { - _assertInStateForForwarding(); - - if (_isInContent) { - if (event.length <= _remainingContentSize) { - // We can fully add this chunk as it consists entirely of data - _listener.add(event); - _remainingContentSize -= event.length; - } else { - // We can add the first bytes as content, the others are padding that we - // shouldn't emit - _listener.add(event.sublistView(0, _remainingContentSize)); - _isInContent = false; - _remainingPaddingSize -= event.length - _remainingContentSize; - _remainingContentSize = 0; - } - } else { - // Ok, the entire event is padding - _remainingPaddingSize -= event.length; - } - - // The underlying stream comes from pkg:tar, so if we get too many bytes - // that's a bug in this package. - assert(_remainingPaddingSize >= 0, 'Stream emitted to many bytes'); - } - - void _forwardError(Object error, StackTrace trace) { - _assertInStateForForwarding(); - - _hadError = true; - _listener.addError(error, trace); - } - - void _forwardDone() { - _assertInStateForForwarding(); - - // Now that the source stream is done, reset the stream state on the reader. - state = _EntryStreamState.done; - _sourceSubscription = null; - _reader._currentStream = null; - - // If the stream stopped after an error, the user is already aware that - // something is wrong. - if (_remainingContentSize > 0 && !_hadError) { - _listener.addError( - TarException('Unexpected end of tar file'), StackTrace.current); - } - unawaited(_listener.close()); - } -} diff --git a/lib/src/third_party/tar/lib/src/sparse.dart b/lib/src/third_party/tar/lib/src/sparse.dart deleted file mode 100644 index bb938d0ac..000000000 --- a/lib/src/third_party/tar/lib/src/sparse.dart +++ /dev/null @@ -1,150 +0,0 @@ -@internal -import 'package:async/async.dart'; -import 'package:meta/meta.dart'; - -import 'exception.dart'; -import 'utils.dart'; - -/// Represents a [length]-sized fragment at [offset] in a file. -/// -/// [SparseEntry]s can represent either data or holes, and we can easily -/// convert between the two if we know the size of the file, all the sparse -/// data and all the sparse entries combined must give the full size. -final class SparseEntry { - final int offset; - final int length; - - SparseEntry(this.offset, this.length); - - int get end => offset + length; - - @override - String toString() => 'offset: $offset, length $length'; - - @override - bool operator ==(Object other) { - if (other is! SparseEntry) return false; - - return offset == other.offset && length == other.length; - } - - @override - int get hashCode => offset ^ length; -} - -/// Generates a stream of the sparse file contents of size [size], given -/// [sparseHoles] and the raw content in [source]. -@internal -Stream> sparseStream( - Stream> source, List sparseHoles, int size) { - if (sparseHoles.isEmpty) { - return ChunkedStreamReader(source).readStream(size); - } - - return _sparseStream(source, sparseHoles, size); -} - -/// Generates a stream of the sparse file contents of size [size], given -/// [sparseHoles] and the raw content in [source]. -/// -/// [sparseHoles] has to be non-empty. -Stream> _sparseStream( - Stream> source, List sparseHoles, int size) async* { - // Current logical position in sparse file. - var position = 0; - - // Index of the next sparse hole in [sparseHoles] to be processed. - var sparseHoleIndex = 0; - - // Iterator through [source] to obtain the data bytes. - final iterator = ChunkedStreamReader(source); - - while (position < size) { - // Yield all the necessary sparse holes. - while (sparseHoleIndex < sparseHoles.length && - sparseHoles[sparseHoleIndex].offset == position) { - final sparseHole = sparseHoles[sparseHoleIndex]; - yield* zeroes(sparseHole.length); - position += sparseHole.length; - sparseHoleIndex++; - } - - if (position == size) break; - - /// Yield up to the next sparse hole's offset, or all the way to the end - /// if there are no sparse holes left. - var yieldTo = size; - if (sparseHoleIndex < sparseHoles.length) { - yieldTo = sparseHoles[sparseHoleIndex].offset; - } - - // Yield data as substream, but make sure that we have enough data. - var checkedPosition = position; - await for (final chunk in iterator.readStream(yieldTo - position)) { - yield chunk; - checkedPosition += chunk.length; - } - - if (checkedPosition != yieldTo) { - throw TarException('Invalid sparse data: Unexpected end of input stream'); - } - - position = yieldTo; - } -} - -/// Reports whether [sparseEntries] is a valid sparse map. -/// It does not matter whether [sparseEntries] represents data fragments or -/// hole fragments. -bool validateSparseEntries(List sparseEntries, int size) { - // Validate all sparse entries. These are the same checks as performed by - // the BSD tar utility. - if (size < 0) return false; - - SparseEntry? previous; - - for (final current in sparseEntries) { - // Negative values are never okay. - if (current.offset < 0 || current.length < 0) return false; - - // Integer overflow with large length. - if (current.offset + current.length < current.offset) return false; - - // Region extends beyond the actual size. - if (current.end > size) return false; - - // Regions cannot overlap and must be in order. - if (previous != null && previous.end > current.offset) return false; - - previous = current; - } - - return true; -} - -/// Converts a sparse map ([source]) from one form to the other. -/// If the input is sparse holes, then it will output sparse datas and -/// vice-versa. The input must have been already validated. -/// -/// This function mutates [source] and returns a normalized map where: -/// * adjacent fragments are coalesced together -/// * only the last fragment may be empty -/// * the endOffset of the last fragment is the total size -List invertSparseEntries(List source, int size) { - final result = []; - var previous = SparseEntry(0, 0); - for (final current in source) { - /// Skip empty fragments - if (current.length == 0) continue; - - final newLength = current.offset - previous.offset; - if (newLength > 0) { - result.add(SparseEntry(previous.offset, newLength)); - } - - previous = SparseEntry(current.end, 0); - } - final lastLength = size - previous.offset; - result.add(SparseEntry(previous.offset, lastLength)); - return result; -} diff --git a/lib/src/third_party/tar/lib/src/utils.dart b/lib/src/third_party/tar/lib/src/utils.dart deleted file mode 100644 index 1c42cc6e2..000000000 --- a/lib/src/third_party/tar/lib/src/utils.dart +++ /dev/null @@ -1,588 +0,0 @@ -@internal -library; - -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; - -import 'charcodes.dart'; -import 'constants.dart'; -import 'exception.dart'; - -const _checksumEnd = checksumOffset + checksumLength; -const _checksumPlaceholder = $space; - -extension ByteBufferUtils on Uint8List { - String readString(int offset, int maxLength) { - return readStringOrNullIfEmpty(offset, maxLength) ?? ''; - } - - Uint8List sublistView(int start, [int? end]) { - return Uint8List.sublistView(this, start, end); - } - - String? readStringOrNullIfEmpty(int offset, int maxLength) { - var data = sublistView(offset, offset + maxLength); - var contentLength = data.indexOf(0); - // If there's no \0, assume that the string fills the whole segment - if (contentLength.isNegative) contentLength = maxLength; - - if (contentLength == 0) return null; - - data = data.sublistView(0, contentLength); - try { - return utf8.decode(data); - } on FormatException { - return String.fromCharCodes(data).trim(); - } - } - - /// Parse an octal string encoded from index [offset] with the maximum length - /// [length]. - int readOctal(int offset, int length) { - var result = 0; - var multiplier = 1; - - for (var i = length - 1; i >= 0; i--) { - final charCode = this[offset + i]; - // Some tar implementations add a \0 or space at the end, ignore that - if (charCode == 0 || charCode == $space) continue; - if (charCode < $0 || charCode > $9) { - throw TarException('Invalid octal value'); - } - - // Obtain the numerical value of this digit - final digit = charCode - $0; - result += digit * multiplier; - multiplier <<= 3; // Multiply by the base, 8 - } - - return result; - } - - /// Parses an encoded int, either as base-256 or octal. - /// - /// This function may return negative numbers. - int readNumeric(int offset, int length) { - if (length == 0) return 0; - - // Check for base-256 (binary) format first. If the first bit is set, then - // all following bits constitute a two's complement encoded number in big- - // endian byte order. - final firstByte = this[offset]; - if (firstByte & 0x80 != 0) { - // Handling negative numbers relies on the following identity: - // -a-1 == ~a - // - // If the number is negative, we use an inversion mask to invert the - // date bytes and treat the value as an unsigned number. - final inverseMask = firstByte & 0x40 != 0 ? 0xff : 0x00; - - // Ignore signal bit in the first byte - var x = (firstByte ^ inverseMask) & 0x7f; - - for (var i = 1; i < length; i++) { - var byte = this[offset + i]; - byte ^= inverseMask; - - x = x << 8 | byte; - } - - return inverseMask == 0xff ? ~x : x; - } - - return readOctal(offset, length); - } - - int computeUnsignedHeaderChecksum() { - // Accessing the last element first helps the VM eliminate bounds checks in - // the loops below. - this[blockSize - 1]; // ignore: unnecessary_statements - var result = checksumLength * _checksumPlaceholder; - - for (var i = 0; i < checksumOffset; i++) { - result += this[i]; - } - for (var i = _checksumEnd; i < blockSize; i++) { - result += this[i]; - } - - return result; - } - - int computeSignedHeaderChecksum() { - this[blockSize - 1]; // ignore: unnecessary_statements - // Note that _checksumPlaceholder.toSigned(8) == _checksumPlaceholder - var result = checksumLength * _checksumPlaceholder; - - for (var i = 0; i < checksumOffset; i++) { - result += this[i].toSigned(8); - } - for (var i = _checksumEnd; i < blockSize; i++) { - result += this[i].toSigned(8); - } - - return result; - } - - bool matchesHeader(List header, {int offset = magicOffset}) { - for (var i = 0; i < header.length; i++) { - if (this[offset + i] != header[i]) return false; - } - - return true; - } - - bool get isAllZeroes { - for (var i = 0; i < length; i++) { - if (this[i] != 0) return false; - } - - return true; - } -} - -bool isNotAscii(int i) => i > 128; - -/// Like [int.parse], but throwing a [TarException] instead of the more-general -/// [FormatException] when it fails. -int parseInt(String source) { - return int.tryParse(source, radix: 10) ?? - (throw TarException('Not an int: $source')); -} - -/// Takes a [paxTimeString] of the form %d.%d as described in the PAX -/// specification. Note that this implementation allows for negative timestamps, -/// which is allowed for by the PAX specification, but not always portable. -/// -/// Note that Dart's [DateTime] class only allows us to give up to microsecond -/// precision, which implies that we cannot parse all the digits in since PAX -/// allows for nanosecond level encoding. -DateTime parsePaxTime(String paxTimeString) { - const maxMicroSecondDigits = 6; - - /// Split [paxTimeString] into seconds and sub-seconds parts. - var secondsString = paxTimeString; - var microSecondsString = ''; - final position = paxTimeString.indexOf('.'); - if (position >= 0) { - secondsString = paxTimeString.substring(0, position); - microSecondsString = paxTimeString.substring(position + 1); - } - - /// Parse the seconds. - final seconds = int.tryParse(secondsString); - if (seconds == null) { - throw TarException.header('Invalid PAX time $paxTimeString detected!'); - } - - if (microSecondsString.replaceAll(RegExp('[0-9]'), '') != '') { - throw TarException.header( - 'Invalid nanoseconds $microSecondsString detected'); - } - - microSecondsString = microSecondsString.padRight(maxMicroSecondDigits, '0'); - microSecondsString = microSecondsString.substring(0, maxMicroSecondDigits); - - var microSeconds = - microSecondsString.isEmpty ? 0 : int.parse(microSecondsString); - if (paxTimeString.startsWith('-')) microSeconds = -microSeconds; - - return microsecondsSinceEpoch(microSeconds + seconds * pow(10, 6).toInt()); -} - -DateTime secondsSinceEpoch(int timestamp) { - return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true); -} - -DateTime millisecondsSinceEpoch(int milliseconds) { - return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true); -} - -DateTime microsecondsSinceEpoch(int microseconds) { - return DateTime.fromMicrosecondsSinceEpoch(microseconds, isUtc: true); -} - -int numBlocks(int fileSize) { - if (fileSize % blockSize == 0) return fileSize ~/ blockSize; - - return fileSize ~/ blockSize + 1; -} - -int nextBlockSize(int fileSize) => numBlocks(fileSize) * blockSize; - -extension ToTyped on List { - Uint8List asUint8List() { - // Flow analysis doesn't work on this. - final $this = this; - return $this is Uint8List ? $this : Uint8List.fromList(this); - } -} - -/// Generates a chunked stream of [length] zeroes. -Stream> zeroes(int length) async* { - // Emit data in chunks for efficiency - const chunkSize = 4 * 1024; - if (length < chunkSize) { - yield Uint8List(length); - return; - } - - final chunk = Uint8List(chunkSize); - for (var i = 0; i < length ~/ chunkSize; i++) { - yield chunk; - } - - final remainingBytes = length % chunkSize; - if (remainingBytes != 0) { - yield Uint8List(remainingBytes); - } -} - -/// An optimized reader reading 512-byte blocks from an input stream. -final class BlockReader { - final Stream> _input; - StreamSubscription>? _subscription; - bool _isClosed = false; - - /// If a request is active, returns the current stream that we're reporting. - /// This controler is synchronous. - StreamController? _outgoing; - - /// The amount of (512-byte) blocks remaining before [_outgoing] should close. - int _remainingBlocksInOutgoing = 0; - - /// A pending tar block that has not been emitted yet. - /// - /// This can happen if we receive small chunks of data in [_onData] that - /// aren't enough to form a full block. - final Uint8List _pendingBlock = Uint8List(blockSize); - - /// The offset in [_pendingBlock] at which new data should start. - /// - /// For instance, if this value is `502`, we're missing `10` additional bytes - /// to complete the [_pendingBlock]. - /// When this value is `0`, there is no active pending block. - int _offsetInPendingBlock = 0; - - /// Additional data that we received, but were unable to dispatch to a - /// downstream listener yet. - /// - /// This can happen if a we receive a large chunk of data and a listener is - /// only interested in a small chunk. - /// - /// We will never have trailing data and a pending block at the same time. - /// When we haver fewer than 512 bytes of trailing data, it should be stored - /// as a pending block instead. - Uint8List? _trailingData; - - /// The offset in the [_trailingData] byte array. - /// - /// When a new listener attaches, we can start by emitting the sublist - /// starting at this offset. - int _offsetInTrailingData = 0; - - BlockReader(this._input); - - /// Emits full blocks. - /// - /// Returns `true` if the listener detached in response to emitting these - /// blocks. In this case, remaining data must be saved in [_trailingData]. - bool _emitBlocks(Uint8List data, {int amount = 1}) { - assert(_remainingBlocksInOutgoing >= amount); - final outgoing = _outgoing!; - - if (!outgoing.isClosed) outgoing.add(data); - - final remainingNow = _remainingBlocksInOutgoing -= amount; - if (remainingNow == 0) { - _outgoing = null; - _pause(); - - // Scheduling this in a microtask becuase the stream controller is - // synchronous. - scheduleMicrotask(() { - // We don't need to await this since the stream controller is not used - // afterwards, if there's a paused listener we don't really care about - // that. - unawaited(outgoing.close()); - }); - return true; - } else if (outgoing.isPaused || outgoing.isClosed) { - _pause(); - return true; - } - - return false; - } - - void _onData(List data) { - assert(_outgoing != null && _trailingData == null); - - final typedData = data.asUint8List(); - var offsetInData = 0; - - /// Saves parts of the current chunks that couldn't be emitted. - void saveTrailingState() { - assert(_trailingData == null && _offsetInPendingBlock == 0); - - final remaining = typedData.length - offsetInData; - - if (remaining == 0) { - return; // Nothing to save, the chunk has been consumed fully. - } else if (remaining < blockSize) { - // Store remaining data as a pending block. - _pendingBlock.setAll(0, typedData.sublistView(offsetInData)); - _offsetInPendingBlock = remaining; - } else { - _trailingData = typedData; - _offsetInTrailingData = offsetInData; - } - } - - // Try to complete a pending block first - var offsetInPending = _offsetInPendingBlock; - final canWriteIntoPending = min(blockSize - offsetInPending, data.length); - - if (offsetInPending != 0 && canWriteIntoPending > 0) { - _pendingBlock.setAll( - offsetInPending, typedData.sublistView(0, canWriteIntoPending)); - offsetInPending = _offsetInPendingBlock += canWriteIntoPending; - offsetInData += canWriteIntoPending; - - // Did this finish the pending block? - if (offsetInPending == blockSize) { - _offsetInPendingBlock = 0; - if (_emitBlocks(_pendingBlock)) { - // Emitting the pending block completed all pending requests. - saveTrailingState(); - return; - } - } else { - // The chunk we received didn't fill up the pending block, so just stop - // here. - assert(offsetInData == data.length); - return; - } - } - - // At this point, the pending block should have been served. - assert(_offsetInPendingBlock == 0); - - final fullBlocksToEmit = min(_remainingBlocksInOutgoing, - (typedData.length - offsetInData) ~/ blockSize); - - if (fullBlocksToEmit > 0) { - _emitBlocks( - typedData.sublistView( - offsetInData, offsetInData += fullBlocksToEmit * blockSize), - amount: fullBlocksToEmit, - ); - } - - saveTrailingState(); - } - - void _onError(Object error, StackTrace trace) { - assert(_outgoing != null && _trailingData == null); - - _outgoing!.addError(error, trace); - } - - void _onDone() { - assert(_outgoing != null && _trailingData == null); - final outgoing = _outgoing!; - - // Add pending data, then close - if (_offsetInPendingBlock != 0) { - outgoing.add(_pendingBlock.sublistView(0, _offsetInPendingBlock)); - } - - _isClosed = true; - - // Can be unawated because this is an onDone callback of the subscription, - // the subscription is already complete and we're just cleaning up. - unawaited(_subscription?.cancel()); - - // Can be unawated because we're fully done here, we won't do anything else - // with the outgoing controller. - unawaited(outgoing.close()); - } - - void _subscribeOrResume() { - // We should not resume the subscription if there is trailing data ready to - // be emitted. - assert(_trailingData == null); - - final sub = _subscription; - if (sub == null) { - _subscription = _input.listen(_onData, - onError: _onError, onDone: _onDone, cancelOnError: true); - } else { - sub.resume(); - } - } - - void _pause() { - final sub = _subscription!; // ignore: cancel_subscriptions - - if (!sub.isPaused) sub.pause(); - } - - Future nextBlock() { - final result = Uint8List(blockSize); - var offset = 0; - - return nextBlocks(1).forEach((chunk) { - result.setAll(offset, chunk); - offset += chunk.length; - }).then((void _) => result.sublistView(0, offset)); - } - - Stream nextBlocks(int amount) { - if (_isClosed || amount == 0) { - return const Stream.empty(); - } - if (_outgoing != null) { - throw StateError( - 'Cannot call nextBlocks() before the previous stream completed.'); - } - assert(_remainingBlocksInOutgoing == 0); - - // We're making this synchronous because we will mostly add events in - // response to receiving chunks from the source stream. We manually ensure - // that other emits are happening asynchronously. - final controller = StreamController(sync: true); - _outgoing = controller; - _remainingBlocksInOutgoing = amount; - - var state = _StreamState.initial; - - /// Sends trailing data to the stream. Returns true if the subscription - /// should still be resumed afterwards. - bool emitTrailing() { - // Attempt to serve requests from pending data first. - final trailing = _trailingData; - if (trailing != null) { - // There should never be trailing data and a pending block at the - // same time - assert(_offsetInPendingBlock == 0); - - var remaining = trailing.length - _offsetInTrailingData; - // If there is trailing data, it should contain a full block - // (otherwise we would have stored it as a pending block) - assert(remaining >= blockSize); - - final blocks = min(_remainingBlocksInOutgoing, remaining ~/ blockSize); - assert(blocks > 0); - - final done = _emitBlocks( - trailing.sublistView(_offsetInTrailingData, - _offsetInTrailingData + blocks * blockSize), - amount: blocks); - - remaining -= blocks * blockSize; - _offsetInTrailingData += blocks * blockSize; - - if (remaining == 0) { - _trailingData = null; - _offsetInTrailingData = 0; - } else if (remaining < blockSize) { - assert(_offsetInPendingBlock == 0); - - // Move trailing data into a pending block - _pendingBlock.setAll(0, trailing.sublistView(_offsetInTrailingData)); - _offsetInPendingBlock = remaining; - _trailingData = null; - _offsetInTrailingData = 0; - } else { - // If there is still more than a full block of data waiting, we - // should not listen. This implies that the stream is done already. - assert(done); - } - - // The listener detached in response to receiving the event. - if (done) { - if (_remainingBlocksInOutgoing == 0) state = _StreamState.done; - return false; - } - } - - return true; - } - - void scheduleInitialEmit() { - scheduleMicrotask(() { - if (state != _StreamState.initial) return; - state = _StreamState.attached; - - if (emitTrailing()) { - _subscribeOrResume(); - } - }); - } - - controller - ..onListen = scheduleInitialEmit - ..onPause = () { - assert( - state == _StreamState.initial || - state == _StreamState.attached || - state == _StreamState.done, - 'Unexpected pause event in $state ($_remainingBlocksInOutgoing blocks remaining).'); - - if (state == _StreamState.initial) { - state = _StreamState.pausedAfterInitial; - } else if (state == _StreamState.attached) { - _pause(); - state = _StreamState.pausedAfterAttached; - } else if (state == _StreamState.done) { - // It may happen that onPause is called in a state where we believe - // the stream to be done already. After the stream is done, we close - // the controller in a new microtask. So if the subscription is paused - // after the last event it emitted but before we close the controller, - // we can get a pause event here. - // There's nothing to do in that case. - assert(_subscription?.isPaused != false); - } - } - ..onResume = () { - // We're done already - if (_remainingBlocksInOutgoing == 0) return; - - assert(state == _StreamState.pausedAfterAttached || - state == _StreamState.pausedAfterInitial); - - if (state == _StreamState.pausedAfterInitial) { - state = _StreamState.initial; - scheduleInitialEmit(); - } else { - state = _StreamState.attached; - if (emitTrailing()) { - _subscribeOrResume(); - } - } - } - ..onCancel = () { - state = _StreamState.done; - }; - - return controller.stream; - } - - FutureOr close() { - _isClosed = true; - return _subscription?.cancel(); - } -} - -enum _StreamState { - initial, - attached, - pausedAfterInitial, - pausedAfterAttached, - done, -} diff --git a/lib/src/third_party/tar/lib/src/writer.dart b/lib/src/third_party/tar/lib/src/writer.dart deleted file mode 100644 index 24f919083..000000000 --- a/lib/src/third_party/tar/lib/src/writer.dart +++ /dev/null @@ -1,499 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'charcodes.dart'; -import 'constants.dart'; -import 'entry.dart'; -import 'format.dart'; -import 'header.dart'; -import 'utils.dart'; - -final class _WritingTransformer - extends StreamTransformerBase> { - final OutputFormat format; - - const _WritingTransformer(this.format); - - @override - Stream> bind(Stream stream) { - // sync because the controller proxies another stream - final controller = StreamController>(sync: true); - controller.onListen = () { - // Can be unawaited since it's the only thing done in onListen and since - // pipe is a terminal operation managing the remaining lifecycle of this - // stream controller. - unawaited(stream.pipe(tarWritingSink(controller, format: format))); - }; - - return controller.stream; - } -} - -/// A stream transformer writing tar entries as byte streams. -/// -/// Regardless of the input stream, the stream returned by this -/// [StreamTransformer.bind] is a single-subscription stream. -/// Apart from that, subscriptions, cancellations, pauses and resumes are -/// propagated as one would expect from a [StreamTransformer]. -/// -/// When piping the resulting stream into a [StreamConsumer], consider using -/// [tarWritingSink] directly. -/// To change the output format of files with long names, use [tarWriterWith]. -const StreamTransformer> tarWriter = - _WritingTransformer(OutputFormat.pax); - -/// Creates a stream transformer writing tar entries as byte streams, with -/// custom encoding options. -/// -/// The [format] [OutputFormat] can be used to select the way tar entries with -/// long file or link names are written. By default, the writer will emit an -/// extended PAX header for the file ([OutputFormat.pax]). -/// Alternatively, [OutputFormat.gnuLongName] can be used to emit special tar -/// entries with the [TypeFlag.gnuLongName] type. -/// -/// Regardless of the input stream, the stream returned by this -/// [StreamTransformer.bind] is a single-subscription stream. -/// Apart from that, subscriptions, cancellations, pauses and resumes are -/// propagated as one would expect from a [StreamTransformer]. -/// -/// When using the default options, prefer using the constant [tarWriter] -/// instead. -StreamTransformer> tarWriterWith( - {OutputFormat format = OutputFormat.pax}) { - return _WritingTransformer(format); -} - -/// Create a sink emitting encoded tar files to the [output] sink. -/// -/// For instance, you can use this to write a tar file: -/// -/// ```dart -/// import 'dart:convert'; -/// import 'dart:io'; -/// import 'package:tar/tar.dart'; -/// -/// Future main() async { -/// Stream entries = Stream.value( -/// TarEntry.data( -/// TarHeader( -/// name: 'example.txt', -/// mode: int.parse('644', radix: 8), -/// ), -/// utf8.encode('This is the content of the tar file'), -/// ), -/// ); -/// -/// final output = File('/tmp/test.tar').openWrite(); -/// await entries.pipe(tarWritingSink(output)); -/// } -/// ``` -/// -/// Note that, if you don't set the [TarHeader.size], outgoing tar entries need -/// to be buffered once, which decreases performance. -/// -/// The [format] argument can be used to control how long file names are written -/// in the tar archive. For more details, see the options in [OutputFormat]. -/// -/// See also: -/// - [tarWriter], a stream transformer using this sink -/// - [StreamSink] -StreamSink tarWritingSink(StreamSink> output, - {OutputFormat format = OutputFormat.pax}) { - return _WritingSink(output, format); -} - -/// A synchronous encoder for in-memory tar files. -/// -/// The default [tarWriter] creates an asynchronous conversion from a stream of -/// tar entries to a byte stream. -/// When all tar entries are in-memory ([SynchronousTarEntry]), it is possible -/// to write them synchronously too. -/// -/// To create a tar archive consisting of a single entry, use -/// [Converter.convert] on this [tarConverter]. -/// To create a tar archive consisting of any number of entries, first call -/// [Converter.startChunkedConversion] with a suitable output sink. Next, call -/// [Sink.add] for each tar entry and finish the archive by calling -/// [Sink.close]. -/// -/// To change the output format of the tar converter, use [tarConverterWith]. -/// To encode any kind of tar entries, use the asynchronous [tarWriter]. -const Converter> tarConverter = - _SynchronousTarConverter(OutputFormat.pax); - -/// A synchronous encoder for in-memory tar files, with custom encoding options. -/// -/// For more information on how to use the converter, see [tarConverter]. -Converter> tarConverterWith( - {OutputFormat format = OutputFormat.pax}) { - return _SynchronousTarConverter(format); -} - -/// This option controls how long file and link names should be written. -/// -/// This option can be passed to writer in [tarWritingSink] or[tarWriterWith]. -enum OutputFormat { - /// Generates an extended PAX headers to encode files with a long name. - /// - /// This is the default option. - pax, - - /// Generates [TypeFlag.gnuLongName] or [TypeFlag.gnuLongLink] entries when - /// encoding files with a long name. - /// - /// When this option is set, `package:tar` will not emit PAX headers which - /// may improve compatibility with some legacy systems like old 7zip versions. - /// - /// Note that this format can't encode large file sizes or long user names. - /// Tar entries can't be written if - /// * their [TarHeader.userName] is longer than 31 bytes in utf8, - /// * their [TarHeader.groupName] is longer than 31 bytes in utf8, or, - /// * their [TarEntry.contents] are larger than 8589934591 byte (around - /// 8 GiB). - /// - /// Attempting to encode such file will throw an [UnsupportedError]. - gnuLongName, -} - -final class _WritingSink implements StreamSink { - final StreamSink> _output; - final _SynchronousTarSink _synchronousWriter; - bool _closed = false; - final Completer _done = Completer(); - - int _pendingOperations = 0; - Future _ready = Future.value(); - - _WritingSink(this._output, OutputFormat format) - : _synchronousWriter = _SynchronousTarSink(_output, format); - - @override - Future get done => _done.future; - - @override - Future add(TarEntry event) { - if (_closed) { - throw StateError('Cannot add event after close was called'); - } - return _doWork(() => _safeAdd(event)); - } - - Future _doWork(FutureOr Function() work) { - _pendingOperations++; - // Chain futures to make sure we only write one entry at a time. - return _ready = _ready - .then((_) => work()) - .catchError(_output.addError) - .whenComplete(() { - _pendingOperations--; - - if (_closed && _pendingOperations == 0) { - _done.complete(_output.close()); - } - }); - } - - Future _safeAdd(TarEntry event) async { - final header = event.header; - var size = header.size; - Uint8List? bufferedData; - if (size < 0) { - final builder = BytesBuilder(); - await event.contents.forEach(builder.add); - bufferedData = builder.takeBytes(); - size = bufferedData.length; - } - - _synchronousWriter._writeHeader(header, size); - - // Write content. - if (bufferedData != null) { - _output.add(bufferedData); - } else { - await _output.addStream(event.contents); - } - - _output.add(_paddingBytes(size)); - } - - @override - void addError(Object error, [StackTrace? stackTrace]) { - _output.addError(error, stackTrace); - } - - @override - Future addStream(Stream stream) async { - await for (final entry in stream) { - await add(entry); - } - } - - @override - Future close() async { - if (!_closed) { - _closed = true; - - // Add two empty blocks at the end. - await _doWork(_synchronousWriter.close); - } - - return done; - } -} - -Uint8List _paddingBytes(int size) { - final padding = -size % blockSize; - assert((size + padding) % blockSize == 0 && - padding <= blockSize && - padding >= 0); - - return Uint8List(padding); -} - -final class _SynchronousTarConverter - extends Converter> { - final OutputFormat format; - - const _SynchronousTarConverter(this.format); - - @override - Sink startChunkedConversion(Sink> sink) { - return _SynchronousTarSink(sink, format); - } - - @override - List convert(SynchronousTarEntry input) { - final output = BytesBuilder(copy: false); - startChunkedConversion(ByteConversionSink.withCallback(output.add)) - ..add(input) - ..close(); - - return output.takeBytes(); - } -} - -final class _SynchronousTarSink implements Sink { - final OutputFormat _format; - final Sink> _output; - - bool _closed = false; - int _paxHeaderCount = 0; - - _SynchronousTarSink(this._output, this._format); - - @override - void add(SynchronousTarEntry data) { - addHeaderAndData(data.header, data.data); - } - - void addHeaderAndData(TarHeader header, List data) { - _throwIfClosed(); - - _writeHeader(header, data.length); - _output - ..add(data) - ..add(_paddingBytes(data.length)); - } - - @override - void close() { - if (_closed) return; - - // End the tar archive by writing two zero blocks. - _output - ..add(UnmodifiableUint8ListView(zeroBlock)) - ..add(UnmodifiableUint8ListView(zeroBlock)); - _output.close(); - - _closed = true; - } - - void _throwIfClosed() { - if (_closed) { - throw StateError('Encoder is closed. ' - 'After calling `endOfArchive()`, encoder must not be used.'); - } - } - - void _writeHeader(TarHeader header, int size) { - assert(header.size < 0 || header.size == size); - - var nameBytes = utf8.encode(header.name); - var linkBytes = utf8.encode(header.linkName ?? ''); - var gnameBytes = utf8.encode(header.groupName ?? ''); - var unameBytes = utf8.encode(header.userName ?? ''); - - // We only get 100 chars for the name and link name. If they are longer, we - // have to insert an entry just to store the names. Some tar implementations - // expect them to be zero-terminated, so use 99 chars to be safe. - final paxHeader = >{}; - - if (nameBytes.length > 99) { - paxHeader[paxPath] = nameBytes; - nameBytes = nameBytes.sublist(0, 99); - } - if (linkBytes.length > 99) { - paxHeader[paxLinkpath] = linkBytes; - linkBytes = linkBytes.sublist(0, 99); - } - - // It's even worse for users and groups, where we only get 31 usable chars. - if (gnameBytes.length > 31) { - paxHeader[paxGname] = gnameBytes; - gnameBytes = gnameBytes.sublist(0, 31); - } - if (unameBytes.length > 31) { - paxHeader[paxUname] = unameBytes; - unameBytes = unameBytes.sublist(0, 31); - } - - if (size > maxIntFor12CharOct) { - paxHeader[paxSize] = ascii.encode(size.toString()); - } - - if (paxHeader.isNotEmpty) { - if (_format == OutputFormat.pax) { - _writePaxHeader(paxHeader); - } else { - _writeGnuLongName(paxHeader); - } - } - - final headerBlock = Uint8List(blockSize) - ..setAll(0, nameBytes) - ..setUint(header.mode, 100, 8) - ..setUint(header.userId, 108, 8) - ..setUint(header.groupId, 116, 8) - ..setUint(size, 124, 12) - ..setUint(header.modified.millisecondsSinceEpoch ~/ 1000, 136, 12) - ..[156] = typeflagToByte(header.typeFlag) - ..setAll(157, linkBytes) - ..setAll(257, magicUstar) - ..setUint(0, 263, 2) // version - ..setAll(265, unameBytes) - ..setAll(297, gnameBytes) - // To calculate the checksum, we first fill the checksum range with spaces - ..setAll(148, List.filled(8, $space)); - - // Then, we take the sum of the header - var checksum = 0; - for (final byte in headerBlock) { - checksum += byte; - } - headerBlock.setUint(checksum, 148, 8); - _output.add(headerBlock); - } - - /// Encodes an extended pax header. - /// - /// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_03 - void _writePaxHeader(Map> values) { - final buffer = BytesBuilder(); - // format of each entry: "%d %s=%s\n", , , - // note that the length includes the trailing \n and the length description - // itself. - values.forEach((key, value) { - final encodedKey = utf8.encode(key); - // +3 for the whitespace, the equals and the \n - final payloadLength = encodedKey.length + value.length + 3; - var indicatedLength = payloadLength; - - // The indicated length contains the length (in decimals) itself. So if - // we had payloadLength=9, then we'd prefix a 9 at which point the whole - // string would have a length of 10. If that happens, increment length. - var actualLength = payloadLength + indicatedLength.toString().length; - - while (actualLength != indicatedLength) { - indicatedLength++; - actualLength = payloadLength + indicatedLength.toString().length; - } - - // With that sorted out, let's add the line - buffer - ..add(utf8.encode(indicatedLength.toString())) - ..addByte($space) - ..add(encodedKey) - ..addByte($equal) - ..add(value) - ..addByte($lf); // \n - }); - - final paxData = buffer.takeBytes(); - addHeaderAndData( - HeaderImpl.internal( - format: TarFormat.pax, - modified: millisecondsSinceEpoch(0), - name: 'PaxHeader/${_paxHeaderCount++}', - mode: 0, - size: paxData.length, - typeFlag: TypeFlag.xHeader, - ), - paxData, - ); - } - - void _writeGnuLongName(Map> values) { - // Ensure that a file that can't be written in the GNU format is not written - const allowedKeys = {paxPath, paxLinkpath}; - final invalidOptions = values.keys.toSet()..removeAll(allowedKeys); - if (invalidOptions.isNotEmpty) { - throw UnsupportedError( - 'Unsupporteed entry for OutputFormat.gnu. It uses long fields that ' - "can't be represented: $invalidOptions. \n" - 'Try using OutputFormat.pax instead.', - ); - } - - final name = values[paxPath]; - final linkName = values[paxLinkpath]; - - void create(List name, TypeFlag flag) { - return addHeaderAndData( - HeaderImpl.internal( - name: '././@LongLink', - modified: millisecondsSinceEpoch(0), - format: TarFormat.gnu, - typeFlag: flag, - ), - name, - ); - } - - if (name != null) { - create(name, TypeFlag.gnuLongName); - } - if (linkName != null) { - create(linkName, TypeFlag.gnuLongLink); - } - } -} - -extension on Uint8List { - void setUint(int value, int position, int length) { - // Values are encoded as octal string, terminated and left-padded with - // space chars. - - // Set terminating space char. - this[position + length - 1] = $space; - - // Write as octal value, we write from right to left - var number = value; - var needsExplicitZero = number == 0; - - for (var pos = position + length - 2; pos >= position; pos--) { - if (number != 0) { - // Write the last octal digit of the number (e.g. the last 4 bits) - this[pos] = (number & 7) + $0; - // then drop the last digit (divide by 8 = 2³) - number >>= 3; - } else if (needsExplicitZero) { - this[pos] = $0; - needsExplicitZero = false; - } else { - // done, left-pad with spaces - this[pos] = $space; - } - } - } -} diff --git a/lib/src/third_party/tar/lib/tar.dart b/lib/src/third_party/tar/lib/tar.dart deleted file mode 100644 index 9948c4f71..000000000 --- a/lib/src/third_party/tar/lib/tar.dart +++ /dev/null @@ -1,16 +0,0 @@ -/// Streaming tar implementation for Dart. -/// -/// To read tar files, see [TarReader]. To write tar files, use [tarWritingSink] -/// or [tarWriter]. -library tar; - -// For dartdoc. -import 'src/reader.dart'; -import 'src/writer.dart'; - -export 'src/entry.dart' show TarEntry, SynchronousTarEntry; -export 'src/exception.dart'; -export 'src/format.dart'; -export 'src/header.dart' show TarHeader, TypeFlag; -export 'src/reader.dart' show TarReader; -export 'src/writer.dart'; diff --git a/lib/src/third_party/tar/vendored-pubspec.yaml b/lib/src/third_party/tar/vendored-pubspec.yaml deleted file mode 100644 index 5d641aa14..000000000 --- a/lib/src/third_party/tar/vendored-pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: tar -description: Memory-efficient, streaming implementation of the tar file format -version: 1.0.1 -repository: https://github.com/simolus3/tar/ - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - async: ^2.6.0 - meta: ^1.3.0 - typed_data: ^1.3.0 - -dev_dependencies: - charcode: ^1.2.0 - extra_pedantic: ^4.0.0 - file: ^6.1.2 - node_io: ^2.1.0 - path: ^1.8.0 - test: ^1.20.0 diff --git a/lib/src/third_party/vendor-state.yaml b/lib/src/third_party/vendor-state.yaml deleted file mode 100644 index 62b5a3242..000000000 --- a/lib/src/third_party/vendor-state.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# DO NOT EDIT: This file is generated by package:vendor version 0.9.0 -version: 0.9.0 -config: - import_rewrites: - oauth2: oauth2 - tar: tar - vendored_dependencies: - oauth2: - package: oauth2 - version: 2.0.1 - import_rewrites: {} - include: - - pubspec.yaml - - README.md - - LICENSE - - CHANGELOG.md - - lib/** - - analysis_options.yaml - tar: - package: tar - version: 1.0.1 - import_rewrites: {} - include: - - pubspec.yaml - - README.md - - LICENSE - - CHANGELOG.md - - lib/** - - analysis_options.yaml diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 9df0a3b70..31ca1d188 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -469,7 +469,7 @@ bool get canUseAnsiCodes { String getAnsi(String ansiCode) => canUseAnsiCodes ? ansiCode : ''; /// Gets a emoji special character as unicode, or the [alternative] if unicode -/// charactors are not supported by stdout. +/// characters are not supported by stdout. String emoji(String unicode, String alternative) => canUseUnicode ? unicode : alternative; diff --git a/pubspec.yaml b/pubspec.yaml index bb1a0bf38..bce7a4bea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: analyzer: ^5.1.0 args: ^2.4.0 async: ^2.6.1 + checks: ^0.2.2 cli_util: ^0.4.0 collection: ^1.15.0 convert: ^3.0.2 @@ -24,9 +25,9 @@ dependencies: shelf: ^1.1.1 source_span: ^1.8.1 stack_trace: ^1.10.0 + tar: ^1.0.1 typed_data: ^1.3.1 usage: ^4.0.2 - vendor: ^0.9.2 yaml: ^3.1.0 yaml_edit: ^2.0.0 @@ -36,4 +37,3 @@ dev_dependencies: test: ^1.21.5 test_descriptor: ^2.0.0 test_process: ^2.0.0 - diff --git a/test/ascii_tree_test.dart b/test/ascii_tree_test.dart index b406f725e..d0a921bd4 100644 --- a/test/ascii_tree_test.dart +++ b/test/ascii_tree_test.dart @@ -61,9 +61,11 @@ void main() { file('path.dart', bytes(100)), ]), ]).create(); - var files = - Package.load(path(appPath), (name) => throw UnimplementedError()) - .listFiles(); + var files = Package.load( + null, + path(appPath), + (name) => throw UnimplementedError(), + ).listFiles(); ctx.expectNextSection( tree.fromFiles(files, baseDir: path(appPath), showFileSizes: true), ); diff --git a/test/cache/add/package_not_found_test.dart b/test/cache/add/package_not_found_test.dart index e929c7bf2..0428bd95c 100644 --- a/test/cache/add/package_not_found_test.dart +++ b/test/cache/add/package_not_found_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; import '../../test_pub.dart'; void main() { - test('fails if the package cound not be found on the source', () async { + test('fails if the package could not be found on the source', () async { await servePackages(); await runPub( diff --git a/test/cache/preload_test.dart b/test/cache/preload_test.dart index 314e5ed65..a9577fe86 100644 --- a/test/cache/preload_test.dart +++ b/test/cache/preload_test.dart @@ -64,7 +64,7 @@ void main() { }); test( - 'installs package according to PUB_HOSTED_URL even on non-offical server', + 'installs package according to PUB_HOSTED_URL even on non-official server', () async { final server = await servePackages(); server.serve('foo', '1.0.0'); diff --git a/test/descriptor.dart b/test/descriptor.dart index 3ad4d6ea2..c7ff85f05 100644 --- a/test/descriptor.dart +++ b/test/descriptor.dart @@ -9,8 +9,8 @@ import 'dart:convert'; import 'package:path/path.dart' as p; import 'package:pub/src/language_version.dart'; +import 'package:pub/src/oauth2.dart'; import 'package:pub/src/package_config.dart'; -import 'package:pub/src/third_party/oauth2/lib/oauth2.dart' as oauth2; import 'package:test_descriptor/test_descriptor.dart'; import 'descriptor/git.dart'; @@ -275,7 +275,7 @@ String _credentialsFileContent( String? refreshToken, DateTime? expiration, }) => - oauth2.Credentials( + Credentials( accessToken, refreshToken: refreshToken, tokenEndpoint: Uri.parse(server.url).resolve('/token'), diff --git a/test/directory_option_test.dart b/test/directory_option_test.dart index 4568eac5f..da0505e19 100644 --- a/test/directory_option_test.dart +++ b/test/directory_option_test.dart @@ -50,7 +50,7 @@ main() => print('Hi'); pubspec({ 'name': 'example', 'dependencies': { - 'test_pkg': {'path': '../'}, + 'myapp': {'path': '../'}, // Wrong name of dependency }, }), ]), diff --git a/test/get/git/dependency_name_match_pubspec_test.dart b/test/get/git/dependency_name_match_pubspec_test.dart index b048f86ba..2dcf1692b 100644 --- a/test/get/git/dependency_name_match_pubspec_test.dart +++ b/test/get/git/dependency_name_match_pubspec_test.dart @@ -28,9 +28,8 @@ void main() { ]).create(); await pubGet( - error: contains( - 'Expected to find package "weirdname", found package "foo".', - ), + error: contains('"name" field doesn\'t match expected name ' + '"weirdname".'), exitCode: exit_codes.DATA, ); }); diff --git a/test/get/hosted/resolve_with_retracted_package_versions_test.dart b/test/get/hosted/resolve_with_retracted_package_versions_test.dart index b6a907e60..bafe70094 100644 --- a/test/get/hosted/resolve_with_retracted_package_versions_test.dart +++ b/test/get/hosted/resolve_with_retracted_package_versions_test.dart @@ -45,7 +45,7 @@ void main() { // Currently retraction does not affect prioritization. I.e., if // pubspec.lock already contains a retracted version, which is the newest - // satisfying the dependency contstraint we will not choose to downgrade. + // satisfying the dependency constraint we will not choose to downgrade. // In this case we expect a newer version to be published at some point which // will then cause pub upgrade to choose that one. test('Allow retracted version when it was already in pubspec.lock', () async { diff --git a/test/io_test.dart b/test/io_test.dart index d788e139f..4f321a0b1 100644 --- a/test/io_test.dart +++ b/test/io_test.dart @@ -9,7 +9,7 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:pub/src/exceptions.dart'; import 'package:pub/src/io.dart'; -import 'package:pub/src/third_party/tar/lib/tar.dart'; +import 'package:tar/tar.dart'; import 'package:test/test.dart'; import 'descriptor.dart' as d; @@ -150,6 +150,25 @@ void main() { ); }); + test('resolves a symlink to parent', () { + expect( + _withCanonicalTempDir((temp) { + _createDir(path.join(temp, 'linked-dir')); + _createDir(path.join(temp, 'linked-dir', 'a')); + _createDir(path.join(temp, 'linked-dir', 'b')); + createSymlink( + path.join(temp, 'linked-dir'), + path.join(temp, 'linked-dir', 'a', 'symlink'), + ); + expect( + canonicalize(path.join(temp, 'linked-dir', 'a', 'symlink', 'b')), + equals(path.join(temp, 'linked-dir', 'b')), + ); + }), + completes, + ); + }); + test('resolves a relative symlink', () { expect( _withCanonicalTempDir((temp) { diff --git a/test/precompilation_test.dart b/test/precompilation_test.dart new file mode 100644 index 000000000..7ed02eae3 --- /dev/null +++ b/test/precompilation_test.dart @@ -0,0 +1,111 @@ +import 'dart:io'; + +import 'package:checks/checks.dart'; +import 'package:pub/src/dart.dart'; +import 'package:pub/src/exceptions.dart'; +import 'package:pub/src/log.dart'; +import 'package:test/test.dart'; + +import 'descriptor.dart'; + +String outputPath() => '$sandbox/output/snapshot'; +String incrementalDillPath() => '${outputPath()}.incremental'; + +// A quite big program is needed for the caching to be an actual advantage. +FileDescriptor foo = file('foo.dart', ''' +foo() { + ${List.generate(500000, (index) => 'print("$index");').join('\n')} +} + '''); + +FileDescriptor workingMain = file( + 'main.dart', + ''' +import 'foo.dart'; + +main() async { + foo(); +} +''', +); + +FileDescriptor brokenMain = file( + 'main.dart', + ''' +import 'foo.dart'; +yadda yadda +main() asyncc { + foo(); +} +''', +); + +Future timeCompilation( + String executable, { + bool fails = false, +}) async { + final s = Stopwatch()..start(); + verbosity = Verbosity.none; + Future compile() async { + await precompile( + executablePath: executable, + name: 'abc', + outputPath: outputPath(), + packageConfigPath: path('app/.dart_tool/package_config.json'), + ); + } + + if (fails) { + await check(compile()).throws(); + } else { + await compile(); + } + verbosity = Verbosity.normal; + return s.elapsed; +} + +void main() { + test('Precompilation is much faster second time and removes old artifacts', + () async { + await dir('app', [ + workingMain, + foo, + packageConfigFile([]), + ]).create(); + final first = await timeCompilation(path('app/main.dart')); + check( + because: 'Should not leave a stray directory.', + File(incrementalDillPath()).existsSync(), + ).isFalse(); + check(File(outputPath()).existsSync()).isTrue(); + + // Do a second compilation to compare the compile times, it should be much + // faster because it can reuse the compiled data in the dill file. + final second = await timeCompilation(path('app/main.dart')); + check(first).isGreaterThan(second * 2); + + // Now create an error to test that the output is placed at a different + // location. + await dir('app', [ + brokenMain, + foo, + packageConfigFile([]), + ]).create(); + final afterErrors = + await timeCompilation(path('app/main.dart'), fails: true); + check(File(incrementalDillPath()).existsSync()).isTrue(); + check(File(outputPath()).existsSync()).isFalse(); + check(first).isGreaterThan(afterErrors * 2); + + // Fix the error, and check that we still use the cached output to improve + // compile times. + await dir('app', [ + workingMain, + ]).create(); + final afterFix = await timeCompilation(path('app/main.dart')); + // The output from the failed compilation should now be gone. + check(File('${outputPath()}.incremental').existsSync()).isFalse(); + check(File(outputPath()).existsSync()).isTrue(); + check(first).isGreaterThan(afterFix * 2); + }); +} diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index 990bb2a48..7af4b2dda 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart @@ -15,6 +15,9 @@ void main() { group('parse()', () { final sources = SystemCache().sources; + var throwsPubspecException = + throwsA(const TypeMatcher()); + void expectPubspecException( String contents, void Function(Pubspec) fn, [ @@ -38,6 +41,24 @@ void main() { Pubspec.parse('version: not a semver', sources); }); + test( + "eagerly throws an error if the pubspec name doesn't match the " + 'expected name', () { + expect( + () => Pubspec.parse('name: foo', sources, expectedName: 'bar'), + throwsPubspecException, + ); + }); + + test( + "eagerly throws an error if the pubspec doesn't have a name and an " + 'expected name is passed', () { + expect( + () => Pubspec.parse('{}', sources, expectedName: 'bar'), + throwsPubspecException, + ); + }); + test('allows a version constraint for dependencies', () { var pubspec = Pubspec.parse( ''' @@ -602,7 +623,7 @@ dependencies: }); test( - 'default upper constraint for the SDK applies only if compatibile ' + 'default upper constraint for the SDK applies only if compatible ' 'with the lower bound', () { var pubspec = Pubspec.parse( ''' @@ -686,11 +707,11 @@ environment: test("throws if the sdk isn't a valid version constraint", () { expectPubspecException( - 'environment: {sdk: "oopies"}', + 'environment: {sdk: "oopsies"}', (pubspec) => pubspec.sdkConstraints, ); expectPubspecException( - 'environment: {sdk: 1.2.3, flutter: "oopies"}', + 'environment: {sdk: 1.2.3, flutter: "oopsies"}', (pubspec) => pubspec.sdkConstraints, ); }); diff --git a/test/test_pub.dart b/test/test_pub.dart index 3bc01592f..951ee21b1 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart @@ -29,10 +29,10 @@ import 'package:pub/src/log.dart' as log; import 'package:pub/src/package_name.dart'; import 'package:pub/src/source/hosted.dart'; import 'package:pub/src/system_cache.dart'; -import 'package:pub/src/third_party/tar/lib/tar.dart'; import 'package:pub/src/utils.dart'; import 'package:pub/src/validator.dart'; import 'package:pub_semver/pub_semver.dart'; +import 'package:tar/tar.dart'; import 'package:test/test.dart' hide fail; import 'package:test/test.dart' as test show fail; import 'package:test_process/test_process.dart'; @@ -1080,7 +1080,7 @@ Stream> tarFromDescriptors(Iterable contents) { } /// Replaces the entry at index 9 in [stream] with a 0. This replaces the os -/// entry of a gzip stream, giving us the same stream and thius stable testing +/// entry of a gzip stream, giving us the same stream and this stable testing /// on all platforms. /// /// See https://www.rfc-editor.org/rfc/rfc1952 section 2.3 for information diff --git a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt index c69e9e259..e4da8be6c 100644 --- a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt +++ b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt @@ -59,9 +59,12 @@ Got dependencies in myapp/example! ## Section 6 $ pub get bar -C 'myapp/example2' Resolving dependencies in myapp/example2... -+ foo 1.0.0 -+ test_pkg 1.0.0 from path myapp -Changed 2 dependencies in myapp/example2! +[STDERR] Error on line 1, column 9 of myapp/pubspec.yaml: "name" field doesn't match expected name "myapp". +[STDERR] ╷ +[STDERR] 1 │ {"name":"test_pkg","version":"1.0.0","homepage":"https://pub.dev","description":"A package, I guess.","environment":{"sdk":">=3.1.2 <=3.2.0"}, dependencies: { foo: ^1.0.0}} +[STDERR] │ ^^^^^^^^^^ +[STDERR] ╵ +[EXIT CODE] 65 -------------------------------- END OF OUTPUT --------------------------------- diff --git a/test/testdata/goldens/help_test/pub outdated --help.txt b/test/testdata/goldens/help_test/pub outdated --help.txt index 14e83235f..ba6b13a61 100644 --- a/test/testdata/goldens/help_test/pub outdated --help.txt +++ b/test/testdata/goldens/help_test/pub outdated --help.txt @@ -12,7 +12,7 @@ Usage: pub outdated [options] (defaults to on) --json Output the results using a json format. --[no-]prereleases Include prereleases in latest version. - --[no-]show-all Include dependencies that are already fullfilling --mode. + --[no-]show-all Include dependencies that are already fulfilling --mode. --[no-]transitive Show transitive dependencies. -C, --directory= Run this in the directory . diff --git a/test/testdata/goldens/help_test/pub upgrade --help.txt b/test/testdata/goldens/help_test/pub upgrade --help.txt index 32b7eeafc..2721d13ed 100644 --- a/test/testdata/goldens/help_test/pub upgrade --help.txt +++ b/test/testdata/goldens/help_test/pub upgrade --help.txt @@ -9,6 +9,7 @@ Usage: pub upgrade [dependencies...] --[no-]offline Use cached packages instead of accessing the network. -n, --dry-run Report what dependencies would change but don't change any. --[no-]precompile Precompile executables in immediate dependencies. + --tighten Updates lower bounds in pubspec.yaml to match the resolved version. --major-versions Upgrades packages to their latest resolvable versions, and updates pubspec.yaml. -C, --directory= Run this in the directory . diff --git a/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt b/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt index ad60ceba9..97b2feff1 100644 --- a/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt +++ b/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt @@ -13,7 +13,7 @@ $ pub outdated random_argument [STDERR] --json Output the results using a json format. [STDERR] --[no-]prereleases Include prereleases in latest version. [STDERR] --[no-]show-all Include dependencies that are already -[STDERR] fullfilling --mode. +[STDERR] fulfilling --mode. [STDERR] --[no-]transitive Show transitive dependencies. [STDERR] -C, --directory= Run this in the directory . [STDERR] @@ -36,7 +36,7 @@ $ pub outdated --bad_flag [STDERR] --json Output the results using a json format. [STDERR] --[no-]prereleases Include prereleases in latest version. [STDERR] --[no-]show-all Include dependencies that are already -[STDERR] fullfilling --mode. +[STDERR] fulfilling --mode. [STDERR] --[no-]transitive Show transitive dependencies. [STDERR] -C, --directory= Run this in the directory . [STDERR] diff --git a/test/upgrade/git/upgrade_to_incompatible_pubspec_test.dart b/test/upgrade/git/upgrade_to_incompatible_pubspec_test.dart index 711c3448d..b45a56fe9 100644 --- a/test/upgrade/git/upgrade_to_incompatible_pubspec_test.dart +++ b/test/upgrade/git/upgrade_to_incompatible_pubspec_test.dart @@ -42,7 +42,7 @@ void main() { ).commit(); await pubUpgrade( - error: contains('Expected to find package "foo", found package "zoo".'), + error: contains('"name" field doesn\'t match expected name "foo".'), exitCode: exit_codes.DATA, ); diff --git a/test/upgrade/upgrade_tighten_test.dart b/test/upgrade/upgrade_tighten_test.dart new file mode 100644 index 000000000..7f6374556 --- /dev/null +++ b/test/upgrade/upgrade_tighten_test.dart @@ -0,0 +1,172 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() { + group('pub upgrade --tighten', () { + test('updates dependency constraints lower bounds and shows summary report', + () async { + final server = await servePackages(); + + server.serve('foo', '1.0.0'); + server.serve('bar', '0.2.0'); + server.serve('baz', '0.2.0'); + server.serve('boo', '1.0.0'); + + await d.dir('boom', [d.libPubspec('boom', '1.0.0')]).create(); + await d.dir('boom2', [d.libPubspec('boom2', '1.5.0')]).create(); + + await d.appDir( + dependencies: { + 'foo': '^1.0.0', + 'bar': '>=0.1.2 <3.0.0', + 'baz': '0.2.0', + 'boo': 'any', + 'boom': {'path': '../boom'}, + 'boom2': {'path': '../boom2', 'version': '^1.0.0'}, + }, + ).create(); + + await pubGet(); + + server.serve('foo', '1.5.0'); + server.serve('bar', '1.5.0'); + + await pubUpgrade( + args: ['--tighten', '--dry-run'], + output: allOf([ + contains('Would change 4 constraints in pubspec.yaml:'), + contains('foo: ^1.0.0 -> ^1.5.0'), + contains('bar: >=0.1.2 <3.0.0 -> >=1.5.0 <3.0.0'), + contains('boo: any -> ^1.0.0'), + contains('boom2: ^1.0.0 -> ^1.5.0'), + ]), + ); + + await pubUpgrade( + args: ['--tighten'], + output: allOf([ + contains('Changed 4 constraints in pubspec.yaml:'), + contains('foo: ^1.0.0 -> ^1.5.0'), + contains('bar: >=0.1.2 <3.0.0 -> >=1.5.0 <3.0.0'), + contains('boo: any -> ^1.0.0'), + contains('boom2: ^1.0.0 -> ^1.5.0'), + ]), + ); + + await d.appDir( + dependencies: { + 'foo': '^1.5.0', + 'bar': '>=1.5.0 <3.0.0', + 'baz': '0.2.0', + 'boo': '^1.0.0', + 'boom': {'path': '../boom'}, + 'boom2': {'path': '../boom2', 'version': '^1.5.0'}, + }, + ).validate(); + await d.appPackageConfigFile([ + d.packageConfigEntry(name: 'foo', version: '1.5.0'), + d.packageConfigEntry(name: 'bar', version: '1.5.0'), + d.packageConfigEntry(name: 'baz', version: '0.2.0'), + d.packageConfigEntry(name: 'boo', version: '1.0.0'), + d.packageConfigEntry(name: 'boom', path: '../boom'), + d.packageConfigEntry(name: 'boom2', path: '../boom2'), + ]).validate(); + }); + + test( + '--major-versions updates dependency constraints lower bounds and shows summary report', + () async { + final server = await servePackages(); + + server.serve('foo', '1.0.0'); + server.serve('bar', '1.0.0'); + + await d.appDir( + dependencies: { + 'foo': '^1.0.0', + 'bar': '^1.0.0', + }, + ).create(); + + await pubGet(); + + server.serve('foo', '2.0.0'); + server.serve('bar', '1.5.0'); + + await pubUpgrade( + args: ['--tighten', '--major-versions'], + output: allOf([ + contains('Changed 2 constraints in pubspec.yaml:'), + contains('foo: ^1.0.0 -> ^2.0.0'), + contains('bar: ^1.0.0 -> ^1.5.0'), + ]), + ); + + await d.appDir( + dependencies: { + 'foo': '^2.0.0', + 'bar': '^1.5.0', + }, + ).validate(); + await d.appPackageConfigFile([ + d.packageConfigEntry(name: 'foo', version: '2.0.0'), + d.packageConfigEntry(name: 'bar', version: '1.5.0'), + ]).validate(); + }); + + test('can tighten a specific package', () async { + final server = await servePackages(); + + server.serve('foo', '1.0.0'); + server.serve('bar', '1.0.0'); + + await d.appDir( + dependencies: { + 'foo': '^1.0.0', + 'bar': '^1.0.0', + }, + ).create(); + + await pubGet(); + + server.serve('foo', '1.5.0'); + server.serve('bar', '1.5.0'); + + await pubUpgrade( + args: ['--tighten', 'foo'], + output: allOf([ + contains('Changed 1 constraint in pubspec.yaml:'), + contains('foo: ^1.0.0 -> ^1.5.0'), + ]), + ); + + await d.appDir( + dependencies: { + 'foo': '^1.5.0', + 'bar': '^1.0.0', + }, + ).validate(); + await d.appPackageConfigFile([ + d.packageConfigEntry(name: 'foo', version: '1.5.0'), + d.packageConfigEntry(name: 'bar', version: '1.0.0'), + ]).validate(); + + server.serve('foo', '2.0.0'); + server.serve('bar', '2.0.0'); + + await pubUpgrade( + args: ['--tighten', 'bar', '--major-versions'], + output: allOf([ + contains('Changed 1 constraint in pubspec.yaml:'), + contains('bar: ^1.0.0 -> ^2.0.0'), + ]), + ); + }); + }); +} diff --git a/test/validator/file_case_test.dart b/test/validator/file_case_test.dart index 37b279ff2..fd43eb308 100644 --- a/test/validator/file_case_test.dart +++ b/test/validator/file_case_test.dart @@ -5,7 +5,7 @@ // These tests only work on case-sensitive file systems (ie. only on linux). @OnPlatform({ 'windows': Skip('Windows file system is case-insensitive'), - 'mac-os': Skip('MacOS file system is case-insensitive'), + 'mac-os': Skip('macOS file system is case-insensitive'), }) library; diff --git a/test/validator/flutter_constraint_test.dart b/test/validator/flutter_constraint_test.dart index 60800a209..5c0b2058c 100644 --- a/test/validator/flutter_constraint_test.dart +++ b/test/validator/flutter_constraint_test.dart @@ -31,7 +31,7 @@ Future setup({ d.pubspec({ 'name': 'test_pkg', 'description': - 'A just long enough decription to fit the requirement of 60 characters', + 'A just long enough description to fit the requirement of 60 characters', 'homepage': 'https://example.com/', 'version': '1.0.0', 'environment': { diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index 457866ee4..df469fb04 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart @@ -1445,12 +1445,10 @@ void sdkConstraint() { await expectResolves( environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')}, - error: equalsIgnoringWhitespace(''' - The current Dart SDK version is 3.1.2+3. + error: contains(''' +The current Dart SDK version is 3.1.2+3. - Because myapp requires SDK version >3.1.2+3, version solving - failed. - '''), +Because myapp requires SDK version >3.1.2+3, version solving failed.'''), ); }); @@ -1711,7 +1709,7 @@ void override() { await expectResolves(result: {'a': '2.0.0', 'b': '1.0.0', 'c': '1.0.0'}); }); - test('backtracks on overidden package for its constraints', () async { + test('backtracks on overridden package for its constraints', () async { await servePackages() ..serve('a', '1.0.0', deps: {'shared': 'any'}) ..serve('a', '2.0.0', deps: {'shared': '1.0.0'}) diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart index 8e28d14bd..a11a2a486 100644 --- a/tool/test-bin/pub_command_runner.dart +++ b/tool/test-bin/pub_command_runner.dart @@ -19,8 +19,8 @@ import 'package:usage/usage.dart'; final Analytics loggingAnalytics = _LoggingAnalytics(); -// A command for explicitly throwing an exception, to test the handling of -// unexpected eceptions. +/// A command for explicitly throwing an exception, to test the handling of +/// unexpected exceptions. class ThrowingCommand extends PubCommand { @override String get name => 'fail'; @@ -36,7 +36,7 @@ class ThrowingCommand extends PubCommand { } } -// A command for testing the ensurePubspecResolved functionality +/// A command for testing the [ensurePubspecResolved] functionality. class EnsurePubspecResolvedCommand extends PubCommand { @override String get name => 'ensure-pubspec-resolved'; diff --git a/tool/test.dart b/tool/test.dart index d0ffe95a8..2eb8adc95 100755 --- a/tool/test.dart +++ b/tool/test.dart @@ -30,16 +30,16 @@ Future main(List args) async { }); final pubSnapshotFilename = path.absolute(path.join('.dart_tool', '_pub', 'pub.dart.snapshot.dart2')); - final pubSnapshotIncrementalFilename = '$pubSnapshotFilename.incremental'; try { - stderr.writeln('Building snapshot'); + final stopwatch = Stopwatch()..start(); + stderr.write('Building snapshot...'); await precompile( executablePath: path.join('bin', 'pub.dart'), outputPath: pubSnapshotFilename, - incrementalDillPath: pubSnapshotIncrementalFilename, name: 'bin/pub.dart', packageConfigPath: path.join('.dart_tool', 'package_config.json'), ); + stderr.writeln(' (${stopwatch.elapsed.inMilliseconds}ms)'); testProcess = await Process.start( Platform.resolvedExecutable, ['run', 'test', ...args], @@ -51,11 +51,6 @@ Future main(List args) async { stderr.writeln('Failed building snapshot: $e'); exitCode = 1; } finally { - try { - await File(pubSnapshotFilename).delete(); - await sub.cancel(); - } on Exception { - // snapshot didn't exist. - } + await sub.cancel(); } } diff --git a/vendor.yaml b/vendor.yaml deleted file mode 100644 index 068dae661..000000000 --- a/vendor.yaml +++ /dev/null @@ -1,10 +0,0 @@ -import_rewrites: - oauth2: oauth2 - tar: tar -vendored_dependencies: - oauth2: - package: oauth2 - version: 2.0.1 - tar: - package: tar - version: 1.0.1