validateMarketplaceManifest function
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,
);
}