AppstreamCollection.fromYaml constructor

AppstreamCollection.fromYaml(
  1. String yaml
)

Decodes an Appstream collection in YAML format.

Implementation

factory AppstreamCollection.fromYaml(String yaml) {
  var yamlDocuments = loadYamlDocuments(_removeInvalidDocuments(yaml));
  if (yamlDocuments.isEmpty) {
    throw FormatException('Empty YAML file');
  }
  var header = yamlDocuments[0];
  if (header.contents is! YamlMap) {
    throw FormatException('Invalid DEP-11 header');
  }
  var headerMap = (header.contents as YamlMap);
  var file = headerMap['File'];
  if (file != 'DEP-11') {
    throw FormatException('Not a DEP-11 file');
  }
  var version = headerMap['Version'];
  if (version == null) {
    throw FormatException('Missing AppStream version');
  }
  var origin = headerMap['Origin'];
  if (origin == null) {
    throw FormatException('Missing repository origin');
  }
  var priority = headerMap['Priority'];
  var mediaBaseUrl = headerMap['MediaBaseUrl'];
  var architecture = headerMap['Architecture'];
  var components = <AppstreamComponent>[];
  for (var doc in yamlDocuments.skip(1)) {
    var component = doc.contents as YamlMap;
    var id = component['ID'];
    if (id == null) {
      throw FormatException('Missing component ID');
    }
    var typeName = component['Type'];
    if (typeName == null) {
      throw FormatException('Missing component type');
    }
    var type = _parseComponentType(typeName);
    var package = component['Package'];
    var name = component['Name'];
    if (name == null) {
      throw FormatException('Missing component name');
    }
    var summary = component['Summary'];
    if (summary == null) {
      throw FormatException('Missing component summary');
    }
    var description = component['Description'];
    var developerName = component['DeveloperName'];
    var projectLicense = component['ProjectLicense'];
    var projectGroup = component['ProjectGroup'];

    var icons = <AppstreamIcon>[];
    var icon = component['Icon'];
    if (icon != null) {
      for (var type in icon.keys) {
        switch (type) {
          case 'stock':
            icons.add(AppstreamStockIcon(icon[type]));
            break;
          case 'cached':
            for (var i in icon[type]) {
              icons.add(AppstreamCachedIcon(i['name'],
                  width: i['width'], height: i['height']));
            }
            break;
          case 'local':
            for (var i in icon[type]) {
              icons.add(AppstreamLocalIcon(i['name'],
                  width: i['width'], height: i['height']));
            }
            break;
          case 'remote':
            for (var i in icon[type]) {
              icons.add(AppstreamRemoteIcon(_makeUrl(mediaBaseUrl, i['url']),
                  width: i['width'], height: i['height']));
            }
            break;
        }
      }
    }

    var urls = <AppstreamUrl>[];
    var url = component['Url'];
    if (url != null) {
      for (var typeName in url.keys) {
        urls.add(
            AppstreamUrl(url[typeName] ?? '', type: _parseUrlType(typeName)));
      }
    }

    var launchables = <AppstreamLaunchable>[];
    var launchable = component['Launchable'];
    if (launchable != null) {
      if (launchable is! YamlMap) {
        throw FormatException('Invalid Launchable type');
      }
      for (var typeName in launchable.keys) {
        var launchableList = launchable[typeName];
        if (launchableList is! YamlList) {
          throw FormatException('Invalid Launchable type');
        }
        switch (typeName) {
          case 'desktop-id':
            launchables.addAll(
                launchableList.map((l) => AppstreamLaunchableDesktopId(l)));
            break;
          case 'service':
            launchables.addAll(
                launchableList.map((l) => AppstreamLaunchableService(l)));
            break;
          case 'cockpit-manifest':
            launchables.addAll(launchableList
                .map((l) => AppstreamLaunchableCockpitManifest(l)));
            break;
          case 'url':
            launchables
                .addAll(launchableList.map((l) => AppstreamLaunchableUrl(l)));
            break;
        }
      }
    }

    var categories = <String>[];
    var categoriesComponent = component['Categories'];
    if (categoriesComponent != null) {
      if (categoriesComponent is! YamlList) {
        throw FormatException('Invalid Categories type');
      }
      categories.addAll(categoriesComponent.cast<String>());
    }

    var keywords = <String, List<String>>{};
    var keywordsComponent = component['Keywords'];
    if (keywordsComponent != null) {
      if (keywordsComponent is! YamlMap) {
        throw FormatException('Invalid Keywords type');
      }
      keywords = keywordsComponent.map(
        (lang, keywordList) => MapEntry(
          lang,
          keywordList.nodes
              .where((e) => e.value != null)
              .map<String>((e) => e.value.toString())
              .toList(),
        ),
      );
    }

    var screenshots = <AppstreamScreenshot>[];
    var screenshotsComponent = component['Screenshots'];
    if (screenshotsComponent != null) {
      if (screenshotsComponent is! YamlList) {
        throw FormatException('Invalid Screenshots type');
      }
      for (var screenshot in screenshotsComponent) {
        var isDefault = screenshot['default'] ?? 'false' == 'true';
        var caption = screenshot['caption'];
        var images = <AppstreamImage>[];
        var thumbnails = screenshot['thumbnails'];
        if (thumbnails != null) {
          if (thumbnails is! YamlList) {
            throw FormatException('Invalid thumbnails type');
          }
          for (var thumbnail in thumbnails) {
            var url = thumbnail['url'];
            if (url == null) {
              throw FormatException('Image missing Url');
            }
            var width = thumbnail['width'];
            var height = thumbnail['height'];
            var lang = thumbnail['lang'];
            images.add(AppstreamImage(
                type: AppstreamImageType.thumbnail,
                url: _makeUrl(mediaBaseUrl, url),
                width: width,
                height: height,
                lang: lang));
          }
        }
        var sourceImage = screenshot['source-image'];
        if (sourceImage != null) {
          var url = sourceImage['url'];
          if (url == null) {
            throw FormatException('Image missing Url');
          }
          var width = sourceImage['width'];
          var height = sourceImage['height'];
          var lang = sourceImage['lang'];
          images.add(AppstreamImage(
              type: AppstreamImageType.source,
              url: _makeUrl(mediaBaseUrl, url),
              width: width,
              height: height,
              lang: lang));
        }
        screenshots.add(AppstreamScreenshot(
            images: images,
            caption: caption != null
                ? _parseYamlTranslatedString(caption)
                : const {},
            isDefault: isDefault));
      }
    }

    var compulsoryForDesktops = <String>[];
    var compulsoryForDesktopsComponent = component['CompulsoryForDesktops'];
    if (compulsoryForDesktopsComponent != null) {
      if (compulsoryForDesktopsComponent is! YamlList) {
        throw FormatException('Invalid CompulsoryForDesktops type');
      }
      compulsoryForDesktops
          .addAll(compulsoryForDesktopsComponent.cast<String>());
    }

    var releases = <AppstreamRelease>[];
    var releasesComponent = component['Releases'];
    if (releasesComponent != null) {
      if (releasesComponent is! YamlList) {
        throw FormatException('Invalid Releases type');
      }
      for (var release in releasesComponent) {
        if (release is! YamlMap) {
          throw FormatException('Invalid release type');
        }
        var version = release['version'];
        DateTime? date;
        var dateAttribute = release['date'];
        var unixTimestamp = release['unix-timestamp'];
        if (unixTimestamp != null) {
          date = DateTime.fromMillisecondsSinceEpoch(unixTimestamp * 1000,
              isUtc: true);
        } else if (dateAttribute != null) {
          date = DateTime.parse(dateAttribute);
        }
        AppstreamReleaseType? type;
        var typeName = release['type'];
        if (typeName != null) {
          type = _parseReleaseType(typeName);
        }
        AppstreamReleaseUrgency? urgency;
        var urgencyName = release['urgency'];
        if (urgencyName != null) {
          urgency = _parseReleaseUrgency(urgencyName);
        }
        var description = release['description'];
        var url = release['url']?['details'];
        var issues = <AppstreamIssue>[];
        var issuesComponent = release['issues'];
        if (issuesComponent != null) {
          if (issuesComponent is! YamlList) {
            throw FormatException('Invalid issues type');
          }
          for (var issue in issuesComponent) {
            if (issue is! YamlMap) {
              throw FormatException('Invalid issue type');
            }
            var id = issue['id'];
            if (id == null) {
              throw FormatException('Issue missing id');
            }
            AppstreamIssueType? type;
            var typeName = issue['type'];
            if (typeName != null) {
              type = _parseIssueType(typeName);
            }
            var url = issue['url'];
            issues.add(AppstreamIssue(id,
                type: type ?? AppstreamIssueType.generic, url: url));
          }
        }
        releases.add(AppstreamRelease(
            version: _parseYamlVersion(version),
            date: date,
            type: type ?? AppstreamReleaseType.stable,
            urgency: urgency ?? AppstreamReleaseUrgency.medium,
            description: description != null
                ? _parseYamlTranslatedString(description)
                : const {},
            url: url,
            issues: issues));
      }
    }

    var provides = <AppstreamProvides>[];
    var providesComponent = component['Provides'];
    if (providesComponent != null) {
      if (providesComponent is! YamlMap) {
        throw FormatException('Invalid Provides type');
      }
      for (var type in providesComponent.keys) {
        var values = providesComponent[type];
        if (values is! YamlList) {
          throw FormatException('Invalid $type provides');
        }
        switch (type) {
          case 'mediatypes':
          case 'mimetypes':
            provides.addAll(values.map((e) => AppstreamProvidesMediatype(e)));
            break;
          case 'libraries':
            provides.addAll(values.map((e) => AppstreamProvidesLibrary(e)));
            break;
          case 'binaries':
            provides.addAll(values.map((e) => AppstreamProvidesBinary(e)));
            break;
          case 'fonts':
            for (var fontComponent in values) {
              if (fontComponent is! YamlMap) {
                throw FormatException('Invalid font provides');
              }
              var name = fontComponent['name'];
              if (name == null) {
                throw FormatException('Missing font name');
              }
              provides.add(AppstreamProvidesFont(name));
            }
            break;
          case 'firmware':
            for (var firmwareComponent in values) {
              if (firmwareComponent is! YamlMap) {
                throw FormatException('Invalid firmware provides');
              }
              var type = firmwareComponent['type'];
              switch (type) {
                case 'runtime':
                  var file = firmwareComponent['file'];
                  if (file == null) {
                    throw FormatException('Missing firmware file');
                  }
                  provides.add(AppstreamProvidesFirmware(
                      AppstreamFirmwareType.runtime, file));
                  break;
                case 'flashed':
                  var guid = firmwareComponent['guid'];
                  if (guid == null) {
                    throw FormatException('Missing firmware guid');
                  }
                  provides.add(AppstreamProvidesFirmware(
                      AppstreamFirmwareType.flashed, guid));
                  break;
              }
            }
            break;
          case 'python2':
            for (var moduleName in values) {
              provides.add(AppstreamProvidesPython2(moduleName));
            }
            break;
          case 'python3':
            for (var moduleName in values) {
              provides.add(AppstreamProvidesPython3(moduleName));
            }
            break;
          case 'modaliases':
            for (var modalias in values) {
              provides.add(AppstreamProvidesModalias(modalias));
            }
            break;
          case 'dbus':
            for (var dbusComponent in values) {
              if (dbusComponent is! YamlMap) {
                throw FormatException('Invalid dbus provides');
              }
              var type = dbusComponent['type'];
              if (type == null) {
                throw FormatException('Missing DBus bus type');
              }
              var service = dbusComponent['service'];
              if (service == null) {
                throw FormatException('Missing DBus service name');
              }
              provides
                  .add(AppstreamProvidesDBus(_parseDBusType(type), service));
            }
            break;
          case 'ids':
            provides.addAll(values.map((e) => AppstreamProvidesId(e)));
            break;
        }
      }
    }

    var languages = <AppstreamLanguage>[];
    var languagesComponent = component['Languages'];
    if (languagesComponent != null) {
      if (languagesComponent is! YamlList) {
        throw FormatException('Invalid Languages type');
      }

      for (var language in languagesComponent) {
        if (language is! YamlMap) {
          throw FormatException('Invalid language type');
        }
        var locale = language['locale'];
        if (locale == null) {
          throw FormatException('Missing language locale');
        }
        var percentage = language['percentage'];
        languages.add(AppstreamLanguage(locale, percentage: percentage));
      }
    }

    var contentRatings = <String, Map<String, AppstreamContentRating>>{};
    var contentRatingComponent = component['ContentRating'];
    if (contentRatingComponent != null) {
      if (contentRatingComponent is! YamlMap) {
        throw FormatException('Invalid ContentRating type');
      }
      for (var type in contentRatingComponent.keys) {
        contentRatings[type] = contentRatingComponent[type]
            .map<String, AppstreamContentRating>((key, value) =>
                MapEntry(key as String, _parseContentRating(value)));
      }
    }

    components.add(AppstreamComponent(
        id: id,
        type: type,
        package: package,
        name: _parseYamlTranslatedString(name),
        summary: _parseYamlTranslatedString(summary),
        description: description != null
            ? _parseYamlTranslatedString(description)
            : const {},
        developerName: developerName != null
            ? _parseYamlTranslatedString(developerName)
            : const {},
        projectLicense: projectLicense,
        projectGroup: projectGroup,
        icons: icons,
        urls: urls,
        launchables: launchables,
        categories: categories,
        keywords: keywords,
        screenshots: screenshots,
        compulsoryForDesktops: compulsoryForDesktops,
        releases: releases,
        provides: provides,
        languages: languages,
        contentRatings: contentRatings));
  }

  return AppstreamCollection(
      version: _parseYamlVersion(version),
      origin: origin,
      architecture: architecture,
      priority: priority,
      components: components);
}