Future summarizePackageArchive()

in pkg/pub_package_reader/lib/pub_package_reader.dart [69:268]


Future<PackageSummary> summarizePackageArchive(
  String archivePath, {

  /// The maximum length of the extracted content text.
  int maxContentLength = 128 * 1024,

  /// The maximum file size of the archive (gzipped or compressed) and
  /// the maximum total size of the files inside the archive.
  int maxArchiveSize = 100 * 1024 * 1024,

  /// The maximum number of files in the archive.
  /// TODO: set this lower once we scan the existing archives
  int maxFileCount = 64 * 1024,
}) async {
  final issues = <ArchiveIssue>[];

  // Run scans before tar parsing...
  issues.addAll(
      await scanArchiveSurface(archivePath, maxArchiveSize: maxArchiveSize)
          .toList());
  if (issues.isNotEmpty) {
    return PackageSummary(issues: issues);
  }

  TarArchive tar;
  try {
    tar = await TarArchive.scan(
      archivePath,
      maxFileCount: maxFileCount,
      maxTotalLengthBytes: maxArchiveSize,
    );
  } catch (e, st) {
    _logger.info('Failed to scan tar archive.', e, st);
    return PackageSummary.fail(
        ArchiveIssue('Failed to scan tar archive. ($e)'));
  }

  // symlinks check
  final brokenSymlinks = tar.brokenSymlinks();
  if (brokenSymlinks.isNotEmpty) {
    final from = brokenSymlinks.keys.first;
    final to = brokenSymlinks[from];
    return PackageSummary.fail(ArchiveIssue(
        'Package archive contains a broken symlink: `$from` -> `$to`.'));
  }

  // processing pubspec.yaml
  final pubspecPath = tar.firstFileNameOrNull(['pubspec.yaml']);
  if (pubspecPath == null) {
    issues.add(ArchiveIssue('pubspec.yaml is missing.'));
    return PackageSummary(issues: issues);
  }

  final pubspecContent = await tar.readContentAsString(pubspecPath);
  // Large pubspec content should be rejected, as either a storage limit will be
  // limiting it, or it will slow down queries and processing for very little
  // reason.
  if (pubspecContent.length > 128 * 1024) {
    issues.add(ArchiveIssue('pubspec.yaml is too large.'));
  }

  // Reject packages using aliases in `pubspec.yaml`, these are poorly supported
  // when transcoding to json.
  if (yamlContainsAliases(pubspecContent)) {
    issues.add(ArchiveIssue(
      'pubspec.yaml may not use references (alias/anchors), '
      'only the subset of YAML that can be encoded as JSON is allowed.',
    ));
    return PackageSummary(issues: issues);
  }

  Pubspec? pubspec;
  try {
    pubspec = Pubspec.parse(pubspecContent);
  } on YamlException catch (e) {
    issues.add(ArchiveIssue('Error parsing pubspec.yaml: $e'));
    return PackageSummary(issues: issues);
  } on Exception catch (e) {
    issues.add(ArchiveIssue('Error parsing pubspec.yaml: $e'));
  }
  // Try again with lenient parsing.
  try {
    pubspec ??= Pubspec.parse(pubspecContent, lenient: true);
  } on Exception catch (e) {
    issues.add(ArchiveIssue('Error parsing pubspec.yaml: $e'));
    return PackageSummary(issues: issues);
  }
  issues.addAll(checkValidJson(pubspecContent));
  issues.addAll(checkAuthors(pubspecContent));
  issues.addAll(checkPlatforms(pubspecContent));
  issues.addAll(checkStrictVersions(pubspec));
  issues.addAll(checkSdkVersionRange(pubspec));
  // Check whether the files can be extracted on case-preserving file systems
  // (e.g. on Windows). We can't allow two files with the same case-insensitive
  // name.
  final lowerCaseFiles = <String, List<String>>{};
  for (final file in tar.fileNames) {
    final lower = file.toLowerCase();
    lowerCaseFiles.putIfAbsent(lower, () => <String>[]).add(file);
  }
  final fileNameCollisions =
      lowerCaseFiles.values.firstWhereOrNull((l) => l.length > 1);
  if (fileNameCollisions != null) {
    issues.add(ArchiveIssue(
        'Filename collision on case-preserving file systems: ${fileNameCollisions.join(' vs. ')}.'));
  }

  if (pubspec.name.trim().isEmpty) {
    issues.add(ArchiveIssue('pubspec.yaml is missing `name`.'));
    return PackageSummary(issues: issues);
  }
  if (pubspec.version == null) {
    issues.add(ArchiveIssue('pubspec.yaml is missing `version`.'));
    return PackageSummary(issues: issues);
  }

  String? readmePath = tar.firstFileNameOrNull(readmeFileNames);
  String? changelogPath = tar.firstFileNameOrNull(changelogFileNames);
  String? examplePath =
      tar.firstFileNameOrNull(exampleFileCandidates(pubspec.name));
  String? licensePath = tar.firstFileNameOrNull(licenseFileNames);

  final contentBytes = await tar.scanAndReadFiles(
    [readmePath, changelogPath, examplePath, licensePath]
        .whereType<String>()
        .toList(),
    maxLength: maxContentLength,
  );

  String? tryParseContentBytes(String? contentPath) {
    if (contentPath == null) return null;
    final bytes = contentBytes[contentPath];
    if (bytes == null) return null;
    if (bytes.length > maxContentLength) {
      issues.add(ArchiveIssue(
          '`$contentPath` exceeds the maximum content length ($maxContentLength bytes).'));
    }
    String content = utf8.decode(bytes, allowMalformed: true);
    if (content.length > maxContentLength) {
      content = content.substring(0, maxContentLength) + '[...]\n\n';
    }
    return content;
  }

  final readmeContent = tryParseContentBytes(readmePath);
  if (readmeContent == null) {
    readmePath = null;
  }
  final changelogContent = tryParseContentBytes(changelogPath);
  if (changelogContent == null) {
    changelogPath = null;
  }
  final exampleContent = tryParseContentBytes(examplePath);
  if (exampleContent == null) {
    examplePath = null;
  }
  final licenseContent = tryParseContentBytes(licensePath);
  if (licenseContent == null) {
    licensePath = null;
  }

  final libraries = tar.fileNames
      .where((file) => file.startsWith('lib/'))
      .where((file) => !file.startsWith('lib/src'))
      .where((file) => file.endsWith('.dart'))
      .map((file) => file.substring('lib/'.length))
      .toList();

  issues.addAll(validatePackageName(pubspec.name));
  issues.addAll(validatePackageVersion(pubspec.version));
  issues.addAll(validatePublishTo(pubspec.publishTo));
  issues.addAll(validateZalgo('description', pubspec.description));
  issues.addAll(syntaxCheckUrl(pubspec.homepage, 'homepage'));
  issues.addAll(syntaxCheckUrl(pubspec.repository?.toString(), 'repository'));
  issues.addAll(syntaxCheckUrl(pubspec.documentation, 'documentation'));
  issues
      .addAll(syntaxCheckUrl(pubspec.issueTracker?.toString(), 'issueTracker'));
  issues.addAll(validateDependencies(pubspec));
  issues.addAll(forbidGitDependencies(pubspec));
  // TODO: re-enable or remove after version pinning gets resolved
  //       https://github.com/dart-lang/pub/issues/2557
  // issues.addAll(forbidPreReleaseSdk(pubspec));
  issues.addAll(requireIosFolderOrFlutter2_20(pubspec, tar.fileNames));
  issues.addAll(requireNonEmptyLicense(licensePath, licenseContent));
  issues.addAll(checkScreenshots(pubspec, tar.fileNames));

  return PackageSummary(
    issues: issues,
    pubspecContent: pubspecContent,
    readmePath: readmePath,
    readmeContent: readmeContent,
    changelogPath: changelogPath,
    changelogContent: changelogContent,
    examplePath: examplePath,
    exampleContent: exampleContent,
    licensePath: licensePath,
    licenseContent: licenseContent,
    libraries: libraries,
  );
}