1use anyhow::{bail, Context, Result};
5use serde::Deserialize;
6use sha2::{Digest, Sha256};
7use std::fs::{self, File};
8use std::io::{Read, Write};
9#[cfg(unix)]
10use std::os::unix::fs::PermissionsExt;
11use std::path::{Path, PathBuf};
12
13pub mod registry;
15pub use registry::{
16 DependencyResolution, DependencyResolver, LocalRegistry, PluginDependency, PluginRegistryEntry,
17 RegistryPersistence, VersionRequirement,
18};
19
20pub mod config;
22pub use config::Config;
23
24pub mod metadata;
26pub use metadata::{DependencyMetadata, PluginMetadata, PluginRequirements, PluginStats};
27
28pub mod remote;
30pub use remote::{CacheStats, HybridRegistry, RemoteRegistry, RemoteRegistryConfig};
31
32pub mod upgrade;
34pub use upgrade::{BackupManager, BackupRecord, SemanticVersion, UpgradeInfo, UpgradeResult};
35
36pub mod abi_compat;
38pub use abi_compat::{
39 ABICompatibleInfo, ABIValidationResult, ABIValidator, ABIVersion, CapabilityInfo,
40 DependencyInfo, MaturityLevel, PluginCategory, ResourceRequirements,
41};
42
43pub mod signature;
45pub use signature::{
46 KeyInfo, PluginSignature, SignatureAlgorithm, SignatureAuditLog, SignatureManager, TrustLevel,
47 VerificationResult,
48};
49
50pub mod security;
52pub use security::{
53 LicenseCompliance, LicenseType, RiskLevel, SecurityAuditReport, SecurityScanResult,
54 Vulnerability, VulnerabilityScanner, VulnerabilitySeverity,
55};
56
57pub mod validation;
59pub use validation::{
60 ManifestValidator, ValidationIssue, ValidationReport, ValidationRule, ValidationSeverity,
61};
62
63pub mod health_check;
65pub use health_check::{
66 Architecture, BinaryCompatibility, HealthCheckResult, HealthReport, HealthScore,
67 HealthSeverity, HealthStatus, PerformanceBaseline, PerformanceThresholds, Platform,
68 PluginHealthChecker, SymbolRequirement,
69};
70
71pub mod compat_matrix;
73pub use compat_matrix::{
74 AbiCompatibilityEntry, AbiVersion, BreakingChange, CompatibilityAnalysis, CompatibilityLevel,
75 CompatibilityReport, DependencyCompatibility, PlatformArch, PlatformSupportEntry,
76 PluginCompatibilityMatrix,
77};
78
79pub mod sandbox;
81pub use sandbox::{
82 Permission, PluginCapability, PluginSandboxVerifier, ResourceLimits, SandboxCheckResult,
83 SandboxRiskLevel, SandboxSeverity, SandboxVerificationReport, SystemCallInfo,
84};
85
86pub mod dep_tree;
88pub use dep_tree::{
89 CircularDependency, DependencyEdge, DependencyGraph, DependencyMetrics, DependencyNode,
90};
91
92pub mod composition;
94pub use composition::{
95 BundleMetadata, BundleType, CompositePlugin, CompositeSize, CompositionManager,
96 ConflictResolution, DependencyResolutionResult, PluginBundle, PluginComponent,
97 ValidationResult, VersionConflict,
98};
99
100pub mod optional_deps;
102pub use optional_deps::{
103 ConditionType, DependencyCondition, FeatureGate, OptionalDependency, OptionalDependencyManager,
104 PlatformSpecific,
105};
106
107pub mod extractor;
109pub use extractor::{extract_artifact, ExtractionResult, ExtractorConfig, PluginExtractor};
110
111pub mod platform;
113pub use platform::{
116 get_valid_artifact_filenames, is_valid_artifact_extension, is_valid_artifact_filename,
117 validate_platform_artifact, ArtifactMetadata, SUPPORTED_ARTIFACT_EXTENSIONS,
118 SUPPORTED_ARTIFACT_FILENAMES,
119};
120
121pub mod publish;
123pub use publish::{ArtifactPublishResult, ArtifactPublisher, LocalArtifact, PublishConfig};
124
125#[derive(Deserialize, Debug)]
126pub struct ManifestPackage {
127 pub name: String,
128 pub version: String,
129 pub abi_version: String,
130 pub entrypoint: Option<String>,
131}
132
133#[derive(Deserialize, Debug)]
134pub struct Manifest {
135 pub package: Option<ManifestPackage>,
136 pub name: Option<String>,
138 pub version: Option<String>,
139 pub abi_version: Option<String>,
140 pub entrypoint: Option<String>,
141}
142
143pub fn read_manifest(path: &Path) -> Result<Manifest> {
144 let s =
145 fs::read_to_string(path).with_context(|| format!("reading manifest {}", path.display()))?;
146 let m: Manifest = toml::from_str(&s).context("parsing plugin.toml")?;
147
148 let name = if let Some(pkg) = &m.package {
150 pkg.name.clone()
151 } else {
152 m.name.clone().unwrap_or_default()
153 };
154
155 let version = if let Some(pkg) = &m.package {
156 pkg.version.clone()
157 } else {
158 m.version.clone().unwrap_or_default()
159 };
160
161 let abi_version = if let Some(pkg) = &m.package {
162 pkg.abi_version.clone()
163 } else {
164 m.abi_version.clone().unwrap_or_default()
165 };
166
167 if name.trim().is_empty() || version.trim().is_empty() || abi_version.trim().is_empty() {
169 bail!("manifest must have name, version, and abi_version (either in [package] section or at top level)");
170 }
171 Ok(m)
172}
173
174pub fn pack_dir(src_dir: &Path, out_path: &Path) -> Result<PathBuf> {
183 let manifest_path = src_dir.join("plugin.toml");
185 let manifest = read_manifest(&manifest_path)?;
186
187 let (name, version) = if let Some(pkg) = &manifest.package {
189 (pkg.name.clone(), pkg.version.clone())
190 } else {
191 (
192 manifest.name.clone().unwrap_or_default(),
193 manifest.version.clone().unwrap_or_default(),
194 )
195 };
196
197 if !name
199 .chars()
200 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
201 {
202 bail!(
203 "Plugin name must be lowercase with hyphens/underscores only (RFC-0003)\n\
204 Got: '{}'\n\
205 Example: 'my-plugin' or 'my_plugin'",
206 name
207 );
208 }
209
210 let version_parts: Vec<&str> = version.split('.').collect();
212 if version_parts.len() < 3 {
213 bail!(
214 "Version must follow semantic versioning (RFC-0003)\n\
215 Got: '{}'\n\
216 Expected format: major.minor.patch (e.g., '1.0.0')",
217 version
218 );
219 }
220 for part in &version_parts[..3] {
221 if part.parse::<u32>().is_err() {
222 bail!(
223 "Version parts must be numeric (RFC-0003)\n\
224 Got: '{}'\n\
225 Expected format: major.minor.patch (e.g., '1.0.0')",
226 version
227 );
228 }
229 }
230
231 let file = File::create(out_path)
232 .with_context(|| format!("creating output {}", out_path.display()))?;
233 let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
234 let mut builder = tar::Builder::new(enc);
235
236 let root = format!("{}-{}", name, version);
237
238 {
240 let mut header = tar::Header::new_gnu();
241 header.set_entry_type(tar::EntryType::Directory);
242 header.set_mode(0o755);
243 header.set_mtime(0);
244 header.set_uid(0);
245 header.set_gid(0);
246 header.set_size(0);
247 header.set_cksum();
248 builder.append_data(&mut header, Path::new(&root), std::io::empty())?;
249 }
250
251 let mut optional_files: Vec<&str> = Vec::new();
253
254 for entry in walkdir::WalkDir::new(src_dir)
256 .into_iter()
257 .filter_map(|e| e.ok())
258 {
259 let path = entry.path();
260 if path == src_dir {
261 continue;
262 }
263 let rel = path.strip_prefix(src_dir).unwrap();
264 let target_path = Path::new(&root).join(rel);
265
266 if let Some(fname) = rel.file_name().and_then(|s| s.to_str()) {
268 if fname == "CHANGELOG.md" {
269 optional_files.push("CHANGELOG.md");
270 }
271 }
272 if rel.starts_with("doc") && !optional_files.contains(&"doc/") {
273 optional_files.push("doc/");
274 }
275
276 if path.is_dir() {
277 let mut header = tar::Header::new_gnu();
278 header.set_entry_type(tar::EntryType::Directory);
279 header.set_mode(0o755);
280 header.set_mtime(0);
281 header.set_uid(0);
282 header.set_gid(0);
283 header.set_size(0);
284 header.set_cksum();
285 builder.append_data(&mut header, &target_path, std::io::empty())?;
286 } else if path.is_file() {
287 let mut f = File::open(path)?;
288 let meta = f.metadata()?;
289 let mut header = tar::Header::new_gnu();
290 header.set_size(meta.len());
291 let mut mode = 0o644;
293 if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
294 if fname == "plugin.so" || fname == "plugin.dll" || fname == "plugin.dylib" {
295 mode = 0o755;
296 }
297 }
298 #[cfg(unix)]
300 {
301 let p = meta.permissions();
302 if (p.mode() & 0o111) != 0 {
303 mode = 0o755;
304 }
305 }
306 header.set_mode(mode);
307 header.set_mtime(0);
308 header.set_uid(0);
309 header.set_gid(0);
310 header.set_cksum();
311 builder.append_data(&mut header, &target_path, &mut f)?;
312 }
313 }
314
315 let enc = builder.into_inner()?;
317 enc.finish()?;
318
319 let sha = compute_sha256(out_path)?;
321 let checksum_name = format!("{}.sha256", out_path.file_name().unwrap().to_string_lossy());
323 let checksum_path = out_path
324 .parent()
325 .unwrap_or_else(|| Path::new("."))
326 .join(checksum_name);
327 let mut f = File::create(&checksum_path)?;
328 writeln!(
329 f,
330 "{} {}",
331 hex::encode(sha),
332 out_path.file_name().unwrap().to_string_lossy()
333 )?;
334
335 if !optional_files.is_empty() {
337 tracing::error!(
338 "RFC-0003: Optional files included: {}",
339 optional_files.join(", ")
340 );
341 }
342
343 Ok(checksum_path)
344}
345
346pub fn pack_dir_with_target(
371 src_dir: &Path,
372 output_dir: &Path,
373 target_triple: &str,
374) -> Result<PathBuf> {
375 let manifest_path = src_dir.join("plugin.toml");
377 let manifest = read_manifest(&manifest_path)?;
378
379 let (name, version) = if let Some(pkg) = &manifest.package {
380 (pkg.name.clone(), pkg.version.clone())
381 } else {
382 (
383 manifest.name.clone().unwrap_or_default(),
384 manifest.version.clone().unwrap_or_default(),
385 )
386 };
387
388 let _platform =
390 crate::platform::Platform::from_target_triple(target_triple).ok_or_else(|| {
391 anyhow::anyhow!(
392 "Unknown platform in target triple: {}\n\
393 Supported: linux, windows, apple/darwin",
394 target_triple
395 )
396 })?;
397
398 let artifact_name = format!("{}-v{}-{}.tar.gz", name, version, target_triple);
400 let out_path = output_dir.join(&artifact_name);
401
402 if !output_dir.exists() {
404 fs::create_dir_all(output_dir)?;
405 }
406
407 pack_dir(src_dir, &out_path)?;
409
410 let _meta = crate::platform::ArtifactMetadata::parse(&artifact_name).with_context(|| {
412 format!(
413 "Generated artifact name is not RFC-0003 compliant: {}",
414 artifact_name
415 )
416 })?;
417
418 let checksum_name = format!("{}.sha256", artifact_name);
420 Ok(output_dir.join(checksum_name))
421}
422
423fn compute_sha256(path: &Path) -> Result<Vec<u8>> {
424 let mut f = File::open(path)?;
425 let mut hasher = Sha256::new();
426 let mut buf = [0u8; 8192];
427 loop {
428 let n = f.read(&mut buf)?;
429 if n == 0 {
430 break;
431 }
432 hasher.update(&buf[..n]);
433 }
434 Ok(hasher.finalize().to_vec())
435}
436
437pub fn verify_artifact(artifact: &Path, checksum_path: Option<&Path>) -> Result<()> {
440 let checksum_path = match checksum_path {
441 Some(p) => p.to_path_buf(),
442 None => {
443 let name = format!("{}.sha256", artifact.file_name().unwrap().to_string_lossy());
444 artifact
445 .parent()
446 .unwrap_or_else(|| Path::new("."))
447 .join(name)
448 }
449 };
450
451 if !checksum_path.exists() {
452 bail!("checksum file not found: {}", checksum_path.display());
453 }
454
455 let s = fs::read_to_string(&checksum_path)?;
457 let token = s.split_whitespace().next().context("checksum file empty")?;
458 let expected = hex::decode(token.trim()).context("decoding checksum hex")?;
459
460 let computed = compute_sha256(artifact)?;
461 if expected != computed {
462 bail!(
463 "checksum mismatch: expected {} got {}",
464 hex::encode(expected),
465 hex::encode(computed)
466 );
467 }
468
469 let f = File::open(artifact)?;
471 let dec = flate2::read::GzDecoder::new(f);
472 let mut ar = tar::Archive::new(dec);
473
474 let mut roots = std::collections::HashSet::new();
475 let mut seen_plugin_toml = false;
476 let mut seen_plugin_so = false;
477 let mut seen_license = false;
478 let mut seen_readme = false;
479
480 for entry in ar.entries()? {
481 let entry = entry?;
482 let path = match entry.path() {
483 Ok(p) => p.into_owned(),
484 Err(_) => continue,
485 };
486 let comps: Vec<_> = path.components().collect();
487 if comps.is_empty() {
488 continue;
489 }
490 if let Some(root_comp) = comps.first() {
492 roots.insert(root_comp.as_os_str().to_owned());
493 }
494 if comps.len() == 2 {
496 if let Some(name) = path.file_name() {
497 match name.to_string_lossy().to_lowercase().as_str() {
498 "plugin.toml" => seen_plugin_toml = true,
499 "plugin.so" | "plugin.dll" | "plugin.dylib" => seen_plugin_so = true,
500 "license" => seen_license = true,
501 "readme.md" => seen_readme = true,
502 _ => {}
503 }
504 }
505 }
506 }
507
508 if roots.len() != 1 {
509 bail!("archive must contain a single root directory");
510 }
511
512 if !(seen_plugin_toml && seen_plugin_so && seen_license && seen_readme) {
513 bail!("archive missing required files: plugin.toml, plugin.so, LICENSE, README.md");
514 }
515
516 let f = File::open(artifact)?;
518 let dec = flate2::read::GzDecoder::new(f);
519 let mut ar = tar::Archive::new(dec);
520 let root = roots.into_iter().next().unwrap();
521 let manifest_path = Path::new(&root).join("plugin.toml");
522 for entry in ar.entries()? {
523 let mut entry = entry?;
524 if entry.path()? == manifest_path {
525 let mut s = String::new();
526 entry.read_to_string(&mut s)?;
527 let m: Manifest = toml::from_str(&s).context("parsing manifest in archive")?;
528
529 let name = if let Some(pkg) = &m.package {
531 pkg.name.clone()
532 } else {
533 m.name.clone().unwrap_or_default()
534 };
535
536 let version = if let Some(pkg) = &m.package {
537 pkg.version.clone()
538 } else {
539 m.version.clone().unwrap_or_default()
540 };
541
542 let abi_version = if let Some(pkg) = &m.package {
543 pkg.abi_version.clone()
544 } else {
545 m.abi_version.clone().unwrap_or_default()
546 };
547
548 if name.trim().is_empty() || version.trim().is_empty() || abi_version.trim().is_empty()
550 {
551 bail!("manifest missing required fields: name, version, abi_version");
552 }
553 return Ok(());
554 }
555 }
556
557 bail!("manifest not found inside archive");
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use tempfile::tempdir;
565
566 #[test]
567 fn pack_and_verify_roundtrip() -> Result<()> {
568 let dir = tempdir()?;
569 let base = dir.path();
570 fs::write(
572 base.join("plugin.toml"),
573 r#"name = "testplugin"
574version = "0.1.0"
575abi_version = "1"
576entrypoint = "init""#,
577 )?;
578 fs::write(base.join("plugin.so"), b"binary")?;
579 fs::write(base.join("LICENSE"), "MIT")?;
580 fs::write(base.join("README.md"), "readme")?;
581
582 let out = dir.path().join("artifact.tar.gz");
583 let checksum = pack_dir(base, &out)?;
584 verify_artifact(&out, Some(&checksum))?;
586 Ok(())
587 }
588}