validateMarketplaceManifest function

Future<ValidationResult> validateMarketplaceManifest(
  1. String filePath
)

Validate a marketplace manifest file (marketplace.json).

Implementation

Future<ValidationResult> validateMarketplaceManifest(String filePath) async {
  final errors = <ValidationError>[];
  final warnings = <ValidationWarning>[];
  final absolutePath = path.absolute(filePath);

  // Read file
  String content;
  try {
    content = await File(absolutePath).readAsString();
  } on FileSystemException catch (e) {
    final code = e.osError?.errorCode == 2 ? 'ENOENT' : null;
    final message = code == 'ENOENT'
        ? 'File not found: $absolutePath'
        : 'Failed to read file: ${e.message}';
    return ValidationResult(
      success: false,
      errors: [ValidationError(path: 'file', message: message, code: code)],
      warnings: [],
      filePath: absolutePath,
      fileType: PluginFileType.marketplace,
    );
  }

  // Parse JSON
  Map<String, dynamic> parsed;
  try {
    final decoded = _jsonDecode(content);
    if (decoded is! Map<String, dynamic>) {
      return ValidationResult(
        success: false,
        errors: [
          const ValidationError(
            path: 'json',
            message: 'Root must be a JSON object',
          ),
        ],
        warnings: [],
        filePath: absolutePath,
        fileType: PluginFileType.marketplace,
      );
    }
    parsed = decoded;
  } catch (e) {
    return ValidationResult(
      success: false,
      errors: [
        ValidationError(path: 'json', message: 'Invalid JSON syntax: $e'),
      ],
      warnings: [],
      filePath: absolutePath,
      fileType: PluginFileType.marketplace,
    );
  }

  // Check path traversal in plugin sources
  if (parsed['plugins'] is List) {
    final plugins = parsed['plugins'] as List;
    for (var i = 0; i < plugins.length; i++) {
      final plugin = plugins[i];
      if (plugin is Map && plugin.containsKey('source')) {
        final source = plugin['source'];
        if (source is String) {
          _checkPathTraversal(
            source,
            'plugins[$i].source',
            errors,
            hint: _marketplaceSourceHint(source),
          );
        }
        if (source is Map &&
            source.containsKey('path') &&
            source['path'] is String) {
          _checkPathTraversal(
            source['path'] as String,
            'plugins[$i].source.path',
            errors,
          );
        }
      }
    }
  }

  // Validate marketplace name
  if (parsed.containsKey('name') && parsed['name'] is String) {
    errors.addAll(validateMarketplaceName(parsed['name'] as String));
  } else {
    errors.add(
      const ValidationError(
        path: 'name',
        message: 'Marketplace must have a name',
      ),
    );
  }

  // Validate plugins array
  if (!parsed.containsKey('plugins') || parsed['plugins'] is! List) {
    warnings.add(
      const ValidationWarning(
        path: 'plugins',
        message: 'Marketplace has no plugins defined',
      ),
    );
  } else {
    final plugins = (parsed['plugins'] as List)
        .map(
          (e) => PluginMarketplaceEntry.fromJson(
            Map<String, dynamic>.from(e as Map),
          ),
        )
        .toList();

    // Check for duplicates
    for (var i = 0; i < plugins.length; i++) {
      final duplicates = plugins.where((p) => p.name == plugins[i].name);
      if (duplicates.length > 1) {
        errors.add(
          ValidationError(
            path: 'plugins[$i].name',
            message:
                'Duplicate plugin name "${plugins[i].name}" found in marketplace',
          ),
        );
      }
    }
  }

  // Warn if no description in metadata
  if (parsed['metadata'] is! Map ||
      (parsed['metadata'] as Map?)?['description'] == null) {
    warnings.add(
      const ValidationWarning(
        path: 'metadata.description',
        message:
            'No marketplace description provided. Adding a description helps users understand what this marketplace offers',
      ),
    );
  }

  return ValidationResult(
    success: errors.isEmpty,
    errors: errors,
    warnings: warnings,
    filePath: absolutePath,
    fileType: PluginFileType.marketplace,
  );
}