1use crate::{BUNDLE_VERSION, BundleError, BundleResult, Platform};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Manifest {
16 pub bundle_version: String,
18
19 pub plugin: PluginInfo,
21
22 pub platforms: HashMap<String, PlatformInfo>,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub build_info: Option<BuildInfo>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub sbom: Option<Sbom>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub schema_checksum: Option<String>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub notices: Option<String>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub license_file: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub public_key: Option<String>,
52
53 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
55 pub schemas: HashMap<String, SchemaInfo>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct PluginInfo {
61 pub name: String,
63
64 pub version: String,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub description: Option<String>,
70
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub authors: Vec<String>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub license: Option<String>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub repository: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PlatformInfo {
90 pub variants: HashMap<String, VariantInfo>,
93}
94
95impl PlatformInfo {
96 pub fn new(library: String, checksum: String) -> Self {
98 let mut variants = HashMap::new();
99 variants.insert(
100 "release".to_string(),
101 VariantInfo {
102 library,
103 checksum,
104 build: None,
105 },
106 );
107 Self { variants }
108 }
109
110 #[must_use]
112 pub fn release(&self) -> Option<&VariantInfo> {
113 self.variants.get("release")
114 }
115
116 #[must_use]
118 pub fn variant(&self, name: &str) -> Option<&VariantInfo> {
119 self.variants.get(name)
120 }
121
122 #[must_use]
124 pub fn default_variant(&self) -> Option<&VariantInfo> {
125 self.release()
126 }
127
128 #[must_use]
130 pub fn variant_names(&self) -> Vec<&str> {
131 self.variants.keys().map(String::as_str).collect()
132 }
133
134 #[must_use]
136 pub fn has_variant(&self, name: &str) -> bool {
137 self.variants.contains_key(name)
138 }
139
140 pub fn add_variant(&mut self, name: String, info: VariantInfo) {
142 self.variants.insert(name, info);
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct VariantInfo {
152 pub library: String,
155
156 pub checksum: String,
159
160 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub build: Option<serde_json::Value>,
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176pub struct BuildInfo {
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub built_by: Option<String>,
180
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub built_at: Option<String>,
184
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub host: Option<String>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub compiler: Option<String>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub rustbridge_version: Option<String>,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub git: Option<GitInfo>,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub custom: Option<HashMap<String, String>>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct GitInfo {
213 pub commit: String,
215
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub branch: Option<String>,
219
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub tag: Option<String>,
223
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub dirty: Option<bool>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Sbom {
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub cyclonedx: Option<String>,
238
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub spdx: Option<String>,
242}
243
244fn is_valid_variant_name(name: &str) -> bool {
249 if name.is_empty() {
250 return false;
251 }
252
253 let chars: Vec<char> = name.chars().collect();
255 if !chars[0].is_ascii_lowercase() && !chars[0].is_ascii_digit() {
256 return false;
257 }
258 if !chars[chars.len() - 1].is_ascii_lowercase() && !chars[chars.len() - 1].is_ascii_digit() {
259 return false;
260 }
261
262 name.chars()
264 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct SchemaInfo {
270 pub path: String,
272
273 pub format: String,
275
276 pub checksum: String,
278
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub description: Option<String>,
282}
283
284impl Manifest {
285 #[must_use]
287 pub fn new(name: &str, version: &str) -> Self {
288 Self {
289 bundle_version: BUNDLE_VERSION.to_string(),
290 plugin: PluginInfo {
291 name: name.to_string(),
292 version: version.to_string(),
293 description: None,
294 authors: Vec::new(),
295 license: None,
296 repository: None,
297 },
298 platforms: HashMap::new(),
299 build_info: None,
300 sbom: None,
301 schema_checksum: None,
302 notices: None,
303 license_file: None,
304 public_key: None,
305 schemas: HashMap::new(),
306 }
307 }
308
309 pub fn add_platform(&mut self, platform: Platform, library_path: &str, checksum: &str) {
314 let platform_key = platform.as_str().to_string();
315
316 if let Some(platform_info) = self.platforms.get_mut(&platform_key) {
317 platform_info.variants.insert(
319 "release".to_string(),
320 VariantInfo {
321 library: library_path.to_string(),
322 checksum: format!("sha256:{checksum}"),
323 build: None,
324 },
325 );
326 } else {
327 self.platforms.insert(
329 platform_key,
330 PlatformInfo::new(library_path.to_string(), format!("sha256:{checksum}")),
331 );
332 }
333 }
334
335 pub fn add_platform_variant(
339 &mut self,
340 platform: Platform,
341 variant: &str,
342 library_path: &str,
343 checksum: &str,
344 build: Option<serde_json::Value>,
345 ) {
346 let platform_key = platform.as_str().to_string();
347
348 let platform_info = self
349 .platforms
350 .entry(platform_key)
351 .or_insert_with(|| PlatformInfo {
352 variants: HashMap::new(),
353 });
354
355 platform_info.variants.insert(
356 variant.to_string(),
357 VariantInfo {
358 library: library_path.to_string(),
359 checksum: format!("sha256:{checksum}"),
360 build,
361 },
362 );
363 }
364
365 pub fn set_public_key(&mut self, public_key: String) {
367 self.public_key = Some(public_key);
368 }
369
370 pub fn add_schema(
372 &mut self,
373 name: String,
374 path: String,
375 format: String,
376 checksum: String,
377 description: Option<String>,
378 ) {
379 self.schemas.insert(
380 name,
381 SchemaInfo {
382 path,
383 format,
384 checksum,
385 description,
386 },
387 );
388 }
389
390 pub fn set_build_info(&mut self, build_info: BuildInfo) {
392 self.build_info = Some(build_info);
393 }
394
395 #[must_use]
397 pub fn get_build_info(&self) -> Option<&BuildInfo> {
398 self.build_info.as_ref()
399 }
400
401 pub fn set_sbom(&mut self, sbom: Sbom) {
403 self.sbom = Some(sbom);
404 }
405
406 #[must_use]
408 pub fn get_sbom(&self) -> Option<&Sbom> {
409 self.sbom.as_ref()
410 }
411
412 pub fn set_schema_checksum(&mut self, checksum: String) {
414 self.schema_checksum = Some(checksum);
415 }
416
417 #[must_use]
419 pub fn get_schema_checksum(&self) -> Option<&str> {
420 self.schema_checksum.as_deref()
421 }
422
423 pub fn set_notices(&mut self, path: String) {
425 self.notices = Some(path);
426 }
427
428 #[must_use]
430 pub fn get_notices(&self) -> Option<&str> {
431 self.notices.as_deref()
432 }
433
434 pub fn set_license_file(&mut self, path: String) {
436 self.license_file = Some(path);
437 }
438
439 #[must_use]
441 pub fn get_license_file(&self) -> Option<&str> {
442 self.license_file.as_deref()
443 }
444
445 #[must_use]
449 pub fn get_variant(&self, platform: Platform, variant: Option<&str>) -> Option<&VariantInfo> {
450 let platform_info = self.platforms.get(platform.as_str())?;
451 let variant_name = variant.unwrap_or("release");
452 platform_info.variants.get(variant_name)
453 }
454
455 #[must_use]
457 pub fn get_release_variant(&self, platform: Platform) -> Option<&VariantInfo> {
458 self.get_variant(platform, Some("release"))
459 }
460
461 #[must_use]
463 pub fn list_variants(&self, platform: Platform) -> Vec<&str> {
464 self.platforms
465 .get(platform.as_str())
466 .map(|p| p.variant_names())
467 .unwrap_or_default()
468 }
469
470 #[must_use]
472 pub fn get_platform(&self, platform: Platform) -> Option<&PlatformInfo> {
473 self.platforms.get(platform.as_str())
474 }
475
476 #[must_use]
478 pub fn supports_platform(&self, platform: Platform) -> bool {
479 self.platforms.contains_key(platform.as_str())
480 }
481
482 #[must_use]
484 pub fn supported_platforms(&self) -> Vec<Platform> {
485 self.platforms
486 .keys()
487 .filter_map(|k| Platform::parse(k))
488 .collect()
489 }
490
491 pub fn validate(&self) -> BundleResult<()> {
493 if self.bundle_version.is_empty() {
495 return Err(BundleError::InvalidManifest(
496 "bundle_version is required".to_string(),
497 ));
498 }
499
500 if self.plugin.name.is_empty() {
502 return Err(BundleError::InvalidManifest(
503 "plugin.name is required".to_string(),
504 ));
505 }
506
507 if self.plugin.version.is_empty() {
509 return Err(BundleError::InvalidManifest(
510 "plugin.version is required".to_string(),
511 ));
512 }
513
514 if self.platforms.is_empty() {
516 return Err(BundleError::InvalidManifest(
517 "at least one platform must be defined".to_string(),
518 ));
519 }
520
521 for (key, info) in &self.platforms {
523 if Platform::parse(key).is_none() {
524 return Err(BundleError::InvalidManifest(format!(
525 "unknown platform: {key}"
526 )));
527 }
528
529 if info.variants.is_empty() {
531 return Err(BundleError::InvalidManifest(format!(
532 "platform {key}: at least one variant is required"
533 )));
534 }
535
536 if !info.variants.contains_key("release") {
538 return Err(BundleError::InvalidManifest(format!(
539 "platform {key}: 'release' variant is required"
540 )));
541 }
542
543 for (variant_name, variant_info) in &info.variants {
545 if !is_valid_variant_name(variant_name) {
547 return Err(BundleError::InvalidManifest(format!(
548 "platform {key}: invalid variant name '{variant_name}' \
549 (must be lowercase alphanumeric with hyphens)"
550 )));
551 }
552
553 if variant_info.library.is_empty() {
554 return Err(BundleError::InvalidManifest(format!(
555 "platform {key}, variant {variant_name}: library path is required"
556 )));
557 }
558
559 if variant_info.checksum.is_empty() {
560 return Err(BundleError::InvalidManifest(format!(
561 "platform {key}, variant {variant_name}: checksum is required"
562 )));
563 }
564
565 if !variant_info.checksum.starts_with("sha256:") {
566 return Err(BundleError::InvalidManifest(format!(
567 "platform {key}, variant {variant_name}: checksum must start with 'sha256:'"
568 )));
569 }
570 }
571 }
572
573 Ok(())
574 }
575
576 pub fn to_json(&self) -> BundleResult<String> {
578 Ok(serde_json::to_string_pretty(self)?)
579 }
580
581 pub fn from_json(json: &str) -> BundleResult<Self> {
583 Ok(serde_json::from_str(json)?)
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 #![allow(non_snake_case)]
590
591 use super::*;
592
593 #[test]
594 fn Manifest___new___creates_valid_minimal_manifest() {
595 let manifest = Manifest::new("test-plugin", "1.0.0");
596
597 assert_eq!(manifest.plugin.name, "test-plugin");
598 assert_eq!(manifest.plugin.version, "1.0.0");
599 assert_eq!(manifest.bundle_version, BUNDLE_VERSION);
600 assert!(manifest.platforms.is_empty());
601 }
602
603 #[test]
604 fn Manifest___add_platform___adds_platform_info() {
605 let mut manifest = Manifest::new("test-plugin", "1.0.0");
606 manifest.add_platform(
607 Platform::LinuxX86_64,
608 "lib/linux-x86_64/libtest.so",
609 "abc123",
610 );
611
612 assert!(manifest.supports_platform(Platform::LinuxX86_64));
613 assert!(!manifest.supports_platform(Platform::WindowsX86_64));
614
615 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
616 let release = info.release().unwrap();
617 assert_eq!(release.library, "lib/linux-x86_64/libtest.so");
618 assert_eq!(release.checksum, "sha256:abc123");
619 }
620
621 #[test]
622 fn Manifest___add_platform___overwrites_existing() {
623 let mut manifest = Manifest::new("test", "1.0.0");
624 manifest.add_platform(Platform::LinuxX86_64, "lib/old.so", "old");
625 manifest.add_platform(Platform::LinuxX86_64, "lib/new.so", "new");
626
627 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
628 let release = info.release().unwrap();
629 assert_eq!(release.library, "lib/new.so");
630 assert_eq!(release.checksum, "sha256:new");
631 }
632
633 #[test]
634 fn Manifest___validate___rejects_empty_name() {
635 let manifest = Manifest::new("", "1.0.0");
636 let result = manifest.validate();
637
638 assert!(result.is_err());
639 assert!(result.unwrap_err().to_string().contains("plugin.name"));
640 }
641
642 #[test]
643 fn Manifest___validate___rejects_empty_version() {
644 let manifest = Manifest::new("test", "");
645 let result = manifest.validate();
646
647 assert!(result.is_err());
648 assert!(result.unwrap_err().to_string().contains("plugin.version"));
649 }
650
651 #[test]
652 fn Manifest___validate___rejects_empty_platforms() {
653 let manifest = Manifest::new("test", "1.0.0");
654 let result = manifest.validate();
655
656 assert!(result.is_err());
657 assert!(
658 result
659 .unwrap_err()
660 .to_string()
661 .contains("at least one platform")
662 );
663 }
664
665 #[test]
666 fn Manifest___validate___rejects_invalid_checksum_format() {
667 let mut manifest = Manifest::new("test", "1.0.0");
668 let mut variants = HashMap::new();
670 variants.insert(
671 "release".to_string(),
672 VariantInfo {
673 library: "lib/test.so".to_string(),
674 checksum: "abc123".to_string(), build: None,
676 },
677 );
678 manifest
679 .platforms
680 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
681
682 let result = manifest.validate();
683
684 assert!(result.is_err());
685 assert!(result.unwrap_err().to_string().contains("sha256:"));
686 }
687
688 #[test]
689 fn Manifest___validate___rejects_unknown_platform() {
690 let mut manifest = Manifest::new("test", "1.0.0");
691 manifest.platforms.insert(
693 "invalid-platform".to_string(),
694 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string()),
695 );
696
697 let result = manifest.validate();
698
699 assert!(result.is_err());
700 assert!(result.unwrap_err().to_string().contains("unknown platform"));
701 }
702
703 #[test]
704 fn Manifest___validate___rejects_empty_library_path() {
705 let mut manifest = Manifest::new("test", "1.0.0");
706 let mut variants = HashMap::new();
707 variants.insert(
708 "release".to_string(),
709 VariantInfo {
710 library: "".to_string(),
711 checksum: "sha256:abc123".to_string(),
712 build: None,
713 },
714 );
715 manifest
716 .platforms
717 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
718
719 let result = manifest.validate();
720
721 assert!(result.is_err());
722 assert!(
723 result
724 .unwrap_err()
725 .to_string()
726 .contains("library path is required")
727 );
728 }
729
730 #[test]
731 fn Manifest___validate___rejects_empty_checksum() {
732 let mut manifest = Manifest::new("test", "1.0.0");
733 let mut variants = HashMap::new();
734 variants.insert(
735 "release".to_string(),
736 VariantInfo {
737 library: "lib/test.so".to_string(),
738 checksum: "".to_string(),
739 build: None,
740 },
741 );
742 manifest
743 .platforms
744 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
745
746 let result = manifest.validate();
747
748 assert!(result.is_err());
749 assert!(
750 result
751 .unwrap_err()
752 .to_string()
753 .contains("checksum is required")
754 );
755 }
756
757 #[test]
758 fn Manifest___validate___rejects_missing_release_variant() {
759 let mut manifest = Manifest::new("test", "1.0.0");
760 let mut variants = HashMap::new();
761 variants.insert(
762 "debug".to_string(), VariantInfo {
764 library: "lib/test.so".to_string(),
765 checksum: "sha256:abc123".to_string(),
766 build: None,
767 },
768 );
769 manifest
770 .platforms
771 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
772
773 let result = manifest.validate();
774
775 assert!(result.is_err());
776 assert!(
777 result
778 .unwrap_err()
779 .to_string()
780 .contains("'release' variant is required")
781 );
782 }
783
784 #[test]
785 fn Manifest___validate___rejects_invalid_variant_name() {
786 let mut manifest = Manifest::new("test", "1.0.0");
787 let mut variants = HashMap::new();
788 variants.insert(
789 "release".to_string(),
790 VariantInfo {
791 library: "lib/test.so".to_string(),
792 checksum: "sha256:abc123".to_string(),
793 build: None,
794 },
795 );
796 variants.insert(
797 "INVALID".to_string(), VariantInfo {
799 library: "lib/test.so".to_string(),
800 checksum: "sha256:abc123".to_string(),
801 build: None,
802 },
803 );
804 manifest
805 .platforms
806 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
807
808 let result = manifest.validate();
809
810 assert!(result.is_err());
811 assert!(
812 result
813 .unwrap_err()
814 .to_string()
815 .contains("invalid variant name")
816 );
817 }
818
819 #[test]
820 fn Manifest___validate___accepts_valid_manifest() {
821 let mut manifest = Manifest::new("test-plugin", "1.0.0");
822 manifest.add_platform(
823 Platform::LinuxX86_64,
824 "lib/linux-x86_64/libtest.so",
825 "abc123",
826 );
827
828 assert!(manifest.validate().is_ok());
829 }
830
831 #[test]
832 fn Manifest___validate___accepts_all_platforms() {
833 let mut manifest = Manifest::new("all-platforms", "1.0.0");
834 for platform in Platform::all() {
835 manifest.add_platform(
836 *platform,
837 &format!("lib/{}/libtest", platform.as_str()),
838 "hash",
839 );
840 }
841
842 assert!(manifest.validate().is_ok());
843 assert_eq!(manifest.supported_platforms().len(), 6);
844 }
845
846 #[test]
847 fn Manifest___json_roundtrip___preserves_data() {
848 let mut manifest = Manifest::new("test-plugin", "1.0.0");
849 manifest.plugin.description = Some("A test plugin".to_string());
850 manifest.add_platform(
851 Platform::LinuxX86_64,
852 "lib/linux-x86_64/libtest.so",
853 "abc123",
854 );
855 manifest.add_platform(
856 Platform::DarwinAarch64,
857 "lib/darwin-aarch64/libtest.dylib",
858 "def456",
859 );
860
861 let json = manifest.to_json().unwrap();
862 let parsed = Manifest::from_json(&json).unwrap();
863
864 assert_eq!(parsed.plugin.name, manifest.plugin.name);
865 assert_eq!(parsed.plugin.version, manifest.plugin.version);
866 assert_eq!(parsed.plugin.description, manifest.plugin.description);
867 assert_eq!(parsed.platforms.len(), 2);
868 }
869
870 #[test]
871 fn Manifest___json_roundtrip___preserves_all_plugin_fields() {
872 let mut manifest = Manifest::new("full-plugin", "2.3.4");
873 manifest.plugin.description = Some("Full description".to_string());
874 manifest.plugin.authors = vec!["Author 1".to_string(), "Author 2".to_string()];
875 manifest.plugin.license = Some("Apache-2.0".to_string());
876 manifest.plugin.repository = Some("https://github.com/test/repo".to_string());
877 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
878
879 let json = manifest.to_json().unwrap();
880 let parsed = Manifest::from_json(&json).unwrap();
881
882 assert_eq!(parsed.plugin.description, manifest.plugin.description);
883 assert_eq!(parsed.plugin.authors, manifest.plugin.authors);
884 assert_eq!(parsed.plugin.license, manifest.plugin.license);
885 assert_eq!(parsed.plugin.repository, manifest.plugin.repository);
886 }
887
888 #[test]
889 fn Manifest___json_roundtrip___preserves_schemas() {
890 let mut manifest = Manifest::new("schema-plugin", "1.0.0");
891 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
892 manifest.add_schema(
893 "messages.h".to_string(),
894 "schema/messages.h".to_string(),
895 "c-header".to_string(),
896 "sha256:abc".to_string(),
897 Some("C header for binary transport".to_string()),
898 );
899
900 let json = manifest.to_json().unwrap();
901 let parsed = Manifest::from_json(&json).unwrap();
902
903 assert_eq!(parsed.schemas.len(), 1);
904 let schema = parsed.schemas.get("messages.h").unwrap();
905 assert_eq!(schema.path, "schema/messages.h");
906 assert_eq!(schema.format, "c-header");
907 assert_eq!(schema.checksum, "sha256:abc");
908 assert_eq!(
909 schema.description,
910 Some("C header for binary transport".to_string())
911 );
912 }
913
914 #[test]
915 fn Manifest___json_roundtrip___preserves_public_key() {
916 let mut manifest = Manifest::new("signed-plugin", "1.0.0");
917 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
918 manifest.set_public_key("RWSxxxxxxxxxxxxxxxx".to_string());
919
920 let json = manifest.to_json().unwrap();
921 let parsed = Manifest::from_json(&json).unwrap();
922
923 assert_eq!(parsed.public_key, Some("RWSxxxxxxxxxxxxxxxx".to_string()));
924 }
925
926 #[test]
927 fn Manifest___from_json___invalid_json___returns_error() {
928 let result = Manifest::from_json("{ invalid }");
929
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn Manifest___from_json___missing_required_fields___returns_error() {
935 let result = Manifest::from_json(r#"{"bundle_version": "1.0"}"#);
936
937 assert!(result.is_err());
938 }
939
940 #[test]
941 fn Manifest___supported_platforms___returns_all_platforms() {
942 let mut manifest = Manifest::new("test", "1.0.0");
943 manifest.add_platform(Platform::LinuxX86_64, "lib/a.so", "a");
944 manifest.add_platform(Platform::DarwinAarch64, "lib/b.dylib", "b");
945
946 let platforms = manifest.supported_platforms();
947 assert_eq!(platforms.len(), 2);
948 assert!(platforms.contains(&Platform::LinuxX86_64));
949 assert!(platforms.contains(&Platform::DarwinAarch64));
950 }
951
952 #[test]
953 fn Manifest___get_platform___returns_none_for_unsupported() {
954 let mut manifest = Manifest::new("test", "1.0.0");
955 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
956
957 assert!(manifest.get_platform(Platform::LinuxX86_64).is_some());
958 assert!(manifest.get_platform(Platform::WindowsX86_64).is_none());
959 }
960
961 #[test]
962 fn BuildInfo___default___all_fields_none() {
963 let build_info = BuildInfo::default();
964
965 assert!(build_info.built_by.is_none());
966 assert!(build_info.built_at.is_none());
967 assert!(build_info.host.is_none());
968 assert!(build_info.compiler.is_none());
969 assert!(build_info.rustbridge_version.is_none());
970 assert!(build_info.git.is_none());
971 }
972
973 #[test]
974 fn Manifest___build_info___roundtrip() {
975 let mut manifest = Manifest::new("test", "1.0.0");
976 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
977 manifest.set_build_info(BuildInfo {
978 built_by: Some("GitHub Actions".to_string()),
979 built_at: Some("2025-01-26T10:30:00Z".to_string()),
980 host: Some("x86_64-unknown-linux-gnu".to_string()),
981 compiler: Some("rustc 1.90.0".to_string()),
982 rustbridge_version: Some("0.2.0".to_string()),
983 git: Some(GitInfo {
984 commit: "abc123".to_string(),
985 branch: Some("main".to_string()),
986 tag: Some("v1.0.0".to_string()),
987 dirty: Some(false),
988 }),
989 custom: None,
990 });
991
992 let json = manifest.to_json().unwrap();
993 let parsed = Manifest::from_json(&json).unwrap();
994
995 let build_info = parsed.get_build_info().unwrap();
996 assert_eq!(build_info.built_by, Some("GitHub Actions".to_string()));
997 assert_eq!(build_info.compiler, Some("rustc 1.90.0".to_string()));
998
999 let git = build_info.git.as_ref().unwrap();
1000 assert_eq!(git.commit, "abc123");
1001 assert_eq!(git.branch, Some("main".to_string()));
1002 assert_eq!(git.dirty, Some(false));
1003 }
1004
1005 #[test]
1006 fn Manifest___sbom___roundtrip() {
1007 let mut manifest = Manifest::new("test", "1.0.0");
1008 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1009 manifest.set_sbom(Sbom {
1010 cyclonedx: Some("sbom/sbom.cdx.json".to_string()),
1011 spdx: Some("sbom/sbom.spdx.json".to_string()),
1012 });
1013
1014 let json = manifest.to_json().unwrap();
1015 let parsed = Manifest::from_json(&json).unwrap();
1016
1017 let sbom = parsed.get_sbom().unwrap();
1018 assert_eq!(sbom.cyclonedx, Some("sbom/sbom.cdx.json".to_string()));
1019 assert_eq!(sbom.spdx, Some("sbom/sbom.spdx.json".to_string()));
1020 }
1021
1022 #[test]
1023 fn Manifest___variants___roundtrip() {
1024 let mut manifest = Manifest::new("test", "1.0.0");
1025 manifest.add_platform_variant(
1026 Platform::LinuxX86_64,
1027 "release",
1028 "lib/linux-x86_64/release/libtest.so",
1029 "hash1",
1030 Some(serde_json::json!({
1031 "profile": "release",
1032 "opt_level": "3"
1033 })),
1034 );
1035 manifest.add_platform_variant(
1036 Platform::LinuxX86_64,
1037 "debug",
1038 "lib/linux-x86_64/debug/libtest.so",
1039 "hash2",
1040 Some(serde_json::json!({
1041 "profile": "debug",
1042 "opt_level": "0"
1043 })),
1044 );
1045
1046 let json = manifest.to_json().unwrap();
1047 let parsed = Manifest::from_json(&json).unwrap();
1048
1049 let variants = parsed.list_variants(Platform::LinuxX86_64);
1050 assert_eq!(variants.len(), 2);
1051 assert!(variants.contains(&"release"));
1052 assert!(variants.contains(&"debug"));
1053
1054 let release = parsed
1055 .get_variant(Platform::LinuxX86_64, Some("release"))
1056 .unwrap();
1057 assert_eq!(release.library, "lib/linux-x86_64/release/libtest.so");
1058 assert_eq!(
1059 release.build.as_ref().unwrap()["profile"],
1060 serde_json::json!("release")
1061 );
1062 }
1063
1064 #[test]
1065 fn Manifest___schema_checksum___roundtrip() {
1066 let mut manifest = Manifest::new("test", "1.0.0");
1067 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1068 manifest.set_schema_checksum("sha256:abcdef123456".to_string());
1069
1070 let json = manifest.to_json().unwrap();
1071 let parsed = Manifest::from_json(&json).unwrap();
1072
1073 assert_eq!(parsed.get_schema_checksum(), Some("sha256:abcdef123456"));
1074 }
1075
1076 #[test]
1077 fn Manifest___notices___roundtrip() {
1078 let mut manifest = Manifest::new("test", "1.0.0");
1079 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1080 manifest.set_notices("docs/NOTICES.txt".to_string());
1081
1082 let json = manifest.to_json().unwrap();
1083 let parsed = Manifest::from_json(&json).unwrap();
1084
1085 assert_eq!(parsed.get_notices(), Some("docs/NOTICES.txt"));
1086 }
1087
1088 #[test]
1089 fn Manifest___license_file___roundtrip() {
1090 let mut manifest = Manifest::new("test", "1.0.0");
1091 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1092 manifest.set_license_file("legal/LICENSE".to_string());
1093
1094 let json = manifest.to_json().unwrap();
1095 let parsed = Manifest::from_json(&json).unwrap();
1096
1097 assert_eq!(parsed.get_license_file(), Some("legal/LICENSE"));
1098 }
1099
1100 #[test]
1101 fn is_valid_variant_name___accepts_valid_names() {
1102 assert!(is_valid_variant_name("release"));
1103 assert!(is_valid_variant_name("debug"));
1104 assert!(is_valid_variant_name("nightly"));
1105 assert!(is_valid_variant_name("opt-size"));
1106 assert!(is_valid_variant_name("v1"));
1107 assert!(is_valid_variant_name("build123"));
1108 }
1109
1110 #[test]
1111 fn is_valid_variant_name___rejects_invalid_names() {
1112 assert!(!is_valid_variant_name("")); assert!(!is_valid_variant_name("RELEASE")); assert!(!is_valid_variant_name("Release")); assert!(!is_valid_variant_name("-debug")); assert!(!is_valid_variant_name("debug-")); assert!(!is_valid_variant_name("debug build")); assert!(!is_valid_variant_name("debug_build")); }
1120
1121 #[test]
1122 fn PlatformInfo___new___creates_release_variant() {
1123 let platform_info =
1124 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string());
1125
1126 assert!(platform_info.has_variant("release"));
1127 let release = platform_info.release().unwrap();
1128 assert_eq!(release.library, "lib/test.so");
1129 assert_eq!(release.checksum, "sha256:abc123");
1130 }
1131
1132 #[test]
1133 fn PlatformInfo___variant_names___returns_all_variants() {
1134 let mut platform_info =
1135 PlatformInfo::new("lib/release.so".to_string(), "sha256:abc".to_string());
1136 platform_info.add_variant(
1137 "debug".to_string(),
1138 VariantInfo {
1139 library: "lib/debug.so".to_string(),
1140 checksum: "sha256:def".to_string(),
1141 build: None,
1142 },
1143 );
1144
1145 let names = platform_info.variant_names();
1146 assert_eq!(names.len(), 2);
1147 assert!(names.contains(&"release"));
1148 assert!(names.contains(&"debug"));
1149 }
1150}